From 6e1e271e3e414b8c158e947a5e769ef1b1126eb4 Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Tue, 17 Dec 2024 18:41:00 +0100 Subject: [PATCH 1/2] refactor: ensure Viper is initialized only once Before this change we have two separate methods, one for initializing Viper - InitViper, and another to retrieve it - GetViper. This may lead to either Viper not being initiated, thus leading to nil pointer dereference error or initializing more than once. After this change, we will ensure Viper is initialized only once. Signed-off-by: Maciej Szulik --- src/cmd/common/viper.go | 13 ++++++------- src/cmd/initialize.go | 2 +- src/cmd/package.go | 2 +- src/cmd/root.go | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/cmd/common/viper.go b/src/cmd/common/viper.go index 360a50c036..33cc075d8a 100644 --- a/src/cmd/common/viper.go +++ b/src/cmd/common/viper.go @@ -116,13 +116,8 @@ var ( vConfigError error ) -// InitViper initializes the viper singleton for the CLI -func InitViper() *viper.Viper { - // Already initialized by some other command - if v != nil { - return v - } - +// initializes the viper singleton for the CLI +func initViper() *viper.Viper { v = viper.New() // Skip for vendor-only commands or the version command @@ -159,6 +154,10 @@ func InitViper() *viper.Viper { // GetViper returns the viper singleton func GetViper() *viper.Viper { + if v == nil { + v = initViper() + } + return v } diff --git a/src/cmd/initialize.go b/src/cmd/initialize.go index 34e30d5e32..67395b90a0 100644 --- a/src/cmd/initialize.go +++ b/src/cmd/initialize.go @@ -45,7 +45,7 @@ func NewInitCommand() *cobra.Command { RunE: o.Run, } - v := common.InitViper() + v := common.GetViper() // Init package variable defaults that are non-zero values // NOTE: these are not in common.setDefaults so that zarf tools update-creds does not erroneously update values back to the default diff --git a/src/cmd/package.go b/src/cmd/package.go index f62d1e2e2c..5d52b8bbec 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -43,7 +43,7 @@ func NewPackageCommand() *cobra.Command { Short: lang.CmdPackageShort, } - v := common.InitViper() + v := common.GetViper() persistentFlags := cmd.PersistentFlags() persistentFlags.IntVar(&config.CommonOptions.OCIConcurrency, "oci-concurrency", v.GetInt(common.VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) diff --git a/src/cmd/root.go b/src/cmd/root.go index 3ba69c02e9..3dfeef41d9 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -178,7 +178,7 @@ func init() { return } - v := common.InitViper() + v := common.GetViper() // Logs rootCmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), lang.RootCmdFlagLogLevel) From bb80bbe5760d51b0d80cbedb577acd8cca9ed58b Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Tue, 17 Dec 2024 18:45:18 +0100 Subject: [PATCH 2/2] refactor: Create structs for each tools command to better isolate commands Signed-off-by: Maciej Szulik --- src/cmd/root.go | 10 +- src/cmd/tools/archiver.go | 156 +++++---- src/cmd/tools/common.go | 35 +- src/cmd/tools/crane.go | 302 ++++++++++------- src/cmd/tools/helm.go | 19 +- src/cmd/tools/k9s.go | 9 +- src/cmd/tools/kubectl.go | 15 +- src/cmd/tools/syft.go | 18 +- src/cmd/tools/wait.go | 80 ++--- src/cmd/tools/yq.go | 14 +- src/cmd/tools/zarf.go | 691 +++++++++++++++++++++----------------- 11 files changed, 763 insertions(+), 586 deletions(-) diff --git a/src/cmd/root.go b/src/cmd/root.go index 3dfeef41d9..153d31c1c9 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -130,6 +130,13 @@ func NewZarfCommand() *cobra.Command { Run: run, } + // Add the tools commands + // IMPORTANT: we need to make sure the tools command are added first + // to ensure the config defaulting doesn't kick in, and inject values + // into zart tools update-creds command + // see https://github.com/zarf-dev/zarf/pull/3340#discussion_r1889221826 + rootCmd.AddCommand(tools.NewToolsCommand()) + // TODO(soltysh): consider adding command groups rootCmd.AddCommand(NewConnectCommand()) rootCmd.AddCommand(NewDestroyCommand()) @@ -170,9 +177,6 @@ func Execute(ctx context.Context) { } func init() { - // Add the tools commands - tools.Include(rootCmd) - // Skip for vendor-only commands if common.CheckVendorOnlyFromArgs() { return diff --git a/src/cmd/tools/archiver.go b/src/cmd/tools/archiver.go index e3bf2f2629..f7aa1e6e4f 100644 --- a/src/cmd/tools/archiver.go +++ b/src/cmd/tools/archiver.go @@ -19,80 +19,108 @@ import ( // ldflags github.com/zarf-dev/zarf/src/cmd/tools.archiverVersion=x.x.x var archiverVersion string -var archiverCmd = &cobra.Command{ - Use: "archiver", - Aliases: []string{"a"}, - Short: lang.CmdToolsArchiverShort, - Version: archiverVersion, +// NewArchiverCommand creates the `tools archiver` sub-command and its nested children. +func NewArchiverCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "archiver", + Aliases: []string{"a"}, + Short: lang.CmdToolsArchiverShort, + Version: archiverVersion, + } + + cmd.AddCommand(NewArchiverCompressCommand()) + cmd.AddCommand(NewArchiverDecompressCommand()) + cmd.AddCommand(newVersionCmd("mholt/archiver", archiverVersion)) + + return cmd } -var archiverCompressCmd = &cobra.Command{ - Use: "compress SOURCES ARCHIVE", - Aliases: []string{"c"}, - Short: lang.CmdToolsArchiverCompressShort, - Args: cobra.MinimumNArgs(2), - RunE: func(_ *cobra.Command, args []string) error { - sourceFiles, destinationArchive := args[:len(args)-1], args[len(args)-1] - err := archiver.Archive(sourceFiles, destinationArchive) - if err != nil { - return fmt.Errorf("unable to perform compression: %w", err) - } - return err - }, +// ArchiverCompressOptions holds the command-line options for 'tools archiver compress' sub-command. +type ArchiverCompressOptions struct{} + +// NewArchiverCompressCommand creates the `tools archiver compress` sub-command. +func NewArchiverCompressCommand() *cobra.Command { + o := ArchiverCompressOptions{} + + cmd := &cobra.Command{ + Use: "compress SOURCES ARCHIVE", + Aliases: []string{"c"}, + Short: lang.CmdToolsArchiverCompressShort, + Args: cobra.MinimumNArgs(2), + RunE: o.Run, + } + + return cmd +} + +// Run performs the execution of 'tools archiver compress' sub-command. +func (o *ArchiverCompressOptions) Run(_ *cobra.Command, args []string) error { + sourceFiles, destinationArchive := args[:len(args)-1], args[len(args)-1] + err := archiver.Archive(sourceFiles, destinationArchive) + if err != nil { + return fmt.Errorf("unable to perform compression: %w", err) + } + return err } -var unarchiveAll bool +// ArchiverDecompressOptions holds the command-line options for 'tools archiver decompress' sub-command. +type ArchiverDecompressOptions struct { + unarchiveAll bool +} + +// NewArchiverDecompressCommand creates the `tools archiver decompress` sub-command. +func NewArchiverDecompressCommand() *cobra.Command { + o := ArchiverDecompressOptions{} + + cmd := &cobra.Command{ + Use: "decompress ARCHIVE DESTINATION", + Aliases: []string{"d"}, + Short: lang.CmdToolsArchiverDecompressShort, + Args: cobra.ExactArgs(2), + RunE: o.Run, + } -var archiverDecompressCmd = &cobra.Command{ - Use: "decompress ARCHIVE DESTINATION", - Aliases: []string{"d"}, - Short: lang.CmdToolsArchiverDecompressShort, - Args: cobra.ExactArgs(2), - RunE: func(_ *cobra.Command, args []string) error { - sourceArchive, destinationPath := args[0], args[1] - err := archiver.Unarchive(sourceArchive, destinationPath) + cmd.Flags().BoolVar(&o.unarchiveAll, "decompress-all", false, "Decompress all tarballs in the archive") + cmd.Flags().BoolVar(&o.unarchiveAll, "unarchive-all", false, "Unarchive all tarballs in the archive") + cmd.MarkFlagsMutuallyExclusive("decompress-all", "unarchive-all") + cmd.Flags().MarkHidden("decompress-all") + + return cmd +} + +// Run performs the execution of 'tools archiver decompress' sub-command. +func (o *ArchiverDecompressOptions) Run(_ *cobra.Command, args []string) error { + sourceArchive, destinationPath := args[0], args[1] + err := archiver.Unarchive(sourceArchive, destinationPath) + if err != nil { + return fmt.Errorf("unable to perform decompression: %w", err) + } + if !o.unarchiveAll { + return nil + } + err = filepath.Walk(destinationPath, func(path string, info os.FileInfo, err error) error { if err != nil { - return fmt.Errorf("unable to perform decompression: %w", err) - } - if !unarchiveAll { - return nil + return err } - err = filepath.Walk(destinationPath, func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(path, ".tar") { + dst := filepath.Join(strings.TrimSuffix(path, ".tar"), "..") + // Unpack sboms.tar differently since it has a different folder structure than components + if info.Name() == layout.SBOMTar { + dst = strings.TrimSuffix(path, ".tar") + } + err := archiver.Unarchive(path, dst) if err != nil { - return err + return fmt.Errorf(lang.ErrUnarchive, path, err.Error()) } - if strings.HasSuffix(path, ".tar") { - dst := filepath.Join(strings.TrimSuffix(path, ".tar"), "..") - // Unpack sboms.tar differently since it has a different folder structure than components - if info.Name() == layout.SBOMTar { - dst = strings.TrimSuffix(path, ".tar") - } - err := archiver.Unarchive(path, dst) - if err != nil { - return fmt.Errorf(lang.ErrUnarchive, path, err.Error()) - } - err = os.Remove(path) - if err != nil { - return fmt.Errorf(lang.ErrRemoveFile, path, err.Error()) - } + err = os.Remove(path) + if err != nil { + return fmt.Errorf(lang.ErrRemoveFile, path, err.Error()) } - return nil - }) - if err != nil { - return fmt.Errorf("unable to unarchive all nested tarballs: %w", err) } return nil - }, -} - -func init() { - toolsCmd.AddCommand(archiverCmd) - - archiverCmd.AddCommand(archiverCompressCmd) - archiverCmd.AddCommand(archiverDecompressCmd) - archiverCmd.AddCommand(newVersionCmd("mholt/archiver", archiverVersion)) - archiverDecompressCmd.Flags().BoolVar(&unarchiveAll, "decompress-all", false, "Decompress all tarballs in the archive") - archiverDecompressCmd.Flags().BoolVar(&unarchiveAll, "unarchive-all", false, "Unarchive all tarballs in the archive") - archiverDecompressCmd.MarkFlagsMutuallyExclusive("decompress-all", "unarchive-all") - archiverDecompressCmd.Flags().MarkHidden("decompress-all") + }) + if err != nil { + return fmt.Errorf("unable to unarchive all nested tarballs: %w", err) + } + return nil } diff --git a/src/cmd/tools/common.go b/src/cmd/tools/common.go index 0aef6e95c5..4633d0dfba 100644 --- a/src/cmd/tools/common.go +++ b/src/cmd/tools/common.go @@ -8,19 +8,36 @@ import ( "fmt" "github.com/spf13/cobra" - + "github.com/zarf-dev/zarf/src/cmd/common" "github.com/zarf-dev/zarf/src/config/lang" ) -var toolsCmd = &cobra.Command{ - Use: "tools", - Aliases: []string{"t"}, - Short: lang.CmdToolsShort, -} +// NewToolsCommand creates the `tools` sub-command and its nested children. +func NewToolsCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "tools", + Aliases: []string{"t"}, + Short: lang.CmdToolsShort, + } + + v := common.GetViper() + + cmd.AddCommand(NewArchiverCommand()) + cmd.AddCommand(NewRegistryCommand()) + cmd.AddCommand(NewHelmCommand()) + cmd.AddCommand(NewK9sCommand()) + cmd.AddCommand(NewKubectlCommand()) + cmd.AddCommand(NewSbomCommand()) + cmd.AddCommand(NewWaitForCommand()) + cmd.AddCommand(NewYQCommand()) + cmd.AddCommand(NewGetCredsCommand()) + cmd.AddCommand(NewUpdateCredsCommand(v)) + cmd.AddCommand(NewClearCacheCommand()) + cmd.AddCommand(NewDownloadInitCommand()) + cmd.AddCommand(NewGenPKICommand()) + cmd.AddCommand(NewGenKeyCommand()) -// Include adds the tools command to the root command. -func Include(rootCmd *cobra.Command) { - rootCmd.AddCommand(toolsCmd) + return cmd } // newVersionCmd is a generic version command for tools diff --git a/src/cmd/tools/crane.go b/src/cmd/tools/crane.go index 972a81bffc..9c02fd26a0 100644 --- a/src/cmd/tools/crane.go +++ b/src/cmd/tools/crane.go @@ -26,16 +26,27 @@ import ( "github.com/zarf-dev/zarf/src/types" ) -func init() { - verbose := false - insecure := false - ndlayers := false - platform := "all" +// RegistryOptions holds the command-line options for 'tools registry' sub-command. +type RegistryOptions struct { + verbose bool + insecure bool + ndlayers bool + platform string +} + +// NewRegistryCommand creates the `tools registry` sub-command and its nested children. +func NewRegistryCommand() *cobra.Command { + o := &RegistryOptions{ + verbose: false, + insecure: false, + ndlayers: false, + platform: "all", + } // No package information is available so do not pass in a list of architectures craneOptions := []crane.Option{} - registryCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "registry", Aliases: []string{"r", "crane"}, Short: lang.CmdToolsRegistryShort, @@ -48,21 +59,21 @@ func init() { // The crane options loading here comes from the rootCmd of crane craneOptions = append(craneOptions, crane.WithContext(cmd.Context())) // TODO(jonjohnsonjr): crane.Verbose option? - if verbose { + if o.verbose { logs.Debug.SetOutput(os.Stderr) } - if insecure { + if o.insecure { craneOptions = append(craneOptions, crane.Insecure) } - if ndlayers { + if o.ndlayers { craneOptions = append(craneOptions, crane.WithNondistributable()) } var err error var v1Platform *v1.Platform - if platform != "all" { - v1Platform, err = v1.ParsePlatform(platform) + if o.platform != "all" { + v1Platform, err = v1.ParsePlatform(o.platform) if err != nil { - return fmt.Errorf("invalid platform %s: %w", platform, err) + return fmt.Errorf("invalid platform %s: %w", o.platform, err) } } @@ -71,151 +82,125 @@ func init() { }, } - pruneCmd := &cobra.Command{ - Use: "prune", - Aliases: []string{"p"}, - Short: lang.CmdToolsRegistryPruneShort, - RunE: pruneImages, - } + cmd.AddCommand(NewRegistryPruneCommand()) + cmd.AddCommand(NewRegistryLoginCommand()) + cmd.AddCommand(NewRegistryCopyCommand()) + cmd.AddCommand(NewRegistryCatalogCommand()) - // Always require confirm flag (no viper) - pruneCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsRegistryPruneFlagConfirm) + // TODO(soltysh): consider splitting craneOptions to be per command + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdList, &craneOptions, lang.CmdToolsRegistryListExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPush, &craneOptions, lang.CmdToolsRegistryPushExample, 1)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPull, &craneOptions, lang.CmdToolsRegistryPullExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDelete, &craneOptions, lang.CmdToolsRegistryDeleteExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDigest, &craneOptions, lang.CmdToolsRegistryDigestExample, 0)) - craneLogin := craneCmd.NewCmdAuthLogin() - craneLogin.Example = "" + cmd.AddCommand(craneCmd.NewCmdVersion()) - registryCmd.AddCommand(craneLogin) + cmd.PersistentFlags().BoolVarP(&o.verbose, "verbose", "v", false, lang.CmdToolsRegistryFlagVerbose) + cmd.PersistentFlags().BoolVar(&o.insecure, "insecure", false, lang.CmdToolsRegistryFlagInsecure) + cmd.PersistentFlags().BoolVar(&o.ndlayers, "allow-nondistributable-artifacts", false, lang.CmdToolsRegistryFlagNonDist) + cmd.PersistentFlags().StringVar(&o.platform, "platform", "all", lang.CmdToolsRegistryFlagPlatform) - craneCopy := craneCmd.NewCmdCopy(&craneOptions) + return cmd +} - registryCmd.AddCommand(craneCopy) - registryCmd.AddCommand(zarfCraneCatalog(&craneOptions)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdList, &craneOptions, lang.CmdToolsRegistryListExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPush, &craneOptions, lang.CmdToolsRegistryPushExample, 1)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPull, &craneOptions, lang.CmdToolsRegistryPullExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDelete, &craneOptions, lang.CmdToolsRegistryDeleteExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDigest, &craneOptions, lang.CmdToolsRegistryDigestExample, 0)) - registryCmd.AddCommand(pruneCmd) - registryCmd.AddCommand(craneCmd.NewCmdVersion()) +// NewRegistryLoginCommand creates the `tools registry login` sub-command. +func NewRegistryLoginCommand() *cobra.Command { + cmd := craneCmd.NewCmdAuthLogin() + cmd.Example = "" + return cmd +} - registryCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, lang.CmdToolsRegistryFlagVerbose) - registryCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, lang.CmdToolsRegistryFlagInsecure) - registryCmd.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, lang.CmdToolsRegistryFlagNonDist) - registryCmd.PersistentFlags().StringVar(&platform, "platform", "all", lang.CmdToolsRegistryFlagPlatform) +// NewRegistryCopyCommand creates the `tools registry copy` sub-command. +func NewRegistryCopyCommand() *cobra.Command { + // No package information is available so do not pass in a list of architectures + craneOptions := []crane.Option{} + cmd := craneCmd.NewCmdCopy(&craneOptions) + return cmd +} - toolsCmd.AddCommand(registryCmd) +// RegistryCatalogOptions holds the command-line options for 'tools registry catalog' sub-command. +type RegistryCatalogOptions struct { + craneOptions []crane.Option + originalRunFn func(cmd *cobra.Command, args []string) error } -// Wrap the original crane catalog with a zarf specific version -func zarfCraneCatalog(cranePlatformOptions *[]crane.Option) *cobra.Command { - craneCatalog := craneCmd.NewCmdCatalog(cranePlatformOptions) +// NewRegistryCatalogCommand creates the `tools registry catalog` sub-command. +func NewRegistryCatalogCommand() *cobra.Command { + o := RegistryCatalogOptions{ + // No package information is available so do not pass in a list of architectures + craneOptions: []crane.Option{}, + } - craneCatalog.Example = lang.CmdToolsRegistryCatalogExample - craneCatalog.Args = nil + cmd := craneCmd.NewCmdCatalog(&o.craneOptions) + cmd.Example = lang.CmdToolsRegistryCatalogExample + cmd.Args = nil - originalCatalogFn := craneCatalog.RunE + o.originalRunFn = cmd.RunE + cmd.RunE = o.Run - craneCatalog.RunE = func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - l := logger.From(cmd.Context()) - if len(args) > 0 { - return originalCatalogFn(cmd, args) - } + return cmd +} - l.Info("retrieving registry information from Zarf state") +// Run performs the execution of 'tools registry catalog' sub-command. +func (o *RegistryCatalogOptions) Run(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + l := logger.From(cmd.Context()) + if len(args) > 0 { + return o.originalRunFn(cmd, args) + } - c, err := cluster.NewCluster() - if err != nil { - return err - } + l.Info("retrieving registry information from Zarf state") - zarfState, err := c.LoadZarfState(ctx) - if err != nil { - return err - } + c, err := cluster.NewCluster() + if err != nil { + return err + } - registryEndpoint, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) - if err != nil { - return err - } + zarfState, err := c.LoadZarfState(ctx) + if err != nil { + return err + } - // Add the correct authentication to the crane command options - authOption := images.WithPullAuth(zarfState.RegistryInfo) - *cranePlatformOptions = append(*cranePlatformOptions, authOption) + registryEndpoint, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) + if err != nil { + return err + } - if tunnel != nil { - defer tunnel.Close() - return tunnel.Wrap(func() error { return originalCatalogFn(cmd, []string{registryEndpoint}) }) - } + // Add the correct authentication to the crane command options + authOption := images.WithPullAuth(zarfState.RegistryInfo) + o.craneOptions = append(o.craneOptions, authOption) - return originalCatalogFn(cmd, []string{registryEndpoint}) + if tunnel != nil { + defer tunnel.Close() + return tunnel.Wrap(func() error { return o.originalRunFn(cmd, []string{registryEndpoint}) }) } - return craneCatalog + return o.originalRunFn(cmd, []string{registryEndpoint}) } -// Wrap the original crane list with a zarf specific version -func zarfCraneInternalWrapper(commandToWrap func(*[]crane.Option) *cobra.Command, cranePlatformOptions *[]crane.Option, exampleText string, imageNameArgumentIndex int) *cobra.Command { - wrappedCommand := commandToWrap(cranePlatformOptions) - - wrappedCommand.Example = exampleText - wrappedCommand.Args = nil +// RegistryPruneOptions holds the command-line options for 'tools registry prune' sub-command. +type RegistryPruneOptions struct{} - originalListFn := wrappedCommand.RunE +// NewRegistryPruneCommand creates the `tools registry prune` sub-command. +func NewRegistryPruneCommand() *cobra.Command { + o := RegistryPruneOptions{} - wrappedCommand.RunE = func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - l := logger.From(ctx) - if len(args) < imageNameArgumentIndex+1 { - return errors.New("not have enough arguments specified for this command") - } - - // Try to connect to a Zarf initialized cluster otherwise then pass it down to crane. - c, err := cluster.NewCluster() - if err != nil { - return originalListFn(cmd, args) - } - - l.Info("retrieving registry information from Zarf state") - - zarfState, err := c.LoadZarfState(ctx) - if err != nil { - l.Warn("could not get Zarf state from Kubernetes cluster, continuing without state information", "error", err.Error()) - return originalListFn(cmd, args) - } - - // Check to see if it matches the existing internal address. - if !strings.HasPrefix(args[imageNameArgumentIndex], zarfState.RegistryInfo.Address) { - return originalListFn(cmd, args) - } - - _, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) - if err != nil { - return err - } - - // Add the correct authentication to the crane command options - authOption := images.WithPushAuth(zarfState.RegistryInfo) - *cranePlatformOptions = append(*cranePlatformOptions, authOption) - - if tunnel != nil { - l.Info("opening a tunnel to the Zarf registry", "local-endpoint", tunnel.Endpoint(), "cluster-address", zarfState.RegistryInfo.Address) - - defer tunnel.Close() - - givenAddress := fmt.Sprintf("%s/", zarfState.RegistryInfo.Address) - tunnelAddress := fmt.Sprintf("%s/", tunnel.Endpoint()) - args[imageNameArgumentIndex] = strings.Replace(args[imageNameArgumentIndex], givenAddress, tunnelAddress, 1) - return tunnel.Wrap(func() error { return originalListFn(cmd, args) }) - } - - return originalListFn(cmd, args) + cmd := &cobra.Command{ + Use: "prune", + Aliases: []string{"p"}, + Short: lang.CmdToolsRegistryPruneShort, + RunE: o.Run, } - return wrappedCommand + // Always require confirm flag (no viper) + cmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsRegistryPruneFlagConfirm) + + return cmd } -func pruneImages(cmd *cobra.Command, _ []string) error { +// Run performs the execution of 'tools registry prune' sub-command. +func (o *RegistryPruneOptions) Run(cmd *cobra.Command, _ []string) error { // Try to connect to a Zarf initialized cluster c, err := cluster.NewCluster() if err != nil { @@ -351,3 +336,64 @@ func doPruneImagesForPackages(ctx context.Context, zarfState *types.ZarfState, z } return nil } + +// Wrap the original crane list with a zarf specific version +func zarfCraneInternalWrapper(commandToWrap func(*[]crane.Option) *cobra.Command, cranePlatformOptions *[]crane.Option, exampleText string, imageNameArgumentIndex int) *cobra.Command { + wrappedCommand := commandToWrap(cranePlatformOptions) + + wrappedCommand.Example = exampleText + wrappedCommand.Args = nil + + originalListFn := wrappedCommand.RunE + + wrappedCommand.RunE = func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + l := logger.From(ctx) + if len(args) < imageNameArgumentIndex+1 { + return errors.New("not have enough arguments specified for this command") + } + + // Try to connect to a Zarf initialized cluster otherwise then pass it down to crane. + c, err := cluster.NewCluster() + if err != nil { + return originalListFn(cmd, args) + } + + l.Info("retrieving registry information from Zarf state") + + zarfState, err := c.LoadZarfState(ctx) + if err != nil { + l.Warn("could not get Zarf state from Kubernetes cluster, continuing without state information", "error", err.Error()) + return originalListFn(cmd, args) + } + + // Check to see if it matches the existing internal address. + if !strings.HasPrefix(args[imageNameArgumentIndex], zarfState.RegistryInfo.Address) { + return originalListFn(cmd, args) + } + + _, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) + if err != nil { + return err + } + + // Add the correct authentication to the crane command options + authOption := images.WithPushAuth(zarfState.RegistryInfo) + *cranePlatformOptions = append(*cranePlatformOptions, authOption) + + if tunnel != nil { + l.Info("opening a tunnel to the Zarf registry", "local-endpoint", tunnel.Endpoint(), "cluster-address", zarfState.RegistryInfo.Address) + + defer tunnel.Close() + + givenAddress := fmt.Sprintf("%s/", zarfState.RegistryInfo.Address) + tunnelAddress := fmt.Sprintf("%s/", tunnel.Endpoint()) + args[imageNameArgumentIndex] = strings.Replace(args[imageNameArgumentIndex], givenAddress, tunnelAddress, 1) + return tunnel.Wrap(func() error { return originalListFn(cmd, args) }) + } + + return originalListFn(cmd, args) + } + + return wrappedCommand +} diff --git a/src/cmd/tools/helm.go b/src/cmd/tools/helm.go index b6c08e316c..fcc3171034 100644 --- a/src/cmd/tools/helm.go +++ b/src/cmd/tools/helm.go @@ -7,18 +7,19 @@ package tools import ( "os" - "github.com/zarf-dev/zarf/src/pkg/logger" - "github.com/zarf-dev/zarf/src/pkg/message" - + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/cmd/tools/helm" "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/zarf-dev/zarf/src/pkg/message" "helm.sh/helm/v3/pkg/action" ) // ldflags github.com/zarf-dev/zarf/src/cmd/tools.helmVersion=x.x.x var helmVersion string -func init() { +// NewHelmCommand creates the `tools helm` sub-command. +func NewHelmCommand() *cobra.Command { actionConfig := new(action.Configuration) // Truncate Helm's arguments so that it thinks its all alone @@ -27,14 +28,14 @@ func init() { helmArgs = os.Args[3:] } // The inclusion of Helm in this manner should be changed once https://github.com/helm/helm/pull/12725 is merged - helmCmd, err := helm.NewRootCmd(actionConfig, os.Stdout, helmArgs) + cmd, err := helm.NewRootCmd(actionConfig, os.Stdout, helmArgs) if err != nil { message.Debug("Failed to initialize helm command", "error", err) logger.Default().Debug("failed to initialize helm command", "error", err) } - helmCmd.Short = lang.CmdToolsHelmShort - helmCmd.Long = lang.CmdToolsHelmLong - helmCmd.AddCommand(newVersionCmd("helm", helmVersion)) + cmd.Short = lang.CmdToolsHelmShort + cmd.Long = lang.CmdToolsHelmLong + cmd.AddCommand(newVersionCmd("helm", helmVersion)) - toolsCmd.AddCommand(helmCmd) + return cmd } diff --git a/src/cmd/tools/k9s.go b/src/cmd/tools/k9s.go index 4866548ce8..80e10d2c76 100644 --- a/src/cmd/tools/k9s.go +++ b/src/cmd/tools/k9s.go @@ -18,8 +18,9 @@ import ( //go:linkname k9sRootCmd github.com/derailed/k9s/cmd.rootCmd var k9sRootCmd *cobra.Command -func init() { - k9sCmd := &cobra.Command{ +// NewK9sCommand creates the `tools k9s` sub-command. +func NewK9sCommand() *cobra.Command { + cmd := &cobra.Command{ Use: "monitor", Aliases: []string{"m", "k9s"}, Short: lang.CmdToolsMonitorShort, @@ -30,7 +31,7 @@ func init() { }, } - k9sCmd.Flags().AddFlagSet(k9sRootCmd.Flags()) + cmd.Flags().AddFlagSet(k9sRootCmd.Flags()) - toolsCmd.AddCommand(k9sCmd) + return cmd } diff --git a/src/cmd/tools/kubectl.go b/src/cmd/tools/kubectl.go index 4054a4e3e6..b65d36b85a 100644 --- a/src/cmd/tools/kubectl.go +++ b/src/cmd/tools/kubectl.go @@ -19,9 +19,10 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" ) -func init() { +// NewKubectlCommand creates the `tools kubectl` sub-command. +func NewKubectlCommand() *cobra.Command { // Kubectl stub command. - kubectlCmd := &cobra.Command{ + cmd := &cobra.Command{ Short: lang.CmdToolsKubectlDocs, Run: func(_ *cobra.Command, _ []string) {}, } @@ -29,17 +30,17 @@ func init() { // Only load this command if it is being called directly. if common.IsVendorCmd(os.Args, []string{"kubectl", "k"}) { // Add the kubectl command to the tools command. - kubectlCmd = kubeCmd.NewDefaultKubectlCommand() + cmd = kubeCmd.NewDefaultKubectlCommand() - if err := kubeCLI.RunNoErrOutput(kubectlCmd); err != nil { + if err := kubeCLI.RunNoErrOutput(cmd); err != nil { // @todo(jeff-mccoy) - Kubectl gets mad about being a subcommand. message.Debug(err) logger.Default().Debug(err.Error()) } } - kubectlCmd.Use = "kubectl" - kubectlCmd.Aliases = []string{"k"} + cmd.Use = "kubectl" + cmd.Aliases = []string{"k"} - toolsCmd.AddCommand(kubectlCmd) + return cmd } diff --git a/src/cmd/tools/syft.go b/src/cmd/tools/syft.go index 57051021a0..7822ea55e8 100644 --- a/src/cmd/tools/syft.go +++ b/src/cmd/tools/syft.go @@ -7,25 +7,27 @@ package tools import ( "github.com/anchore/clio" syftCLI "github.com/anchore/syft/cmd/syft/cli" + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/config/lang" ) // ldflags github.com/zarf-dev/zarf/src/cmd/tools.syftVersion=x.x.x var syftVersion string -func init() { - syftCmd := syftCLI.Command(clio.Identification{ +// NewSbomCommand creates the `tools sbom` sub-command. +func NewSbomCommand() *cobra.Command { + cmd := syftCLI.Command(clio.Identification{ Name: "syft", Version: syftVersion, }) - syftCmd.Use = "sbom" - syftCmd.Short = lang.CmdToolsSbomShort - syftCmd.Aliases = []string{"s", "syft"} - syftCmd.Example = "" + cmd.Use = "sbom" + cmd.Short = lang.CmdToolsSbomShort + cmd.Aliases = []string{"s", "syft"} + cmd.Example = "" - for _, subCmd := range syftCmd.Commands() { + for _, subCmd := range cmd.Commands() { subCmd.Example = "" } - toolsCmd.AddCommand(syftCmd) + return cmd } diff --git a/src/cmd/tools/wait.go b/src/cmd/tools/wait.go index fda0344fbc..f58b87376e 100644 --- a/src/cmd/tools/wait.go +++ b/src/cmd/tools/wait.go @@ -17,50 +17,54 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" ) -var ( +// WaitForOptions holds the command-line options for 'tools registry' sub-command. +type WaitForOptions struct { waitTimeout string waitNamespace string -) +} + +// NewWaitForCommand creates the `tools wait-for` sub-command. +func NewWaitForCommand() *cobra.Command { + o := WaitForOptions{} + cmd := &cobra.Command{ + Use: "wait-for { KIND | PROTOCOL } { NAME | SELECTOR | URI } { CONDITION | HTTP_CODE }", + Aliases: []string{"w", "wait"}, + Short: lang.CmdToolsWaitForShort, + Long: lang.CmdToolsWaitForLong, + Example: lang.CmdToolsWaitForExample, + Args: cobra.MinimumNArgs(1), + RunE: o.Run, + } -var waitForCmd = &cobra.Command{ - Use: "wait-for { KIND | PROTOCOL } { NAME | SELECTOR | URI } { CONDITION | HTTP_CODE }", - Aliases: []string{"w", "wait"}, - Short: lang.CmdToolsWaitForShort, - Long: lang.CmdToolsWaitForLong, - Example: lang.CmdToolsWaitForExample, - Args: cobra.MinimumNArgs(1), - RunE: func(_ *cobra.Command, args []string) error { - // Parse the timeout string - timeout, err := time.ParseDuration(waitTimeout) - if err != nil { - return fmt.Errorf("invalid timeout duration %s, use a valid duration string e.g. 1s, 2m, 3h: %w", waitTimeout, err) - } + cmd.Flags().StringVar(&o.waitTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) + cmd.Flags().StringVarP(&o.waitNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) + cmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) - kind := args[0] + return cmd +} - // identifier is optional to allow for commands like `zarf tools wait-for storageclass` without specifying a name. - identifier := "" - if len(args) > 1 { - identifier = args[1] - } +// Run performs the execution of 'tools wait-for' sub-command. +func (o *WaitForOptions) Run(_ *cobra.Command, args []string) error { + // Parse the timeout string + timeout, err := time.ParseDuration(o.waitTimeout) + if err != nil { + return fmt.Errorf("invalid timeout duration %s, use a valid duration string e.g. 1s, 2m, 3h: %w", o.waitTimeout, err) + } - // Condition is optional, default to "exists". - condition := "" - if len(args) > 2 { - condition = args[2] - } + kind := args[0] - // Execute the wait command. - if err := utils.ExecuteWait(waitTimeout, waitNamespace, condition, kind, identifier, timeout); err != nil { - return err - } - return err - }, -} + // identifier is optional to allow for commands like `zarf tools wait-for storageclass` without specifying a name. + identifier := "" + if len(args) > 1 { + identifier = args[1] + } + + // Condition is optional, default to "exists". + condition := "" + if len(args) > 2 { + condition = args[2] + } -func init() { - toolsCmd.AddCommand(waitForCmd) - waitForCmd.Flags().StringVar(&waitTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) - waitForCmd.Flags().StringVarP(&waitNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) - waitForCmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) + // Execute the wait command. + return utils.ExecuteWait(o.waitTimeout, o.waitNamespace, condition, kind, identifier, timeout) } diff --git a/src/cmd/tools/yq.go b/src/cmd/tools/yq.go index 41839f568b..3528980702 100644 --- a/src/cmd/tools/yq.go +++ b/src/cmd/tools/yq.go @@ -6,14 +6,16 @@ package tools import ( yq "github.com/mikefarah/yq/v4/cmd" + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/config/lang" ) -func init() { - yqCmd := yq.New() - yqCmd.Example = lang.CmdToolsYqExample - yqCmd.Use = "yq" - for _, subCmd := range yqCmd.Commands() { +// NewYQCommand creates the `tools yq` sub-command and its nested children. +func NewYQCommand() *cobra.Command { + cmd := yq.New() + cmd.Example = lang.CmdToolsYqExample + cmd.Use = "yq" + for _, subCmd := range cmd.Commands() { if subCmd.Name() == "eval" { subCmd.Example = lang.CmdToolsYqEvalExample } @@ -22,5 +24,5 @@ func init() { } } - toolsCmd.AddCommand(yqCmd) + return cmd } diff --git a/src/cmd/tools/zarf.go b/src/cmd/tools/zarf.go index cb2904389a..16bb468455 100644 --- a/src/cmd/tools/zarf.go +++ b/src/cmd/tools/zarf.go @@ -13,11 +13,11 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" - "github.com/sigstore/cosign/v2/pkg/cosign" - "github.com/spf13/cobra" - "github.com/defenseunicorns/pkg/helpers/v2" "github.com/defenseunicorns/pkg/oci" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/zarf-dev/zarf/src/cmd/common" "github.com/zarf-dev/zarf/src/config" @@ -46,163 +46,55 @@ const ( agentKey = "agent" ) -var getCredsCmd = &cobra.Command{ - Use: "get-creds", - Short: lang.CmdToolsGetCredsShort, - Long: lang.CmdToolsGetCredsLong, - Example: lang.CmdToolsGetCredsExample, - Aliases: []string{"gc"}, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) - defer cancel() - c, err := cluster.NewClusterWithWait(timeoutCtx) - if err != nil { - return err - } - - state, err := c.LoadZarfState(ctx) - if err != nil { - return err - } - // TODO: Determine if this is actually needed. - if state.Distro == "" { - return errors.New("zarf state secret did not load properly") - } +// GetCredsOptions holds the command-line options for 'tools get-creds' sub-command. +type GetCredsOptions struct{} + +// NewGetCredsCommand creates the `tools get-creds` sub-command. +func NewGetCredsCommand() *cobra.Command { + o := GetCredsOptions{} + + cmd := &cobra.Command{ + Use: "get-creds", + Short: lang.CmdToolsGetCredsShort, + Long: lang.CmdToolsGetCredsLong, + Example: lang.CmdToolsGetCredsExample, + Aliases: []string{"gc"}, + Args: cobra.MaximumNArgs(1), + RunE: o.Run, + } - if len(args) > 0 { - // If a component name is provided, only show that component's credentials - // Printing both the pterm output and slogger for now - printComponentCredential(ctx, state, args[0]) - message.PrintComponentCredential(state, args[0]) - } else { - message.PrintCredentialTable(state, nil) - } - return nil - }, + return cmd } -var updateCredsCmd = &cobra.Command{ - Use: "update-creds", - Short: lang.CmdToolsUpdateCredsShort, - Long: lang.CmdToolsUpdateCredsLong, - Example: lang.CmdToolsUpdateCredsExample, - Aliases: []string{"uc"}, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - validKeys := []string{message.RegistryKey, message.GitKey, message.ArtifactKey, message.AgentKey} - if len(args) == 0 { - args = validKeys - } else { - if !slices.Contains(validKeys, args[0]) { - cmd.Help() - return fmt.Errorf("invalid service key specified, valid key choices are: %v", validKeys) - } - } - - ctx := cmd.Context() - l := logger.From(ctx) +// Run performs the execution of 'tools get-creds' sub-command. +func (o *GetCredsOptions) Run(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() - timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) - defer cancel() - c, err := cluster.NewClusterWithWait(timeoutCtx) - if err != nil { - return err - } + timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) + defer cancel() + c, err := cluster.NewClusterWithWait(timeoutCtx) + if err != nil { + return err + } - oldState, err := c.LoadZarfState(ctx) - if err != nil { - return err - } - // TODO: Determine if this is actually needed. - if oldState.Distro == "" { - return errors.New("zarf state secret did not load properly") - } - newState, err := cluster.MergeZarfState(oldState, updateCredsInitOpts, args) - if err != nil { - return fmt.Errorf("unable to update Zarf credentials: %w", err) - } + state, err := c.LoadZarfState(ctx) + if err != nil { + return err + } + // TODO: Determine if this is actually needed. + if state.Distro == "" { + return errors.New("zarf state secret did not load properly") + } + if len(args) > 0 { + // If a component name is provided, only show that component's credentials // Printing both the pterm output and slogger for now - message.PrintCredentialUpdates(oldState, newState, args) - printCredentialUpdates(ctx, oldState, newState, args) - - confirm := config.CommonOptions.Confirm - - if !confirm { - prompt := &survey.Confirm{ - Message: lang.CmdToolsUpdateCredsConfirmContinue, - } - if err := survey.AskOne(prompt, &confirm); err != nil { - return fmt.Errorf("confirm selection canceled: %w", err) - } - } - - if confirm { - // Update registry and git pull secrets - if slices.Contains(args, message.RegistryKey) { - err := c.UpdateZarfManagedImageSecrets(ctx, newState) - if err != nil { - return err - } - } - if slices.Contains(args, message.GitKey) { - err := c.UpdateZarfManagedGitSecrets(ctx, newState) - if err != nil { - return err - } - } - // TODO once Zarf is changed so the default state is empty for a service when it is not deployed - // and sufficient time has passed for users state to get updated we can remove this check - internalGitServerExists, err := c.InternalGitServerExists(cmd.Context()) - if err != nil { - return err - } - - // Update artifact token (if internal) - if slices.Contains(args, message.ArtifactKey) && newState.ArtifactServer.PushToken == "" && newState.ArtifactServer.IsInternal() && internalGitServerExists { - newState.ArtifactServer.PushToken, err = c.UpdateInternalArtifactServerToken(ctx, oldState.GitServer) - if err != nil { - return fmt.Errorf("unable to create the new Gitea artifact token: %w", err) - } - } - - // Save the final Zarf State - err = c.SaveZarfState(ctx, newState) - if err != nil { - return fmt.Errorf("failed to save the Zarf State to the cluster: %w", err) - } - - // Update Zarf 'init' component Helm releases if present - h := helm.NewClusterOnly(&types.PackagerConfig{}, template.GetZarfVariableConfig(cmd.Context()), newState, c) - - if slices.Contains(args, message.RegistryKey) && newState.RegistryInfo.IsInternal() { - err = h.UpdateZarfRegistryValues(ctx) - if err != nil { - // Warn if we couldn't actually update the registry (it might not be installed and we should try to continue) - message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateRegistry, err.Error()) - l.Warn("unable to update Zarf Registry values", "error", err.Error()) - } - } - if slices.Contains(args, message.GitKey) && newState.GitServer.IsInternal() && internalGitServerExists { - err := c.UpdateInternalGitServerSecret(cmd.Context(), oldState.GitServer, newState.GitServer) - if err != nil { - return fmt.Errorf("unable to update Zarf Git Server values: %w", err) - } - } - if slices.Contains(args, message.AgentKey) { - err = h.UpdateZarfAgentValues(ctx) - if err != nil { - // Warn if we couldn't actually update the agent (it might not be installed and we should try to continue) - message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateAgent, err.Error()) - l.Warn("unable to update Zarf Agent TLS secrets", "error", err.Error()) - } - } - } - return nil - }, + printComponentCredential(ctx, state, args[0]) + message.PrintComponentCredential(state, args[0]) + } else { + message.PrintCredentialTable(state, nil) + } + return nil } func printComponentCredential(ctx context.Context, state *types.ZarfState, componentName string) { @@ -225,6 +117,167 @@ func printComponentCredential(ctx context.Context, state *types.ZarfState, compo } } +// UpdateCredsOptions holds the command-line options for 'tools update-creds' sub-command. +type UpdateCredsOptions struct{} + +// NewUpdateCredsCommand creates the `tools update-creds` sub-command. +func NewUpdateCredsCommand(v *viper.Viper) *cobra.Command { + o := UpdateCredsOptions{} + + cmd := &cobra.Command{ + Use: "update-creds", + Short: lang.CmdToolsUpdateCredsShort, + Long: lang.CmdToolsUpdateCredsLong, + Example: lang.CmdToolsUpdateCredsExample, + Aliases: []string{"uc"}, + Args: cobra.MaximumNArgs(1), + RunE: o.Run, + } + + // Always require confirm flag (no viper) + cmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsUpdateCredsConfirmFlag) + + // Flags for using an external Git server + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.Address, "git-url", v.GetString(common.VInitGitURL), lang.CmdInitFlagGitURL) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushUsername, "git-push-username", v.GetString(common.VInitGitPushUser), lang.CmdInitFlagGitPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushPassword, "git-push-password", v.GetString(common.VInitGitPushPass), lang.CmdInitFlagGitPushPass) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullUsername, "git-pull-username", v.GetString(common.VInitGitPullUser), lang.CmdInitFlagGitPullUser) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullPassword, "git-pull-password", v.GetString(common.VInitGitPullPass), lang.CmdInitFlagGitPullPass) + + // Flags for using an external registry + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.Address, "registry-url", v.GetString(common.VInitRegistryURL), lang.CmdInitFlagRegURL) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushUsername, "registry-push-username", v.GetString(common.VInitRegistryPushUser), lang.CmdInitFlagRegPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushPassword, "registry-push-password", v.GetString(common.VInitRegistryPushPass), lang.CmdInitFlagRegPushPass) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullUsername, "registry-pull-username", v.GetString(common.VInitRegistryPullUser), lang.CmdInitFlagRegPullUser) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullPassword, "registry-pull-password", v.GetString(common.VInitRegistryPullPass), lang.CmdInitFlagRegPullPass) + + // Flags for using an external artifact server + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.Address, "artifact-url", v.GetString(common.VInitArtifactURL), lang.CmdInitFlagArtifactURL) + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushUsername, "artifact-push-username", v.GetString(common.VInitArtifactPushUser), lang.CmdInitFlagArtifactPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushToken, "artifact-push-token", v.GetString(common.VInitArtifactPushToken), lang.CmdInitFlagArtifactPushToken) + + cmd.Flags().SortFlags = true + + return cmd +} + +// Run performs the execution of 'tools update-creds' sub-command. +func (o *UpdateCredsOptions) Run(cmd *cobra.Command, args []string) error { + validKeys := []string{message.RegistryKey, message.GitKey, message.ArtifactKey, message.AgentKey} + if len(args) == 0 { + args = validKeys + } else { + if !slices.Contains(validKeys, args[0]) { + cmd.Help() + return fmt.Errorf("invalid service key specified, valid key choices are: %v", validKeys) + } + } + + ctx := cmd.Context() + l := logger.From(ctx) + + timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) + defer cancel() + c, err := cluster.NewClusterWithWait(timeoutCtx) + if err != nil { + return err + } + + oldState, err := c.LoadZarfState(ctx) + if err != nil { + return err + } + // TODO: Determine if this is actually needed. + if oldState.Distro == "" { + return errors.New("zarf state secret did not load properly") + } + newState, err := cluster.MergeZarfState(oldState, updateCredsInitOpts, args) + if err != nil { + return fmt.Errorf("unable to update Zarf credentials: %w", err) + } + + // Printing both the pterm output and slogger for now + message.PrintCredentialUpdates(oldState, newState, args) + printCredentialUpdates(ctx, oldState, newState, args) + + confirm := config.CommonOptions.Confirm + + if !confirm { + prompt := &survey.Confirm{ + Message: lang.CmdToolsUpdateCredsConfirmContinue, + } + if err := survey.AskOne(prompt, &confirm); err != nil { + return fmt.Errorf("confirm selection canceled: %w", err) + } + } + + if !confirm { + return nil + } + + // Update registry and git pull secrets + if slices.Contains(args, message.RegistryKey) { + err := c.UpdateZarfManagedImageSecrets(ctx, newState) + if err != nil { + return err + } + } + if slices.Contains(args, message.GitKey) { + err := c.UpdateZarfManagedGitSecrets(ctx, newState) + if err != nil { + return err + } + } + // TODO once Zarf is changed so the default state is empty for a service when it is not deployed + // and sufficient time has passed for users state to get updated we can remove this check + internalGitServerExists, err := c.InternalGitServerExists(cmd.Context()) + if err != nil { + return err + } + + // Update artifact token (if internal) + if slices.Contains(args, message.ArtifactKey) && newState.ArtifactServer.PushToken == "" && newState.ArtifactServer.IsInternal() && internalGitServerExists { + newState.ArtifactServer.PushToken, err = c.UpdateInternalArtifactServerToken(ctx, oldState.GitServer) + if err != nil { + return fmt.Errorf("unable to create the new Gitea artifact token: %w", err) + } + } + + // Save the final Zarf State + err = c.SaveZarfState(ctx, newState) + if err != nil { + return fmt.Errorf("failed to save the Zarf State to the cluster: %w", err) + } + + // Update Zarf 'init' component Helm releases if present + h := helm.NewClusterOnly(&types.PackagerConfig{}, template.GetZarfVariableConfig(cmd.Context()), newState, c) + + if slices.Contains(args, message.RegistryKey) && newState.RegistryInfo.IsInternal() { + err = h.UpdateZarfRegistryValues(ctx) + if err != nil { + // Warn if we couldn't actually update the registry (it might not be installed and we should try to continue) + message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateRegistry, err.Error()) + l.Warn("unable to update Zarf Registry values", "error", err.Error()) + } + } + if slices.Contains(args, message.GitKey) && newState.GitServer.IsInternal() && internalGitServerExists { + err := c.UpdateInternalGitServerSecret(cmd.Context(), oldState.GitServer, newState.GitServer) + if err != nil { + return fmt.Errorf("unable to update Zarf Git Server values: %w", err) + } + } + if slices.Contains(args, message.AgentKey) { + err = h.UpdateZarfAgentValues(ctx) + if err != nil { + // Warn if we couldn't actually update the agent (it might not be installed and we should try to continue) + message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateAgent, err.Error()) + l.Warn("unable to update Zarf Agent TLS secrets", "error", err.Error()) + } + } + + return nil +} + func printCredentialUpdates(ctx context.Context, oldState *types.ZarfState, newState *types.ZarfState, services []string) { // Pause the logfile's output to avoid credentials being printed to the log file l := logger.From(ctx) @@ -263,184 +316,202 @@ func printCredentialUpdates(ctx context.Context, oldState *types.ZarfState, newS } } -var clearCacheCmd = &cobra.Command{ - Use: "clear-cache", - Aliases: []string{"c"}, - Short: lang.CmdToolsClearCacheShort, - RunE: func(cmd *cobra.Command, _ []string) error { - l := logger.From(cmd.Context()) - cachePath, err := config.GetAbsCachePath() - if err != nil { - return err - } - message.Notef(lang.CmdToolsClearCacheDir, cachePath) - l.Info("clearing cache", "path", cachePath) - if err := os.RemoveAll(cachePath); err != nil { - return fmt.Errorf("unable to clear the cache directory %s: %w", cachePath, err) - } - message.Successf(lang.CmdToolsClearCacheSuccess, cachePath) - return nil - }, +// ClearCacheOptions holds the command-line options for 'tools clear-cache' sub-command. +type ClearCacheOptions struct{} + +// NewClearCacheCommand creates the `tools clear-cache` sub-command. +func NewClearCacheCommand() *cobra.Command { + o := &ClearCacheOptions{} + + cmd := &cobra.Command{ + Use: "clear-cache", + Aliases: []string{"c"}, + Short: lang.CmdToolsClearCacheShort, + RunE: o.Run, + } + + cmd.Flags().StringVar(&config.CommonOptions.CachePath, "zarf-cache", config.ZarfDefaultCachePath, lang.CmdToolsClearCacheFlagCachePath) + + return cmd } -var downloadInitCmd = &cobra.Command{ - Use: "download-init", - Short: lang.CmdToolsDownloadInitShort, - RunE: func(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - url := zoci.GetInitPackageURL(config.CLIVersion) - remote, err := zoci.NewRemote(ctx, url, oci.PlatformForArch(config.GetArch())) - if err != nil { - return fmt.Errorf("unable to download the init package: %w", err) - } - source := &sources.OCISource{Remote: remote} - _, err = source.Collect(ctx, outputDirectory) - if err != nil { - return fmt.Errorf("unable to download the init package: %w", err) - } - return nil - }, +// Run performs the execution of 'tools clear-cache' sub-command. +func (o *ClearCacheOptions) Run(cmd *cobra.Command, _ []string) error { + l := logger.From(cmd.Context()) + cachePath, err := config.GetAbsCachePath() + if err != nil { + return err + } + message.Notef(lang.CmdToolsClearCacheDir, cachePath) + l.Info("clearing cache", "path", cachePath) + if err := os.RemoveAll(cachePath); err != nil { + return fmt.Errorf("unable to clear the cache directory %s: %w", cachePath, err) + } + message.Successf(lang.CmdToolsClearCacheSuccess, cachePath) + + return nil } -var generatePKICmd = &cobra.Command{ - Use: "gen-pki HOST", - Aliases: []string{"pki"}, - Short: lang.CmdToolsGenPkiShort, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - pki, err := pki.GeneratePKI(args[0], subAltNames...) - if err != nil { - return err - } - if err := os.WriteFile("tls.ca", pki.CA, helpers.ReadAllWriteUser); err != nil { - return err - } - if err := os.WriteFile("tls.crt", pki.Cert, helpers.ReadAllWriteUser); err != nil { - return err - } - if err := os.WriteFile("tls.key", pki.Key, helpers.ReadWriteUser); err != nil { - return err - } - message.Successf(lang.CmdToolsGenPkiSuccess, args[0]) - logger.From(cmd.Context()).Info("successfully created a chain of trust", "host", args[0]) - return nil - }, +// DownloadInitOptions holds the command-line options for 'tools download-init' sub-command. +type DownloadInitOptions struct{} + +// NewDownloadInitCommand creates the `tools download-init` sub-command. +func NewDownloadInitCommand() *cobra.Command { + o := &DownloadInitOptions{} + + cmd := &cobra.Command{ + Use: "download-init", + Short: lang.CmdToolsDownloadInitShort, + RunE: o.Run, + } + + cmd.Flags().StringVarP(&outputDirectory, "output-directory", "o", "", lang.CmdToolsDownloadInitFlagOutputDirectory) + + return cmd } -var generateKeyCmd = &cobra.Command{ - Use: "gen-key", - Aliases: []string{"key"}, - Short: lang.CmdToolsGenKeyShort, - RunE: func(cmd *cobra.Command, _ []string) error { - // Utility function to prompt the user for the password to the private key - passwordFunc := func(bool) ([]byte, error) { - // perform the first prompt - var password string - prompt := &survey.Password{ - Message: lang.CmdToolsGenKeyPrompt, - } - if err := survey.AskOne(prompt, &password); err != nil { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) - } - - // perform the second prompt - var doubleCheck string - rePrompt := &survey.Password{ - Message: lang.CmdToolsGenKeyPromptAgain, - } - if err := survey.AskOne(rePrompt, &doubleCheck); err != nil { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) - } - - // check if the passwords match - if password != doubleCheck { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrPasswordsNotMatch) - } - - return []byte(password), nil - } +// Run performs the execution of 'tools download-init' sub-command. +func (o *DownloadInitOptions) Run(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + url := zoci.GetInitPackageURL(config.CLIVersion) + remote, err := zoci.NewRemote(ctx, url, oci.PlatformForArch(config.GetArch())) + if err != nil { + return fmt.Errorf("unable to download the init package: %w", err) + } + source := &sources.OCISource{Remote: remote} + _, err = source.Collect(ctx, outputDirectory) + if err != nil { + return fmt.Errorf("unable to download the init package: %w", err) + } + return nil +} - // Use cosign to generate the keypair - keyBytes, err := cosign.GenerateKeyPair(passwordFunc) - if err != nil { - return fmt.Errorf("unable to generate key pair: %w", err) - } +// GenPKIOptions holds the command-line options for 'tools gen-pki' sub-command. +type GenPKIOptions struct{} - prvKeyFileName := "cosign.key" - pubKeyFileName := "cosign.pub" - - // Check if we are about to overwrite existing key files - _, prvKeyExistsErr := os.Stat(prvKeyFileName) - _, pubKeyExistsErr := os.Stat(pubKeyFileName) - if prvKeyExistsErr == nil || pubKeyExistsErr == nil { - var confirm bool - confirmOverwritePrompt := &survey.Confirm{ - Message: fmt.Sprintf(lang.CmdToolsGenKeyPromptExists, prvKeyFileName), - } - err := survey.AskOne(confirmOverwritePrompt, &confirm) - if err != nil { - return err - } - if !confirm { - return errors.New("did not receive confirmation for overwriting key file(s)") - } - } +// NewGenPKICommand creates the `tools gen-pki` sub-command. +func NewGenPKICommand() *cobra.Command { + o := &GenPKIOptions{} - // Write the key file contents to disk - if err := os.WriteFile(prvKeyFileName, keyBytes.PrivateBytes, helpers.ReadWriteUser); err != nil { - return err - } - if err := os.WriteFile(pubKeyFileName, keyBytes.PublicBytes, helpers.ReadAllWriteUser); err != nil { - return err - } + cmd := &cobra.Command{ + Use: "gen-pki HOST", + Aliases: []string{"pki"}, + Short: lang.CmdToolsGenPkiShort, + Args: cobra.ExactArgs(1), + RunE: o.Run, + } - message.Successf(lang.CmdToolsGenKeySuccess, prvKeyFileName, pubKeyFileName) - logger.From(cmd.Context()).Info("Successfully generated key pair", - "private-key-path", prvKeyExistsErr, - "public-key-path", pubKeyFileName) - return nil - }, + cmd.Flags().StringArrayVar(&subAltNames, "sub-alt-name", []string{}, lang.CmdToolsGenPkiFlagAltName) + + return cmd } -func init() { - v := common.InitViper() +// Run performs the execution of 'tools gen-pki' sub-command. +func (o *GenPKIOptions) Run(cmd *cobra.Command, args []string) error { + pki, err := pki.GeneratePKI(args[0], subAltNames...) + if err != nil { + return err + } + if err := os.WriteFile("tls.ca", pki.CA, helpers.ReadAllWriteUser); err != nil { + return err + } + if err := os.WriteFile("tls.crt", pki.Cert, helpers.ReadAllWriteUser); err != nil { + return err + } + if err := os.WriteFile("tls.key", pki.Key, helpers.ReadWriteUser); err != nil { + return err + } + message.Successf(lang.CmdToolsGenPkiSuccess, args[0]) + logger.From(cmd.Context()).Info("successfully created a chain of trust", "host", args[0]) - toolsCmd.AddCommand(getCredsCmd) + return nil +} - toolsCmd.AddCommand(updateCredsCmd) +// GenKeyOptions holds the command-line options for 'tools gen-key' sub-command. +type GenKeyOptions struct{} - // Always require confirm flag (no viper) - updateCredsCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsUpdateCredsConfirmFlag) +// NewGenKeyCommand creates the `tools gen-key` sub-command. +func NewGenKeyCommand() *cobra.Command { + o := &GenKeyOptions{} - // Flags for using an external Git server - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.Address, "git-url", v.GetString(common.VInitGitURL), lang.CmdInitFlagGitURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushUsername, "git-push-username", v.GetString(common.VInitGitPushUser), lang.CmdInitFlagGitPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushPassword, "git-push-password", v.GetString(common.VInitGitPushPass), lang.CmdInitFlagGitPushPass) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullUsername, "git-pull-username", v.GetString(common.VInitGitPullUser), lang.CmdInitFlagGitPullUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullPassword, "git-pull-password", v.GetString(common.VInitGitPullPass), lang.CmdInitFlagGitPullPass) + cmd := &cobra.Command{ + Use: "gen-key", + Aliases: []string{"key"}, + Short: lang.CmdToolsGenKeyShort, + RunE: o.Run, + } - // Flags for using an external registry - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.Address, "registry-url", v.GetString(common.VInitRegistryURL), lang.CmdInitFlagRegURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushUsername, "registry-push-username", v.GetString(common.VInitRegistryPushUser), lang.CmdInitFlagRegPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushPassword, "registry-push-password", v.GetString(common.VInitRegistryPushPass), lang.CmdInitFlagRegPushPass) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullUsername, "registry-pull-username", v.GetString(common.VInitRegistryPullUser), lang.CmdInitFlagRegPullUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullPassword, "registry-pull-password", v.GetString(common.VInitRegistryPullPass), lang.CmdInitFlagRegPullPass) + return cmd +} - // Flags for using an external artifact server - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.Address, "artifact-url", v.GetString(common.VInitArtifactURL), lang.CmdInitFlagArtifactURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushUsername, "artifact-push-username", v.GetString(common.VInitArtifactPushUser), lang.CmdInitFlagArtifactPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushToken, "artifact-push-token", v.GetString(common.VInitArtifactPushToken), lang.CmdInitFlagArtifactPushToken) +// Run performs the execution of 'tools gen-key' sub-command. +func (o *GenKeyOptions) Run(cmd *cobra.Command, _ []string) error { + // Utility function to prompt the user for the password to the private key + passwordFunc := func(bool) ([]byte, error) { + // perform the first prompt + var password string + prompt := &survey.Password{ + Message: lang.CmdToolsGenKeyPrompt, + } + if err := survey.AskOne(prompt, &password); err != nil { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) + } + + // perform the second prompt + var doubleCheck string + rePrompt := &survey.Password{ + Message: lang.CmdToolsGenKeyPromptAgain, + } + if err := survey.AskOne(rePrompt, &doubleCheck); err != nil { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) + } + + // check if the passwords match + if password != doubleCheck { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrPasswordsNotMatch) + } + + return []byte(password), nil + } - updateCredsCmd.Flags().SortFlags = true + // Use cosign to generate the keypair + keyBytes, err := cosign.GenerateKeyPair(passwordFunc) + if err != nil { + return fmt.Errorf("unable to generate key pair: %w", err) + } - toolsCmd.AddCommand(clearCacheCmd) - clearCacheCmd.Flags().StringVar(&config.CommonOptions.CachePath, "zarf-cache", config.ZarfDefaultCachePath, lang.CmdToolsClearCacheFlagCachePath) + prvKeyFileName := "cosign.key" + pubKeyFileName := "cosign.pub" - toolsCmd.AddCommand(downloadInitCmd) - downloadInitCmd.Flags().StringVarP(&outputDirectory, "output-directory", "o", "", lang.CmdToolsDownloadInitFlagOutputDirectory) + // Check if we are about to overwrite existing key files + _, prvKeyExistsErr := os.Stat(prvKeyFileName) + _, pubKeyExistsErr := os.Stat(pubKeyFileName) + if prvKeyExistsErr == nil || pubKeyExistsErr == nil { + var confirm bool + confirmOverwritePrompt := &survey.Confirm{ + Message: fmt.Sprintf(lang.CmdToolsGenKeyPromptExists, prvKeyFileName), + } + err := survey.AskOne(confirmOverwritePrompt, &confirm) + if err != nil { + return err + } + if !confirm { + return errors.New("did not receive confirmation for overwriting key file(s)") + } + } + + // Write the key file contents to disk + if err := os.WriteFile(prvKeyFileName, keyBytes.PrivateBytes, helpers.ReadWriteUser); err != nil { + return err + } + if err := os.WriteFile(pubKeyFileName, keyBytes.PublicBytes, helpers.ReadAllWriteUser); err != nil { + return err + } - toolsCmd.AddCommand(generatePKICmd) - generatePKICmd.Flags().StringArrayVar(&subAltNames, "sub-alt-name", []string{}, lang.CmdToolsGenPkiFlagAltName) + message.Successf(lang.CmdToolsGenKeySuccess, prvKeyFileName, pubKeyFileName) + logger.From(cmd.Context()).Info("Successfully generated key pair", + "private-key-path", prvKeyExistsErr, + "public-key-path", pubKeyFileName) - toolsCmd.AddCommand(generateKeyCmd) + return nil }