builder: a clone may offer the forge's credential, through git's own store
A private repository could not be built: the builder clones anonymously, and had no way to say who it is. It already holds exactly one credential to exactly the right place — the package-registry binding and its sealed secret, one gitea user whose password answers npm and git alike — so a clone now offers that, and nothing new is minted or carried. Offered, never pushed: the credential is written as a git credential-store file (0600, in the workspace, never argv) and named with -c credential.helper, so git itself decides when it applies — only on an authentication challenge, and only for the URL it was written for, scheme, host and port included. A public repository clones exactly as before; a repository on any other host is never shown it. The same store rides along on an artifact's own context clone, so a private module with a private context builds too.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -202,6 +203,7 @@ func answer(ctx context.Context, channel *amqp.Channel, publisher builder.Publis
|
|||||||
// not after a clone that then fails at npm ci.
|
// not after a clone that then fails at npm ci.
|
||||||
built, err = builder.Build(ctx, builder.Command, publisher,
|
built, err = builder.Build(ctx, builder.Command, publisher,
|
||||||
request.Repository, request.Path, request.Ref, workspace, request.Held, npmrc,
|
request.Repository, request.Path, request.Ref, workspace, request.Held, npmrc,
|
||||||
|
forgeFrom(),
|
||||||
func(step, message string) {
|
func(step, message string) {
|
||||||
fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message)
|
fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message)
|
||||||
})
|
})
|
||||||
@@ -367,6 +369,53 @@ func packagesFrom() (builder.Npmrc, error) {
|
|||||||
return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil
|
return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forgeFrom is the git credential this builder may offer a clone, composed from the same binding
|
||||||
|
// and sealed secret its package-registry half already reads: the forge that answers npm is the
|
||||||
|
// forge that hosts the repositories, and its provisioner applies one password to one user for
|
||||||
|
// both. Anything missing means no credential, and every clone stays anonymous — which is all a
|
||||||
|
// mesh of public repositories ever needs.
|
||||||
|
//
|
||||||
|
// The URL names the binding's own address — the machine the mesh says the forge is on — so a
|
||||||
|
// private repository is registered and built by that address, and a clone of anything else is
|
||||||
|
// never shown this credential (git's credential store matches the whole origin).
|
||||||
|
func forgeFrom() builder.GitCredential {
|
||||||
|
path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING"))
|
||||||
|
if path == "" {
|
||||||
|
return builder.GitCredential{}
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return builder.GitCredential{}
|
||||||
|
}
|
||||||
|
var told struct {
|
||||||
|
At string `json:"at"`
|
||||||
|
As string `json:"as"`
|
||||||
|
Serves map[string]any `json:"serves"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &told); err != nil || told.At == "" || told.As == "" {
|
||||||
|
return builder.GitCredential{}
|
||||||
|
}
|
||||||
|
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
||||||
|
if file := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); file != "" {
|
||||||
|
if raw, err := os.ReadFile(file); err == nil {
|
||||||
|
secret = strings.TrimSpace(string(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if secret == "" {
|
||||||
|
return builder.GitCredential{}
|
||||||
|
}
|
||||||
|
scheme := "https"
|
||||||
|
if s, ok := told.Serves["scheme"]; ok {
|
||||||
|
scheme = fmt.Sprintf("%v", s)
|
||||||
|
}
|
||||||
|
host := told.At
|
||||||
|
if port, ok := told.Serves["port"]; ok {
|
||||||
|
host = fmt.Sprintf("%s:%v", told.At, port)
|
||||||
|
}
|
||||||
|
made := url.URL{Scheme: scheme, User: url.UserPassword(told.As, secret), Host: host}
|
||||||
|
return builder.GitCredential{URL: made.String()}
|
||||||
|
}
|
||||||
|
|
||||||
func short(commit string) string {
|
func short(commit string) string {
|
||||||
if len(commit) > 8 {
|
if len(commit) > 8 {
|
||||||
return commit[:8]
|
return commit[:8]
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ func buildOnce(ctx context.Context, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
||||||
|
forgeFrom(),
|
||||||
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
||||||
if buildErr != nil {
|
if buildErr != nil {
|
||||||
return buildErr
|
return buildErr
|
||||||
|
|||||||
@@ -63,6 +63,19 @@ type Result struct {
|
|||||||
Built []catalogue.Built
|
Built []catalogue.Built
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitCredential is the forge credential a clone may present when the server asks for one.
|
||||||
|
//
|
||||||
|
// **Offered, never pushed.** It is written as a git credential-store file and named to git with
|
||||||
|
// `-c credential.helper=store`, so git itself decides when it applies: only on an authentication
|
||||||
|
// challenge, and only for the URL it was written for — scheme, host and port included. A public
|
||||||
|
// repository clones exactly as before, and a repository on any other host is never shown it.
|
||||||
|
type GitCredential struct {
|
||||||
|
// URL is the credential-store line — scheme://user:password@host[:port] — naming the one
|
||||||
|
// server this credential belongs to. Empty means the builder holds none and every clone is
|
||||||
|
// anonymous, as it always was.
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
// Build clones a repository at a ref, reads its manifest, produces what it declares, publishes
|
// Build clones a repository at a ref, reads its manifest, produces what it declares, publishes
|
||||||
// each, and returns the manifest the mesh should hold.
|
// each, and returns the manifest the mesh should hold.
|
||||||
//
|
//
|
||||||
@@ -70,7 +83,8 @@ type Result struct {
|
|||||||
// archive failed would otherwise leave half of itself in the store under a digest the mesh never
|
// archive failed would otherwise leave half of itself in the store under a digest the mesh never
|
||||||
// records — reachable, unreferenced, and indistinguishable from something in use.
|
// records — reachable, unreferenced, and indistinguishable from something in use.
|
||||||
func Build(ctx context.Context, run Runner, publish Publisher,
|
func Build(ctx context.Context, run Runner, publish Publisher,
|
||||||
repository, path, ref, workspace string, held map[string]string, npmrc Npmrc, log Log) (Result, error) {
|
repository, path, ref, workspace string, held map[string]string, npmrc Npmrc,
|
||||||
|
forge GitCredential, log Log) (Result, error) {
|
||||||
|
|
||||||
say := logging(log)
|
say := logging(log)
|
||||||
say("clone", "%s%s at %s", repository, describePath(path), refOrHead(ref))
|
say("clone", "%s%s at %s", repository, describePath(path), refOrHead(ref))
|
||||||
@@ -80,6 +94,15 @@ func Build(ctx context.Context, run Runner, publish Publisher,
|
|||||||
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
}
|
}
|
||||||
|
// The credential is a file git reads, never an argument: a URL carrying a password in argv
|
||||||
|
// would be readable by anything that can list processes for as long as a clone runs.
|
||||||
|
credentials := ""
|
||||||
|
if forge.URL != "" {
|
||||||
|
credentials = filepath.Join(workspace, "git-credentials")
|
||||||
|
if err := os.WriteFile(credentials, []byte(forge.URL+"\n"), 0o600); err != nil {
|
||||||
|
return Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
tree := filepath.Join(workspace, "source")
|
tree := filepath.Join(workspace, "source")
|
||||||
if err := os.RemoveAll(tree); err != nil {
|
if err := os.RemoveAll(tree); err != nil {
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
@@ -87,7 +110,7 @@ func Build(ctx context.Context, run Runner, publish Publisher,
|
|||||||
// A fresh clone every time rather than a fetch into a tree that is already there. A build
|
// A fresh clone every time rather than a fetch into a tree that is already there. A build
|
||||||
// that reuses a working tree can succeed because of something a previous build left behind,
|
// that reuses a working tree can succeed because of something a previous build left behind,
|
||||||
// and that is a build nobody can reproduce.
|
// and that is a build nobody can reproduce.
|
||||||
if _, err := run(ctx, workspace, "git", "clone", "--quiet", repository, tree); err != nil {
|
if _, err := run(ctx, workspace, "git", cloneWith(credentials, "clone", "--quiet", repository, tree)...); err != nil {
|
||||||
say("clone", "FAILED: %v", err)
|
say("clone", "FAILED: %v", err)
|
||||||
return Result{}, fmt.Errorf("cannot clone %s: %w", repository, err)
|
return Result{}, fmt.Errorf("cannot clone %s: %w", repository, err)
|
||||||
}
|
}
|
||||||
@@ -177,7 +200,7 @@ func Build(ctx context.Context, run Runner, publish Publisher,
|
|||||||
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
||||||
for _, a := range artifacts {
|
for _, a := range artifacts {
|
||||||
say("artifact", "%s (%s%s) — starting", a.Name, a.Kind, langSuffix(a))
|
say("artifact", "%s (%s%s) — starting", a.Name, a.Kind, langSuffix(a))
|
||||||
made, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, a, args, held, npmrcPath, say)
|
made, err := one(ctx, run, publish, manifest.Module, within, workspace, commit, credentials, a, args, held, npmrcPath, say)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
say("artifact", "%s FAILED: %v", a.Name, err)
|
say("artifact", "%s FAILED: %v", a.Name, err)
|
||||||
return Result{}, err
|
return Result{}, err
|
||||||
@@ -213,14 +236,14 @@ func logging(log Log) func(step, format string, args ...any) {
|
|||||||
// contextFrom clones an image artifact's own build context, when it names one apart from this
|
// contextFrom clones an image artifact's own build context, when it names one apart from this
|
||||||
// module's own repository — a fresh tree, the same way the module's own is, keyed by artifact
|
// module's own repository — a fresh tree, the same way the module's own is, keyed by artifact
|
||||||
// name so two artifacts of one module naming different contexts do not collide.
|
// name so two artifacts of one module naming different contexts do not collide.
|
||||||
func contextFrom(ctx context.Context, run Runner, workspace, artifact string,
|
func contextFrom(ctx context.Context, run Runner, workspace, artifact, credentials string,
|
||||||
from catalogue.ArtifactContext, say func(step, format string, args ...any)) (string, error) {
|
from catalogue.ArtifactContext, say func(step, format string, args ...any)) (string, error) {
|
||||||
say("context", "cloning %s at %s for %s", from.Repository, refOrHead(from.Ref), artifact)
|
say("context", "cloning %s at %s for %s", from.Repository, refOrHead(from.Ref), artifact)
|
||||||
dir := filepath.Join(workspace, "context-"+artifact)
|
dir := filepath.Join(workspace, "context-"+artifact)
|
||||||
if err := os.RemoveAll(dir); err != nil {
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if _, err := run(ctx, workspace, "git", "clone", "--quiet", from.Repository, dir); err != nil {
|
if _, err := run(ctx, workspace, "git", cloneWith(credentials, "clone", "--quiet", from.Repository, dir)...); err != nil {
|
||||||
return "", fmt.Errorf("cannot clone %s: %w", from.Repository, err)
|
return "", fmt.Errorf("cannot clone %s: %w", from.Repository, err)
|
||||||
}
|
}
|
||||||
if from.Ref != "" {
|
if from.Ref != "" {
|
||||||
@@ -232,6 +255,21 @@ func contextFrom(ctx context.Context, run Runner, workspace, artifact string,
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cloneWith is a git invocation that may offer a stored credential.
|
||||||
|
//
|
||||||
|
// The first `-c credential.helper=` clears every helper the environment might carry, so exactly
|
||||||
|
// one place answers an authentication challenge: the file the builder wrote. Without a file, the
|
||||||
|
// invocation is exactly what it always was.
|
||||||
|
func cloneWith(credentials string, rest ...string) []string {
|
||||||
|
if credentials == "" {
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
return append([]string{
|
||||||
|
"-c", "credential.helper=",
|
||||||
|
"-c", "credential.helper=store --file=" + credentials,
|
||||||
|
}, rest...)
|
||||||
|
}
|
||||||
|
|
||||||
func describePath(path string) string {
|
func describePath(path string) string {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -355,7 +393,7 @@ func wantsPackages(manifest catalogue.Manifest, within string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func one(ctx context.Context, run Runner, publish Publisher,
|
func one(ctx context.Context, run Runner, publish Publisher,
|
||||||
module, tree, workspace, commit string, a catalogue.Artifact, args []string,
|
module, tree, workspace, commit, credentials string, a catalogue.Artifact, args []string,
|
||||||
held map[string]string, npmrc string, say func(step, format string, args ...any)) (catalogue.Built, error) {
|
held map[string]string, npmrc string, say func(step, format string, args ...any)) (catalogue.Built, error) {
|
||||||
|
|
||||||
switch a.Kind {
|
switch a.Kind {
|
||||||
@@ -433,7 +471,7 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
|||||||
recipePath := a.From
|
recipePath := a.From
|
||||||
buildDir := tree
|
buildDir := tree
|
||||||
if a.Context != nil {
|
if a.Context != nil {
|
||||||
cloned, err := contextFrom(ctx, run, workspace, a.Name, *a.Context, say)
|
cloned, err := contextFrom(ctx, run, workspace, a.Name, credentials, *a.Context, say)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return catalogue.Built{}, fmt.Errorf("%s: %s's context: %w", module, a.Name, err)
|
return catalogue.Built{}, fmt.Errorf("%s: %s's context: %w", module, a.Name, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,17 @@ func (r *recorded) run(_ context.Context, dir, name string, args ...string) (str
|
|||||||
line := name + " " + strings.Join(args, " ")
|
line := name + " " + strings.Join(args, " ")
|
||||||
r.ran = append(r.ran, line)
|
r.ran = append(r.ran, line)
|
||||||
r.dirs = append(r.dirs, dir)
|
r.dirs = append(r.dirs, dir)
|
||||||
|
// A clone may carry `-c` configuration in front of the verb — the credential store — so the
|
||||||
|
// verb is found rather than assumed first.
|
||||||
|
isClone := false
|
||||||
|
for _, a := range args {
|
||||||
|
if a == "clone" {
|
||||||
|
isClone = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case name == "git" && len(args) > 0 && args[0] == "clone":
|
case name == "git" && isClone:
|
||||||
repository := args[len(args)-2]
|
repository := args[len(args)-2]
|
||||||
tree := args[len(args)-1]
|
tree := args[len(args)-1]
|
||||||
if err := os.MkdirAll(tree, 0o755); err != nil {
|
if err := os.MkdirAll(tree, 0o755); err != nil {
|
||||||
@@ -119,7 +128,7 @@ func TestABuildProducesAManifestThePinsAreIn(t *testing.T) {
|
|||||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||||
})
|
})
|
||||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, nil)
|
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -145,7 +154,7 @@ func TestTwoBuildsOfOneCommitProduceOneDigest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
// A year apart, so a packer carrying timestamps cannot accidentally agree.
|
// A year apart, so a packer carrying timestamps cannot accidentally agree.
|
||||||
r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC)
|
r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC)
|
||||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil)
|
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -165,7 +174,7 @@ func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) {
|
|||||||
// unreferenced, and indistinguishable from something in use.
|
// unreferenced, and indistinguishable from something in use.
|
||||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"})
|
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"})
|
||||||
// `files` is missing, so packing the archive fails — after the image would have been pushed.
|
// `files` is missing, so packing the archive fails — after the image would have been pushed.
|
||||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil)
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a build with a missing input succeeded")
|
t.Fatal("a build with a missing input succeeded")
|
||||||
}
|
}
|
||||||
@@ -177,7 +186,7 @@ func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) {
|
|||||||
func TestARepositoryWithNoManifestSaysSo(t *testing.T) {
|
func TestARepositoryWithNoManifestSaysSo(t *testing.T) {
|
||||||
workspace := t.TempDir()
|
workspace := t.TempDir()
|
||||||
r := &recorded{contents: map[string]string{"README.md": "nothing to see"}}
|
r := &recorded{contents: map[string]string{"README.md": "nothing to see"}}
|
||||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil)
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a repository with nothing saying what it is was built")
|
t.Fatal("a repository with nothing saying what it is was built")
|
||||||
}
|
}
|
||||||
@@ -190,7 +199,7 @@ func TestAModuleThatBuildsNothingStillProducesAManifest(t *testing.T) {
|
|||||||
// Most of what a person installs is configuration.
|
// Most of what a person installs is configuration.
|
||||||
r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[
|
r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[
|
||||||
{"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil)
|
{"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil)
|
||||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, nil)
|
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -220,7 +229,7 @@ func TestTheTreeIsFreshEveryTime(t *testing.T) {
|
|||||||
if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil {
|
if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err != nil {
|
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(leftover); err == nil {
|
if _, err := os.Stat(leftover); err == nil {
|
||||||
@@ -233,7 +242,7 @@ func TestABuildThatCannotPushFails(t *testing.T) {
|
|||||||
"Dockerfile": "FROM scratch", "files/a": "b",
|
"Dockerfile": "FROM scratch", "files/a": "b",
|
||||||
})
|
})
|
||||||
r.failPush = true
|
r.failPush = true
|
||||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, nil); err == nil {
|
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err == nil {
|
||||||
t.Fatal("a build that could publish nothing reported success")
|
t.Fatal("a build that could publish nothing reported success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,7 +256,7 @@ func TestAnUpstreamImageIsMirroredRatherThanBuilt(t *testing.T) {
|
|||||||
"resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}`
|
"resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}`
|
||||||
|
|
||||||
r, workspace := aRepository(t, mirrors, nil)
|
r, workspace := aRepository(t, mirrors, nil)
|
||||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, nil)
|
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -313,7 +322,7 @@ func TestAModuleIsBuiltFromItsPathWithinTheRepository(t *testing.T) {
|
|||||||
"modules/other/" + ManifestName: `{"module":"other","version":"1"}`,
|
"modules/other/" + ManifestName: `{"module":"other","version":"1"}`,
|
||||||
}}
|
}}
|
||||||
got, err := Build(context.Background(), r.run, r,
|
got, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, nil)
|
"https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -332,7 +341,7 @@ func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) {
|
|||||||
for _, escaping := range []string{"../../etc", "/etc"} {
|
for _, escaping := range []string{"../../etc", "/etc"} {
|
||||||
r := &recorded{contents: map[string]string{ManifestName: withBoth}}
|
r := &recorded{contents: map[string]string{ManifestName: withBoth}}
|
||||||
_, err := Build(context.Background(), r.run, r,
|
_, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, nil)
|
"https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("%q was accepted as a module's path", escaping)
|
t.Fatalf("%q was accepted as a module's path", escaping)
|
||||||
}
|
}
|
||||||
@@ -369,7 +378,7 @@ func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := Build(context.Background(), r.run, r,
|
_, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, nil)
|
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -411,3 +420,99 @@ func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) {
|
|||||||
t.Errorf("the build was not given a context: %s", build)
|
t.Errorf("the build was not given a context: %s", build)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The forge credential is offered through git's own credential store — a file, never argv — and
|
||||||
|
// git decides when it applies. What is checked: the clone names the store, the secret never
|
||||||
|
// appears in a command line, and the file holds exactly the URL at 0600.
|
||||||
|
func TestABuildOffersTheForgesCredentialThroughGitsOwnStore(t *testing.T) {
|
||||||
|
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||||
|
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||||
|
})
|
||||||
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||||
|
workspace, nil, Npmrc{},
|
||||||
|
GitCredential{URL: "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stored := filepath.Join(workspace, "git-credentials")
|
||||||
|
clone := r.ran[0]
|
||||||
|
if !strings.Contains(clone, "credential.helper=store --file="+stored) {
|
||||||
|
t.Fatalf("the clone does not name the credential store: %s", clone)
|
||||||
|
}
|
||||||
|
for _, line := range r.ran {
|
||||||
|
if strings.Contains(line, "sw0rdfi5h") {
|
||||||
|
t.Fatalf("the secret is in a command line, readable by anything that can list processes: %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(stored)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(raw)) != "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000" {
|
||||||
|
t.Fatalf("the store does not hold the credential as given: %q", raw)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(stored)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm() != 0o600 {
|
||||||
|
t.Fatalf("the credential file is readable beyond its owner: %v", info.Mode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without a credential, a clone is exactly the invocation it always was, and no credential file
|
||||||
|
// appears — the builder a mesh of public repositories runs is unchanged.
|
||||||
|
func TestABuildWithNoCredentialClonesExactlyAsBefore(t *testing.T) {
|
||||||
|
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||||
|
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||||
|
})
|
||||||
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||||
|
workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(r.ran[0], "git clone --quiet ") {
|
||||||
|
t.Fatalf("a credential-less clone grew flags: %s", r.ran[0])
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("a credential file was written with no credential to put in it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An artifact's own context is cloned with the same offer: a private module whose context is a
|
||||||
|
// second private repository on the same forge builds, and the secret still never reaches argv.
|
||||||
|
func TestAContextCloneCarriesTheSameCredentialStore(t *testing.T) {
|
||||||
|
const withContext = `{"module":"route-proxy","version":"1",
|
||||||
|
"build":{"artifacts":[
|
||||||
|
{"name":"server","kind":"image","from":"Dockerfile",
|
||||||
|
"context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}`
|
||||||
|
r := &recorded{
|
||||||
|
contents: map[string]string{
|
||||||
|
ManifestName: withContext,
|
||||||
|
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
|
||||||
|
},
|
||||||
|
secondary: map[string]map[string]string{
|
||||||
|
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
workspace := t.TempDir()
|
||||||
|
_, err := Build(context.Background(), r.run, r,
|
||||||
|
"https://forge.invalid/catalogue.git", "", "", workspace, nil, Npmrc{},
|
||||||
|
GitCredential{URL: "https://builder:s3cret@forge.invalid"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stored := filepath.Join(workspace, "git-credentials")
|
||||||
|
var contextClone string
|
||||||
|
for _, line := range r.ran {
|
||||||
|
if strings.Contains(line, "clone") && strings.Contains(line, "source.git") {
|
||||||
|
contextClone = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contextClone == "" {
|
||||||
|
t.Fatalf("the context was never cloned: %v", r.ran)
|
||||||
|
}
|
||||||
|
if !strings.Contains(contextClone, "credential.helper=store --file="+stored) {
|
||||||
|
t.Fatalf("the context clone does not name the credential store: %s", contextClone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) {
|
|||||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||||
|
|
||||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, nil)
|
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) {
|
|||||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||||
|
|
||||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||||
"https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, nil)
|
"https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a bundle was built with no toolchain to compile it in")
|
t.Fatal("a bundle was built with no toolchain to compile it in")
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
|||||||
|
|
||||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||||
"https://forge.invalid/greeter.git", "", "", workspace,
|
"https://forge.invalid/greeter.git", "", "", workspace,
|
||||||
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, nil)
|
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a language nothing can compile was accepted")
|
t.Fatal("a language nothing can compile was accepted")
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) {
|
|||||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||||
|
|
||||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, nil)
|
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("a module with two bundles did not build: %v", err)
|
t.Fatalf("a module with two bundles did not build: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) {
|
|||||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY .npmrc ./", "files/x": "y"})
|
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY .npmrc ./", "files/x": "y"})
|
||||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||||
if _, err := Build(context.Background(), r.run, r,
|
if _, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, nil); err != nil {
|
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||||
t.Fatalf("the build failed: %v", err)
|
t.Fatalf("the build failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) {
|
|||||||
func TestAnImageBuildWithoutACredentialGetsNoHostNetwork(t *testing.T) {
|
func TestAnImageBuildWithoutACredentialGetsNoHostNetwork(t *testing.T) {
|
||||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch", "files/x": "y"})
|
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch", "files/x": "y"})
|
||||||
if _, err := Build(context.Background(), r.run, r,
|
if _, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, nil); err != nil {
|
"https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||||
t.Fatalf("the build failed: %v", err)
|
t.Fatalf("the build failed: %v", err)
|
||||||
}
|
}
|
||||||
for _, line := range r.ran {
|
for _, line := range r.ran {
|
||||||
@@ -127,7 +127,7 @@ func TestAPackageIsBuiltOnAPublicBaseAndPublishedByVersion(t *testing.T) {
|
|||||||
})
|
})
|
||||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||||
got, err := Build(context.Background(), r.run, r,
|
got, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, nil)
|
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, GitCredential{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("the package did not build: %v", err)
|
t.Fatalf("the package did not build: %v", err)
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ func TestAPackageWithNoRegistryIsRefused(t *testing.T) {
|
|||||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||||
})
|
})
|
||||||
_, err := Build(context.Background(), r.run, r,
|
_, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, nil)
|
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("a package built with no registry to publish to, silently")
|
t.Fatal("a package built with no registry to publish to, silently")
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ func TestAnImageThatDoesNotAskForTheCredentialDoesNotGetIt(t *testing.T) {
|
|||||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||||
if _, err := Build(context.Background(), r.run, r,
|
if _, err := Build(context.Background(), r.run, r,
|
||||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, nil); err != nil {
|
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||||
t.Fatalf("the build failed: %v", err)
|
t.Fatalf("the build failed: %v", err)
|
||||||
}
|
}
|
||||||
for _, line := range r.ran {
|
for _, line := range r.ran {
|
||||||
|
|||||||
Reference in New Issue
Block a user