diff --git a/cmd/uncloud/image/ls.go b/cmd/uncloud/image/ls.go index 324a7e08..891352f5 100644 --- a/cmd/uncloud/image/ls.go +++ b/cmd/uncloud/image/ls.go @@ -57,6 +57,10 @@ func NewListCommand() *cobra.Command { uncli := cmd.Context().Value("cli").(*cli.CLI) return list(cmd.Context(), uncli, opts) }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + uncli := cmd.Context().Value("cli").(*cli.CLI) + return completion.Images(cmd.Context(), uncli, args, toComplete) + }, } cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil, diff --git a/cmd/uncloud/image/push.go b/cmd/uncloud/image/push.go index 09394f3d..2c8ec82b 100644 --- a/cmd/uncloud/image/push.go +++ b/cmd/uncloud/image/push.go @@ -44,6 +44,9 @@ The image is uploaded to all cluster machines (default) or the specified machine opts.image = args[0] return push(cmd.Context(), uncli, opts) }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + return completion.LocalImages(cmd.Context(), args, toComplete) + }, } cmd.Flags().StringSliceVarP(&opts.machines, "machine", "m", nil, diff --git a/cmd/uncloud/service/run.go b/cmd/uncloud/service/run.go index 01ff5eca..3570fafb 100644 --- a/cmd/uncloud/service/run.go +++ b/cmd/uncloud/service/run.go @@ -58,6 +58,12 @@ func NewRunCommand(groupID string) *cobra.Command { return run(cmd.Context(), uncli, opts) }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return completion.LocalImages(cmd.Context(), args, toComplete) + }, GroupID: groupID, } diff --git a/internal/cli/completion/image.go b/internal/cli/completion/image.go new file mode 100644 index 00000000..88957ebd --- /dev/null +++ b/internal/cli/completion/image.go @@ -0,0 +1,70 @@ +package completion + +import ( + "context" + "slices" + "strings" + + "github.com/docker/docker/api/types/image" + dockerclient "github.com/docker/docker/client" + "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/docker" + "github.com/psviderski/uncloud/pkg/api" + "github.com/spf13/cobra" +) + +// Images allows for completion of remote images. +func Images(ctx context.Context, uncli *cli.CLI, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + client, err := uncli.ConnectCluster(ctx) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + defer client.Close() + + machineImages, err := client.ListImages(ctx, api.ImageFilter{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + names := []cobra.Completion{} + for _, machineImage := range machineImages { + for _, image := range machineImage.Images { + if len(image.RepoTags) > 0 { + if slices.Contains(args, image.RepoTags[0]) { + continue + } + if strings.HasPrefix(image.RepoTags[0], toComplete) { + names = append(names, image.RepoTags[0]) + } + } + } + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +// LocalImages allows for local image completion. +func LocalImages(ctx context.Context, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + dockerCliWrapped, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + dockerCli := &docker.Client{Client: dockerCliWrapped} + defer dockerCli.Close() + + images, err := dockerCli.ImageList(context.Background(), image.ListOptions{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + names := []cobra.Completion{} + for _, image := range images { + if len(image.RepoTags) > 0 { + if slices.Contains(args, image.RepoTags[0]) { + continue + } + if strings.HasPrefix(image.RepoTags[0], toComplete) { + names = append(names, image.RepoTags[0]) + } + } + } + return names, cobra.ShellCompDirectiveNoFileComp +}