diff --git a/cmd/unikraft/testdata/TestGolden/build/help b/cmd/unikraft/testdata/TestGolden/build/help index deafbe31..fa9f5b88 100644 --- a/cmd/unikraft/testdata/TestGolden/build/help +++ b/cmd/unikraft/testdata/TestGolden/build/help @@ -38,6 +38,8 @@ stdout: Secret to expose to the build (format: "id=mysecret[,src=/local/secret]"). --ssh SSH agent socket or keys to expose to the build (format: "default|[=|[,]]"). + --insecure + Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all. Global flags: -h, --help diff --git a/cmd/unikraft/testdata/TestGolden/images/help b/cmd/unikraft/testdata/TestGolden/images/help index f29e1de0..f9083d76 100644 --- a/cmd/unikraft/testdata/TestGolden/images/help +++ b/cmd/unikraft/testdata/TestGolden/images/help @@ -10,9 +10,9 @@ stdout: list, ls List images. get, inspect, show - Inspect a image. + Inspect an image. delete, rm, remove - Remove a image. + Remove an image. copy Copy images. @@ -41,7 +41,7 @@ stdout: $ unikraft image get --help stdout: - Inspect a image. + Inspect an image. Usage: unikraft images get [ ...] [flags] @@ -73,6 +73,8 @@ stdout: Specify which fields to include in the output. -o, --output Output format. One of: kv, table, json, yaml, raw, quiet, template. + --insecure + Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all. Global flags: -h, --help @@ -152,7 +154,7 @@ stdout: Copy images. Usage: - unikraft images copy + unikraft images copy [flags] Arguments: @@ -171,6 +173,10 @@ stdout: # Download an image from a remote registry unikraft image copy unikraft.io/official/redis:latest ./my-redis-image.tar + Flags: + --insecure + Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all. + Global flags: -h, --help Show context-sensitive help. diff --git a/internal/cmd/build.go b/internal/cmd/build.go index 36b7f374..5f706714 100644 --- a/internal/cmd/build.go +++ b/internal/cmd/build.go @@ -27,6 +27,8 @@ type BuildCmd struct { NoCache bool `help:"Do not use cache when building the image."` Secret []string `help:"Secret to expose to the build (format: \"id=mysecret[,src=/local/secret]\")."` SSH []string `help:"SSH agent socket or keys to expose to the build (format: \"default|[=|[,]]\")."` + + Insecure []string `help:"Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all." type:"optional"` } func (BuildCmd) Examples() []kingkong.Example { @@ -125,7 +127,16 @@ func (c *BuildCmd) Run(ctx context.Context, cfg *config.Config) error { return err } - access, err := images.Accessor(ctx) + var opts []images.AccessorOpt + if c.Insecure != nil { + if len(c.Insecure) > 0 { + opts = append(opts, images.WithInsecureRegistry(c.Insecure...)) + } else { + opts = append(opts, images.WithInsecureRegistries()) + } + } + + access, err := images.Accessor(ctx, opts...) if err != nil { return err } diff --git a/internal/cmd/images.go b/internal/cmd/images.go index f9c0d4cc..d7309306 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -38,10 +38,10 @@ import ( type ImagesCmd struct { cmd.ResourceCmd[ImageEntry] cmd.ListableResourceCmd[ImageEntry] - cmd.GettableResourceCmd[Image] - cmd.DeletableResourceCmd[Image] - Copy ImagesCopyCmd `cmd:"" help:"Copy images."` + Get ImagesGetCmd `cmd:"" help:"Inspect an image." aliases:"inspect,show"` + Delete ImagesDeleteCmd `cmd:"" help:"Remove an image." aliases:"rm,remove"` + Copy ImagesCopyCmd `cmd:"" help:"Copy images."` } type Image struct { @@ -640,9 +640,47 @@ func (ImageEntry) loadFromPlatform(image platform.Image, metro *config.Metro) ([ return results, nil } +type ImagesGetCmd struct { + cmd.ResourceGetCmd[Image] + Insecure []string `help:"Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all." type:"optional"` +} + +func (c *ImagesGetCmd) Run(ctx context.Context, stdio config.Stdio, sandbox *resource.Sandbox) error { + if c.Insecure != nil { + var opts []images.AccessorOpt + if len(c.Insecure) > 0 { + opts = append(opts, images.WithInsecureRegistry(c.Insecure...)) + } else { + opts = append(opts, images.WithInsecureRegistries()) + } + ctx = images.WithInsecureContext(ctx, opts...) + } + return c.ResourceGetCmd.Run(ctx, stdio, sandbox) +} + +type ImagesDeleteCmd struct { + cmd.ResourceRemoveCmd[Image] + Insecure []string `help:"Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all." type:"optional"` +} + +func (c *ImagesDeleteCmd) Run(ctx context.Context, stdio config.Stdio, sandbox *resource.Sandbox) error { + if c.Insecure != nil { + var opts []images.AccessorOpt + if len(c.Insecure) > 0 { + opts = append(opts, images.WithInsecureRegistry(c.Insecure...)) + } else { + opts = append(opts, images.WithInsecureRegistries()) + } + ctx = images.WithInsecureContext(ctx, opts...) + } + return c.ResourceRemoveCmd.Run(ctx, stdio, sandbox) +} + type ImagesCopyCmd struct { Source string `arg:"" help:"Source image reference."` Dest string `arg:"" help:"Destination image reference."` + + Insecure []string `help:"Allow insecure (HTTP/unverified TLS) connections to registries. Specify hostnames to restrict, or omit to apply to all." type:"optional"` } func (cmd ImagesCopyCmd) Examples() []kingkong.Example { @@ -669,7 +707,16 @@ func (cmd ImagesCopyCmd) Examples() []kingkong.Example { } func (cmd ImagesCopyCmd) Run(ctx context.Context) error { - access, err := images.Accessor(ctx) + var opts []images.AccessorOpt + if cmd.Insecure != nil { + if len(cmd.Insecure) > 0 { + opts = append(opts, images.WithInsecureRegistry(cmd.Insecure...)) + } else { + opts = append(opts, images.WithInsecureRegistries()) + } + } + + access, err := images.Accessor(ctx, opts...) if err != nil { return err } diff --git a/internal/images/images.go b/internal/images/images.go index d1f4de0d..e011ae86 100644 --- a/internal/images/images.go +++ b/internal/images/images.go @@ -24,14 +24,33 @@ var defaultRegistries = []string{ "index.unikraft.io", } -func Accessor(ctx context.Context) (*imagespec.Accessor, error) { +type insecureContextKey struct{} + +// WithInsecureContext returns a context that carries insecure registry options, +// which are picked up by Accessor. +func WithInsecureContext(ctx context.Context, opts ...AccessorOpt) context.Context { + return context.WithValue(ctx, insecureContextKey{}, opts) +} + +func Accessor(ctx context.Context, opts ...AccessorOpt) (*imagespec.Accessor, error) { + if len(opts) == 0 { + if ctxOpts, ok := ctx.Value(insecureContextKey{}).([]AccessorOpt); ok { + opts = ctxOpts + } + } + + var o accessorOpts + for _, opt := range opts { + opt(&o) + } + cfg := config.FromContextOrDefault(ctx) profile, err := cfg.CurrentProfile() if err != nil { return nil, err } - options := resolverOptions(profile) + options := resolverOptions(profile, o.insecureRegistries, o.allInsecure) resolver := docker.NewResolver(options) return imagespec.NewAccessor( imagespec.WithResolver(resolver), @@ -41,6 +60,26 @@ func Accessor(ctx context.Context) (*imagespec.Accessor, error) { ), nil } +// AccessorOpt is a functional option for configuring an Accessor. +type AccessorOpt func(*accessorOpts) + +type accessorOpts struct { + insecureRegistries []string + allInsecure bool +} + +func WithInsecureRegistry(hosts ...string) AccessorOpt { + return func(o *accessorOpts) { + o.insecureRegistries = hosts + } +} + +func WithInsecureRegistries() AccessorOpt { + return func(o *accessorOpts) { + o.allInsecure = true + } +} + func ParseNormalizedNamed(key string) (reference.Named, error) { return ParseNormalizedNamedMetro(nil, key) } diff --git a/internal/images/resolver.go b/internal/images/resolver.go index 6878a111..a443da9b 100644 --- a/internal/images/resolver.go +++ b/internal/images/resolver.go @@ -13,7 +13,6 @@ import ( "slices" "strings" - "github.com/containerd/containerd/v2/core/remotes" "github.com/containerd/containerd/v2/core/remotes/docker" dockerconfig "github.com/docker/cli/cli/config" @@ -22,7 +21,7 @@ import ( "unikraft.com/cli/internal/version" ) -func resolverOptions(profile *config.Profile) docker.ResolverOptions { +func resolverOptions(profile *config.Profile, insecureRegistries []string, allInsecure bool) docker.ResolverOptions { headers := http.Header{} headers.Set("User-Agent", version.UserAgent()) @@ -34,7 +33,17 @@ func resolverOptions(profile *config.Profile) docker.ResolverOptions { } } + isInsecureRegistry := func(host string) bool { + if allInsecure { + return true + } + return slices.Contains(insecureRegistries, host) + } + httpHost := func(host string) (bool, error) { + if isInsecureRegistry(host) { + return true, nil + } for _, index := range indexes { if host == index.Host { return index.HTTP, nil @@ -43,6 +52,9 @@ func resolverOptions(profile *config.Profile) docker.ResolverOptions { return false, nil } insecureHost := func(host string) (bool, error) { + if isInsecureRegistry(host) { + return true, nil + } for _, index := range indexes { if host == index.Host { return index.Insecure, nil @@ -93,10 +105,6 @@ func resolverOptions(profile *config.Profile) docker.ResolverOptions { } } -func Resolver(profile *config.Profile) remotes.Resolver { - return docker.NewResolver(resolverOptions(profile)) -} - func fallbackHost(registryHosts ...docker.RegistryHosts) docker.RegistryHosts { return func(host string) ([]docker.RegistryHost, error) { var allHosts []docker.RegistryHost diff --git a/internal/x/kong/mapper.go b/internal/x/kong/mapper.go index 22ddb545..ef650649 100644 --- a/internal/x/kong/mapper.go +++ b/internal/x/kong/mapper.go @@ -16,21 +16,15 @@ func Optional() kong.MapperFunc { // NOTE: this works with: // --optional // --optional=value - // --optional value + // -ovalue // --optional --other-flag (does not consume --other-flag) - // but not with: - // --optional notvalue - // because the latter is ambiguous with positional arguments. + // but NOT with: + // --optional value + // -o value + // because the latter forms would be ambiguous with positional arguments. return func(ctx *kong.DecodeContext, target reflect.Value) error { switch tok := ctx.Scan.Peek(); tok.Type { - case kong.FlagValueToken: - r := kong.NewRegistry().RegisterDefaults() - return r.ForValue(target).Decode(ctx, target) - case kong.UntypedToken: - // Don't consume tokens that look like flags (e.g. --sort, -s). - if s, ok := tok.Value.(string); ok && len(s) > 0 && s[0] == '-' { - return nil - } + case kong.FlagValueToken, kong.ShortFlagTailToken: r := kong.NewRegistry().RegisterDefaults() return r.ForValue(target).Decode(ctx, target) default: diff --git a/internal/x/kong/mapper_test.go b/internal/x/kong/mapper_test.go index 10de2ac6..5a4b9fc6 100644 --- a/internal/x/kong/mapper_test.go +++ b/internal/x/kong/mapper_test.go @@ -19,6 +19,7 @@ func TestOptional(t *testing.T) { type CLI struct { Watch *time.Duration `short:"w" long:"watch" type:"optional"` Sort string `long:"sort"` + Arg string `arg:"" optional:""` } tests := []struct { @@ -26,6 +27,7 @@ func TestOptional(t *testing.T) { args []string wantWatch *time.Duration wantSort string + wantArg string }{ { name: "no flags", @@ -40,8 +42,8 @@ func TestOptional(t *testing.T) { wantSort: "", }, { - name: "watch with value", - args: []string{"-w", "5s"}, + name: "short watch with inline value", + args: []string{"-w5s"}, wantWatch: new(5 * time.Second), wantSort: "", }, @@ -69,6 +71,18 @@ func TestOptional(t *testing.T) { wantWatch: new(5 * time.Second), wantSort: "name", }, + { + name: "space-separated long does not consume value", + args: []string{"--watch", "5s"}, + wantWatch: new(time.Duration), + wantArg: "5s", + }, + { + name: "space-separated short does not consume value", + args: []string{"-w", "5s"}, + wantWatch: new(time.Duration), + wantArg: "5s", + }, } for _, tt := range tests { @@ -87,6 +101,7 @@ func TestOptional(t *testing.T) { assert.Equal(t, *tt.wantWatch, *cli.Watch) } assert.Equal(t, tt.wantSort, cli.Sort) + assert.Equal(t, tt.wantArg, cli.Arg) }) } }