Loading cmd/felis/imagebundle.go 0 → 100644 +109 −0 Changes for cmd/felis/imagebundle.go: 109 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "bufio" "context" "errors" "flag" "fmt" "io" "os" "os/signal" "strings" "syscall" "felis.lolicon.best/internal/imagepush" ) // cmdImageBundle writes a release's image bundle: one OCI layout tar holding every // image an install runs, for one platform, plus its listing (one "role name // manifest-digest config-digest" line per image). deploy/build-release-artifacts.sh // runs it in CI; deploy/bootstrap.sh imports the tar into k3s's containerd and // pushes it into the platform registry with push-image --image. // // --layout role=name=path an image buildx wrote with --output type=oci // --pull role=ref a digest-pinned public image, named repository@digest // // The tar and the listing are written beside their final paths and renamed into // place, so a failed run leaves neither behind. func cmdImageBundle(args []string, _, stderr io.Writer) int { fs := flag.NewFlagSet("image-bundle", flag.ContinueOnError) fs.SetOutput(stderr) platform := fs.String("platform", "", "os/arch the bundle is for, e.g. linux/arm64") out := fs.String("out", "", "path of the bundle tar to write") list := fs.String("list", "", "path of the listing to write") var images []imagepush.BundleImage fs.Func("layout", "role=name=path of an OCI layout tar (repeatable)", func(v string) error { role, rest, ok := strings.Cut(v, "=") name, path, ok2 := strings.Cut(rest, "=") if !ok || !ok2 || role == "" || name == "" || path == "" { return fmt.Errorf("want role=name=path, got %q", v) } images = append(images, imagepush.BundleImage{Role: role, Name: name, Layout: path}) return nil }) fs.Func("pull", "role=ref of a digest-pinned public image (repeatable)", func(v string) error { role, ref, ok := strings.Cut(v, "=") if !ok || role == "" || !strings.Contains(ref, "@sha256:") { return fmt.Errorf("want role=ref with ref pinned by digest, got %q", v) } images = append(images, imagepush.BundleImage{Role: role, Name: imagepush.PinnedName(ref), Source: ref}) return nil }) if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return 0 } return 2 } if *platform == "" || *out == "" || *list == "" || len(images) == 0 { fmt.Fprintln(stderr, "felis image-bundle: --platform, --out, --list and at least one --layout or --pull are required") return 2 } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := writeImageBundle(ctx, &imagepush.Source{Platform: *platform}, images, *out, *list); err != nil { fmt.Fprintf(stderr, "felis image-bundle: %v\n", err) return 1 } return 0 } func writeImageBundle(ctx context.Context, s *imagepush.Source, images []imagepush.BundleImage, out, list string) (err error) { tmpOut, tmpList := out+".tmp", list+".tmp" defer func() { if err != nil { os.Remove(tmpOut) os.Remove(tmpList) } }() f, err := os.Create(tmpOut) if err != nil { return err } w := bufio.NewWriterSize(f, 1<<20) entries, err := imagepush.WriteBundle(ctx, s, images, w) if err == nil { err = w.Flush() } if err == nil { err = f.Sync() } if cerr := f.Close(); err == nil { err = cerr } if err != nil { return err } var b strings.Builder for _, e := range entries { fmt.Fprintf(&b, "%s %s %s %s\n", e.Role, e.Name, e.Digest, e.Config) } if err := os.WriteFile(tmpList, []byte(b.String()), 0o644); err != nil { return err } if err := os.Rename(tmpOut, out); err != nil { return err } return os.Rename(tmpList, list) } cmd/felis/imagebundle_test.go 0 → 100644 +131 −0 Changes for cmd/felis/imagebundle_test.go: 131 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "archive/tar" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" ) // writeTestLayout writes an OCI layout tar holding one linux/arch image with one // layer, the way buildx --output type=oci leaves a single-platform build, and // returns its path with the manifest and config digests. func writeTestLayout(t *testing.T, dir, arch, layer string) (path, manifestDigest, configDigest string) { t.Helper() blobs := map[string][]byte{} add := func(b []byte) (string, int) { sum := sha256.Sum256(b) d := "sha256:" + hex.EncodeToString(sum[:]) blobs[d] = b return d, len(b) } cfgDigest, cfgSize := add([]byte(`{"architecture":"` + arch + `","os":"linux","rootfs":{"type":"layers"}}`)) layerDigest, layerSize := add([]byte(layer)) manifest := fmt.Sprintf(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json",`+ `"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":%q,"size":%d},`+ `"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":%q,"size":%d}]}`, cfgDigest, cfgSize, layerDigest, layerSize) mDigest, mSize := add([]byte(manifest)) index, _ := json.Marshal(map[string]any{ "schemaVersion": 2, "manifests": []map[string]any{{ "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": mDigest, "size": mSize, }}, }) var buf bytes.Buffer tw := tar.NewWriter(&buf) put := func(name string, b []byte) { tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(b)), Typeflag: tar.TypeReg}) tw.Write(b) } put("oci-layout", []byte(`{"imageLayoutVersion":"1.0.0"}`)) put("index.json", index) for d, b := range blobs { put("blobs/sha256/"+strings.TrimPrefix(d, "sha256:"), b) } tw.Close() path = filepath.Join(dir, arch+"-"+layer+".tar") if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil { t.Fatal(err) } return path, mDigest, cfgDigest } func TestImageBundleWritesTheListingTheInstallerReads(t *testing.T) { dir := t.TempDir() limbo, limboDigest, limboConfig := writeTestLayout(t, dir, "arm64", "limbo") lobby, lobbyDigest, lobbyConfig := writeTestLayout(t, dir, "arm64", "lobby") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") var stderr bytes.Buffer code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + limbo, "--layout", "lobby=registry.felis.svc:5000/felis/lobby:demo=" + lobby, }, nil, &stderr) if code != 0 { t.Fatalf("image-bundle = %d: %s", code, stderr.String()) } // deploy/bootstrap.sh reads this with `read -r role name digest config`. got, _ := os.ReadFile(list) want := "limbo registry.felis.svc:5000/felis/limbo:demo " + limboDigest + " " + limboConfig + "\n" + "lobby registry.felis.svc:5000/felis/lobby:demo " + lobbyDigest + " " + lobbyConfig + "\n" if string(got) != want { t.Errorf("listing:\n%s\nwant:\n%s", got, want) } if fi, err := os.Stat(out); err != nil || fi.Size() == 0 { t.Errorf("bundle: %v", err) } if leftovers, _ := filepath.Glob(filepath.Join(dir, "*.tmp")); len(leftovers) != 0 { t.Errorf("left behind %v", leftovers) } } func TestImageBundleLeavesNothingWhenAnImageIsRefused(t *testing.T) { dir := t.TempDir() amd, _, _ := writeTestLayout(t, dir, "amd64", "limbo") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") // The pair an earlier run wrote stays as it was: a listing beside a bundle it // does not describe would have the installer look for images that are not there. os.WriteFile(out, []byte("old bundle"), 0o644) os.WriteFile(list, []byte("old listing\n"), 0o644) var stderr bytes.Buffer code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + amd}, nil, &stderr) if code != 1 || !strings.Contains(stderr.String(), "is a linux/amd64 image") { t.Fatalf("image-bundle = %d: %s", code, stderr.String()) } if got, _ := os.ReadFile(out); string(got) != "old bundle" { t.Errorf("bundle was replaced with %d bytes", len(got)) } if got, _ := os.ReadFile(list); string(got) != "old listing\n" { t.Errorf("listing was replaced with %q", got) } if leftovers, _ := filepath.Glob(filepath.Join(dir, "*.tmp")); len(leftovers) != 0 { t.Errorf("left behind %v", leftovers) } } func TestPushImageReadsABundleByImageName(t *testing.T) { dir := t.TempDir() limbo, _, _ := writeTestLayout(t, dir, "arm64", "limbo") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") if code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + limbo}, nil, &bytes.Buffer{}); code != 0 { t.Fatal("image-bundle failed") } t.Setenv("FELIS_REGISTRY_USERNAME", "platform") t.Setenv("FELIS_REGISTRY_PASSWORD", "x") // The name is looked up in the bundle's index before the registry is contacted, // so a name the bundle lacks fails here with what it does hold. var stderr bytes.Buffer code := cmdPushImage([]string{"--tar", out, "--image", "registry.felis.svc:5000/felis/lobby:demo", "--ref", "127.0.0.1:1/felis/lobby:demo"}, &bytes.Buffer{}, &stderr) if code != 1 || !strings.Contains(stderr.String(), "holds no image named registry.felis.svc:5000/felis/lobby:demo (it holds: registry.felis.svc:5000/felis/limbo:demo") { t.Fatalf("push-image = %d: %s", code, stderr.String()) } } cmd/felis/registrygate.go +9 −2 Changes for cmd/felis/registrygate.go: 9 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -131,7 +131,8 @@ func loopbackAddr(addr string) bool { func cmdPushImage(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("push-image", flag.ContinueOnError) fs.SetOutput(stderr) tarPath := fs.String("tar", "", "image tarball Kaniko wrote with --tar-path") tarPath := fs.String("tar", "", "image tarball Kaniko wrote with --tar-path, or with --image an OCI layout tar") image := fs.String("image", "", "push the image this name (io.containerd.image.name) marks in the OCI layout tar --tar, e.g. a release's image bundle") ref := fs.String("ref", "", "host/repository:tag to publish it as") scheme := fs.String("scheme", "http", "registry scheme: http for the in-cluster registry, https otherwise") if err := fs.Parse(args); err != nil { Loading @@ -154,7 +155,13 @@ func cmdPushImage(args []string, stdout, stderr io.Writer) int { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() p := &imagepush.Pusher{Scheme: *scheme, Username: user, Password: pass, Log: stderr} digest, err := p.Push(ctx, *tarPath, *ref) var digest string var err error if *image != "" { digest, err = p.PushLayout(ctx, *tarPath, *image, *ref) } else { digest, err = p.Push(ctx, *tarPath, *ref) } if err != nil { fmt.Fprintf(stderr, "felis push-image: %v\n", err) return 1 Loading cmd/felis/run.go +1 −0 Changes for cmd/felis/run.go: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ var commands = map[string]func(args []string, stdout, stderr io.Writer) int{ "init-forwarding": cmdInitForwarding, "init-volume": cmdInitVolume, "pin-images": cmdPinImages, "image-bundle": cmdImageBundle, "version": cmdVersion, "update": cmdUpdate, "watchdog": cmdWatchdog, Loading cmd/felis/run_test.go +1 −1 Changes for cmd/felis/run_test.go: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -43,7 +43,7 @@ func TestRunUnknownCommand(t *testing.T) { // decision rather than an oversight. var undocumentedCommands = map[string]bool{ "bootstrap-assets": true, "init-forwarding": true, "init-volume": true, "pin-images": true, "pin-images": true, "image-bundle": true, } // The usage text and the dispatch table must describe the same set of commands. Loading Loading
cmd/felis/imagebundle.go 0 → 100644 +109 −0 Changes for cmd/felis/imagebundle.go: 109 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "bufio" "context" "errors" "flag" "fmt" "io" "os" "os/signal" "strings" "syscall" "felis.lolicon.best/internal/imagepush" ) // cmdImageBundle writes a release's image bundle: one OCI layout tar holding every // image an install runs, for one platform, plus its listing (one "role name // manifest-digest config-digest" line per image). deploy/build-release-artifacts.sh // runs it in CI; deploy/bootstrap.sh imports the tar into k3s's containerd and // pushes it into the platform registry with push-image --image. // // --layout role=name=path an image buildx wrote with --output type=oci // --pull role=ref a digest-pinned public image, named repository@digest // // The tar and the listing are written beside their final paths and renamed into // place, so a failed run leaves neither behind. func cmdImageBundle(args []string, _, stderr io.Writer) int { fs := flag.NewFlagSet("image-bundle", flag.ContinueOnError) fs.SetOutput(stderr) platform := fs.String("platform", "", "os/arch the bundle is for, e.g. linux/arm64") out := fs.String("out", "", "path of the bundle tar to write") list := fs.String("list", "", "path of the listing to write") var images []imagepush.BundleImage fs.Func("layout", "role=name=path of an OCI layout tar (repeatable)", func(v string) error { role, rest, ok := strings.Cut(v, "=") name, path, ok2 := strings.Cut(rest, "=") if !ok || !ok2 || role == "" || name == "" || path == "" { return fmt.Errorf("want role=name=path, got %q", v) } images = append(images, imagepush.BundleImage{Role: role, Name: name, Layout: path}) return nil }) fs.Func("pull", "role=ref of a digest-pinned public image (repeatable)", func(v string) error { role, ref, ok := strings.Cut(v, "=") if !ok || role == "" || !strings.Contains(ref, "@sha256:") { return fmt.Errorf("want role=ref with ref pinned by digest, got %q", v) } images = append(images, imagepush.BundleImage{Role: role, Name: imagepush.PinnedName(ref), Source: ref}) return nil }) if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return 0 } return 2 } if *platform == "" || *out == "" || *list == "" || len(images) == 0 { fmt.Fprintln(stderr, "felis image-bundle: --platform, --out, --list and at least one --layout or --pull are required") return 2 } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := writeImageBundle(ctx, &imagepush.Source{Platform: *platform}, images, *out, *list); err != nil { fmt.Fprintf(stderr, "felis image-bundle: %v\n", err) return 1 } return 0 } func writeImageBundle(ctx context.Context, s *imagepush.Source, images []imagepush.BundleImage, out, list string) (err error) { tmpOut, tmpList := out+".tmp", list+".tmp" defer func() { if err != nil { os.Remove(tmpOut) os.Remove(tmpList) } }() f, err := os.Create(tmpOut) if err != nil { return err } w := bufio.NewWriterSize(f, 1<<20) entries, err := imagepush.WriteBundle(ctx, s, images, w) if err == nil { err = w.Flush() } if err == nil { err = f.Sync() } if cerr := f.Close(); err == nil { err = cerr } if err != nil { return err } var b strings.Builder for _, e := range entries { fmt.Fprintf(&b, "%s %s %s %s\n", e.Role, e.Name, e.Digest, e.Config) } if err := os.WriteFile(tmpList, []byte(b.String()), 0o644); err != nil { return err } if err := os.Rename(tmpOut, out); err != nil { return err } return os.Rename(tmpList, list) }
cmd/felis/imagebundle_test.go 0 → 100644 +131 −0 Changes for cmd/felis/imagebundle_test.go: 131 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "archive/tar" "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" ) // writeTestLayout writes an OCI layout tar holding one linux/arch image with one // layer, the way buildx --output type=oci leaves a single-platform build, and // returns its path with the manifest and config digests. func writeTestLayout(t *testing.T, dir, arch, layer string) (path, manifestDigest, configDigest string) { t.Helper() blobs := map[string][]byte{} add := func(b []byte) (string, int) { sum := sha256.Sum256(b) d := "sha256:" + hex.EncodeToString(sum[:]) blobs[d] = b return d, len(b) } cfgDigest, cfgSize := add([]byte(`{"architecture":"` + arch + `","os":"linux","rootfs":{"type":"layers"}}`)) layerDigest, layerSize := add([]byte(layer)) manifest := fmt.Sprintf(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json",`+ `"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":%q,"size":%d},`+ `"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":%q,"size":%d}]}`, cfgDigest, cfgSize, layerDigest, layerSize) mDigest, mSize := add([]byte(manifest)) index, _ := json.Marshal(map[string]any{ "schemaVersion": 2, "manifests": []map[string]any{{ "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": mDigest, "size": mSize, }}, }) var buf bytes.Buffer tw := tar.NewWriter(&buf) put := func(name string, b []byte) { tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(b)), Typeflag: tar.TypeReg}) tw.Write(b) } put("oci-layout", []byte(`{"imageLayoutVersion":"1.0.0"}`)) put("index.json", index) for d, b := range blobs { put("blobs/sha256/"+strings.TrimPrefix(d, "sha256:"), b) } tw.Close() path = filepath.Join(dir, arch+"-"+layer+".tar") if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil { t.Fatal(err) } return path, mDigest, cfgDigest } func TestImageBundleWritesTheListingTheInstallerReads(t *testing.T) { dir := t.TempDir() limbo, limboDigest, limboConfig := writeTestLayout(t, dir, "arm64", "limbo") lobby, lobbyDigest, lobbyConfig := writeTestLayout(t, dir, "arm64", "lobby") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") var stderr bytes.Buffer code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + limbo, "--layout", "lobby=registry.felis.svc:5000/felis/lobby:demo=" + lobby, }, nil, &stderr) if code != 0 { t.Fatalf("image-bundle = %d: %s", code, stderr.String()) } // deploy/bootstrap.sh reads this with `read -r role name digest config`. got, _ := os.ReadFile(list) want := "limbo registry.felis.svc:5000/felis/limbo:demo " + limboDigest + " " + limboConfig + "\n" + "lobby registry.felis.svc:5000/felis/lobby:demo " + lobbyDigest + " " + lobbyConfig + "\n" if string(got) != want { t.Errorf("listing:\n%s\nwant:\n%s", got, want) } if fi, err := os.Stat(out); err != nil || fi.Size() == 0 { t.Errorf("bundle: %v", err) } if leftovers, _ := filepath.Glob(filepath.Join(dir, "*.tmp")); len(leftovers) != 0 { t.Errorf("left behind %v", leftovers) } } func TestImageBundleLeavesNothingWhenAnImageIsRefused(t *testing.T) { dir := t.TempDir() amd, _, _ := writeTestLayout(t, dir, "amd64", "limbo") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") // The pair an earlier run wrote stays as it was: a listing beside a bundle it // does not describe would have the installer look for images that are not there. os.WriteFile(out, []byte("old bundle"), 0o644) os.WriteFile(list, []byte("old listing\n"), 0o644) var stderr bytes.Buffer code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + amd}, nil, &stderr) if code != 1 || !strings.Contains(stderr.String(), "is a linux/amd64 image") { t.Fatalf("image-bundle = %d: %s", code, stderr.String()) } if got, _ := os.ReadFile(out); string(got) != "old bundle" { t.Errorf("bundle was replaced with %d bytes", len(got)) } if got, _ := os.ReadFile(list); string(got) != "old listing\n" { t.Errorf("listing was replaced with %q", got) } if leftovers, _ := filepath.Glob(filepath.Join(dir, "*.tmp")); len(leftovers) != 0 { t.Errorf("left behind %v", leftovers) } } func TestPushImageReadsABundleByImageName(t *testing.T) { dir := t.TempDir() limbo, _, _ := writeTestLayout(t, dir, "arm64", "limbo") out, list := filepath.Join(dir, "images.tar"), filepath.Join(dir, "images.txt") if code := cmdImageBundle([]string{"--platform", "linux/arm64", "--out", out, "--list", list, "--layout", "limbo=registry.felis.svc:5000/felis/limbo:demo=" + limbo}, nil, &bytes.Buffer{}); code != 0 { t.Fatal("image-bundle failed") } t.Setenv("FELIS_REGISTRY_USERNAME", "platform") t.Setenv("FELIS_REGISTRY_PASSWORD", "x") // The name is looked up in the bundle's index before the registry is contacted, // so a name the bundle lacks fails here with what it does hold. var stderr bytes.Buffer code := cmdPushImage([]string{"--tar", out, "--image", "registry.felis.svc:5000/felis/lobby:demo", "--ref", "127.0.0.1:1/felis/lobby:demo"}, &bytes.Buffer{}, &stderr) if code != 1 || !strings.Contains(stderr.String(), "holds no image named registry.felis.svc:5000/felis/lobby:demo (it holds: registry.felis.svc:5000/felis/limbo:demo") { t.Fatalf("push-image = %d: %s", code, stderr.String()) } }
cmd/felis/registrygate.go +9 −2 Changes for cmd/felis/registrygate.go: 9 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -131,7 +131,8 @@ func loopbackAddr(addr string) bool { func cmdPushImage(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("push-image", flag.ContinueOnError) fs.SetOutput(stderr) tarPath := fs.String("tar", "", "image tarball Kaniko wrote with --tar-path") tarPath := fs.String("tar", "", "image tarball Kaniko wrote with --tar-path, or with --image an OCI layout tar") image := fs.String("image", "", "push the image this name (io.containerd.image.name) marks in the OCI layout tar --tar, e.g. a release's image bundle") ref := fs.String("ref", "", "host/repository:tag to publish it as") scheme := fs.String("scheme", "http", "registry scheme: http for the in-cluster registry, https otherwise") if err := fs.Parse(args); err != nil { Loading @@ -154,7 +155,13 @@ func cmdPushImage(args []string, stdout, stderr io.Writer) int { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() p := &imagepush.Pusher{Scheme: *scheme, Username: user, Password: pass, Log: stderr} digest, err := p.Push(ctx, *tarPath, *ref) var digest string var err error if *image != "" { digest, err = p.PushLayout(ctx, *tarPath, *image, *ref) } else { digest, err = p.Push(ctx, *tarPath, *ref) } if err != nil { fmt.Fprintf(stderr, "felis push-image: %v\n", err) return 1 Loading
cmd/felis/run.go +1 −0 Changes for cmd/felis/run.go: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ var commands = map[string]func(args []string, stdout, stderr io.Writer) int{ "init-forwarding": cmdInitForwarding, "init-volume": cmdInitVolume, "pin-images": cmdPinImages, "image-bundle": cmdImageBundle, "version": cmdVersion, "update": cmdUpdate, "watchdog": cmdWatchdog, Loading
cmd/felis/run_test.go +1 −1 Changes for cmd/felis/run_test.go: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -43,7 +43,7 @@ func TestRunUnknownCommand(t *testing.T) { // decision rather than an oversight. var undocumentedCommands = map[string]bool{ "bootstrap-assets": true, "init-forwarding": true, "init-volume": true, "pin-images": true, "pin-images": true, "image-bundle": true, } // The usage text and the dispatch table must describe the same set of commands. Loading