From 691a656b8ac2ddab4584e40fc86464341dfc4f94 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Wed, 18 Dec 2024 15:58:47 -0600 Subject: [PATCH 01/11] chore: refactor CLI initialization Signed-off-by: nathan-nicholson --- cmd/info.go | 4 --- cmd/logs.go | 4 --- cmd/reset.go | 4 --- cmd/root.go | 68 ++++++++++++++++++++++++++------------------------ cmd/version.go | 4 --- 5 files changed, 36 insertions(+), 48 deletions(-) diff --git a/cmd/info.go b/cmd/info.go index f99d4ecfb..5495e7230 100755 --- a/cmd/info.go +++ b/cmd/info.go @@ -49,7 +49,3 @@ var infoCmd = &cobra.Command{ return nil }, } - -func init() { - rootCmd.AddCommand(infoCmd) -} diff --git a/cmd/logs.go b/cmd/logs.go index 426db0374..3500006e7 100755 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -44,7 +44,3 @@ var logsCmd = &cobra.Command{ return nil }, } - -func init() { - rootCmd.AddCommand(logsCmd) -} diff --git a/cmd/reset.go b/cmd/reset.go index 7264c2522..c41b29445 100755 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -76,10 +76,6 @@ var resetCmd = &cobra.Command{ }, } -func init() { - rootCmd.AddCommand(resetCmd) -} - // parseConfigEntryKubefirstChecks gathers the kubefirst-checks section of the Viper // config file and parses as a map[string]bool func parseConfigEntryKubefirstChecks(checks map[string]interface{}) (map[string]bool, error) { diff --git a/cmd/root.go b/cmd/root.go index 3a9b40591..f910a64c4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ package cmd import ( "fmt" + "os" "github.com/konstructio/kubefirst-api/pkg/configs" "github.com/konstructio/kubefirst-api/pkg/progressPrinter" @@ -20,43 +21,31 @@ import ( "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "kubefirst", - Short: "kubefirst management cluster installer base command", - Long: `kubefirst management cluster installer provisions an - open source application delivery platform in under an hour. - checkout the docs at https://kubefirst.konstruct.io/docs/.`, - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - // wire viper config for flags for all commands - return configs.InitializeViperConfig(cmd) - }, - Run: func(_ *cobra.Command, _ []string) { - fmt.Println("To learn more about kubefirst, run:") - fmt.Println(" kubefirst help") - progress.Progress.Quit() - }, -} - // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - // This will allow all child commands to have informUser available for free. - // Refers: https://github.com/konstructio/runtime/issues/525 - // Before removing next line, please read ticket above. - common.CheckForVersionUpdate() - progressPrinter.GetInstance() - if err := rootCmd.Execute(); err != nil { - fmt.Println("Error occurred during command execution:", err) - fmt.Println("If a detailed error message was available, please make the necessary corrections before retrying.") - fmt.Println("You can re-run the last command to try the operation again.") - progress.Progress.Quit() + // rootCmd represents the base command when called without any subcommands + rootCmd := &cobra.Command{ + Use: "kubefirst", + Short: "kubefirst management cluster installer base command", + Long: `kubefirst management cluster installer provisions an + open source application delivery platform in under an hour. + checkout the docs at https://kubefirst.konstruct.io/docs/.`, + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + // wire viper config for flags for all commands + return configs.InitializeViperConfig(cmd) + }, + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("To learn more about kubefirst, run:") + fmt.Println(" kubefirst help") + progress.Progress.Quit() + }, + SilenceUsage: true, + SilenceErrors: true, } -} -func init() { - cobra.OnInitialize() - rootCmd.SilenceUsage = true + printer := os.Stderr + rootCmd.AddCommand( betaCmd, aws.NewCommand(), @@ -67,5 +56,20 @@ func init() { LaunchCommand(), LetsEncryptCommand(), TerraformCommand(), + infoCmd, + versionCmd, + resetCmd, + logsCmd, ) + // This will allow all child commands to have informUser available for free. + // Refers: https://github.com/konstructio/runtime/issues/525 + // Before removing next line, please read ticket above. + common.CheckForVersionUpdate() + progressPrinter.GetInstance() + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(printer, "Error occurred during command execution: %v\n", err) + fmt.Fprintln(printer, "If a detailed error message was available, please make the necessary corrections before retrying.") + fmt.Fprintln(printer, "You can re-run the last command to try the operation again.") + os.Exit(1) + } } diff --git a/cmd/version.go b/cmd/version.go index f446bb0eb..b5c64c805 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -12,10 +12,6 @@ import ( "github.com/spf13/cobra" ) -func init() { - rootCmd.AddCommand(versionCmd) -} - var versionCmd = &cobra.Command{ Use: "version", Short: "print the version number for kubefirst-cli", From 048690cb7583b32bcccfc5dea198989991094d73 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Thu, 19 Dec 2024 11:19:47 -0600 Subject: [PATCH 02/11] chore: adding new Printer implementation Signed-off-by: nathan-nicholson --- cmd/aws/command.go | 26 ++++++++--- internal/common/printer.go | 31 +++++++++++++ internal/common/printer_test.go | 79 +++++++++++++++++++++++++++++++++ internal/logger/logger.go | 8 ++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 internal/common/printer.go create mode 100644 internal/common/printer_test.go create mode 100644 internal/logger/logger.go diff --git a/cmd/aws/command.go b/cmd/aws/command.go index f011c82a2..c4d54fdb2 100644 --- a/cmd/aws/command.go +++ b/cmd/aws/command.go @@ -8,10 +8,11 @@ package aws import ( "fmt" + "io" "github.com/konstructio/kubefirst-api/pkg/constants" "github.com/konstructio/kubefirst/internal/common" - "github.com/konstructio/kubefirst/internal/progress" + "github.com/konstructio/kubefirst/internal/logger" "github.com/spf13/cobra" ) @@ -44,6 +45,23 @@ var ( supportedGitProtocolOverride = []string{"https", "ssh"} ) +type Printer interface { + AddWriter(w io.Writer) + Print(s string) error +} + +type awsCommand struct { + logger logger.Logger + printer Printer +} + +func NewAwsCommand(logger logger.Logger, printer Printer) *awsCommand { + return &awsCommand{ + logger, + printer, + } +} + func NewCommand() *cobra.Command { awsCmd := &cobra.Command{ Use: "aws", @@ -51,11 +69,7 @@ func NewCommand() *cobra.Command { Long: "kubefirst aws", Run: func(_ *cobra.Command, _ []string) { fmt.Println("To learn more about aws in kubefirst, run:") - fmt.Println(" kubefirst help") - - if progress.Progress != nil { - progress.Progress.Quit() - } + fmt.Println(" kubefirst aws help") }, } diff --git a/internal/common/printer.go b/internal/common/printer.go new file mode 100644 index 000000000..c497050ed --- /dev/null +++ b/internal/common/printer.go @@ -0,0 +1,31 @@ +package common + +import ( + "fmt" + "io" +) + +type Printer struct { + writers []io.Writer +} + +func NewPrinter(writers ...io.Writer) *Printer { + return &Printer{ + writers: writers, + } +} + +func (p *Printer) AddWriter(w io.Writer) { + p.writers = append(p.writers, w) +} + +func (p *Printer) Print(s string) error { + for _, w := range p.writers { + _, err := fmt.Fprint(w, s) + if err != nil { + return fmt.Errorf("failed to write to writer: %w", err) + } + } + + return nil +} diff --git a/internal/common/printer_test.go b/internal/common/printer_test.go new file mode 100644 index 000000000..fe30f2b84 --- /dev/null +++ b/internal/common/printer_test.go @@ -0,0 +1,79 @@ +package common + +import ( + "bytes" + "io" + "testing" +) + +type FailingWriter struct{} + +func (f *FailingWriter) Write(p []byte) (n int, err error) { + return 0, io.ErrUnexpectedEOF +} + +func (f *FailingWriter) String() string { + return "" +} + +func TestAwsCommand_Print(t *testing.T) { + tests := []struct { + name string + input string + writers []io.Writer + wantErr bool + }{ + { + name: "write single string to single writer", + input: "test message", + writers: []io.Writer{&bytes.Buffer{}}, + wantErr: false, + }, + { + name: "write single string to multiple writers", + input: "test message", + writers: []io.Writer{&bytes.Buffer{}, &bytes.Buffer{}}, + wantErr: false, + }, + { + name: "write empty string", + input: "", + writers: []io.Writer{&bytes.Buffer{}}, + wantErr: false, + }, + { + name: "fail to write to writer", + input: "test message", + writers: []io.Writer{&FailingWriter{}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + printer := NewPrinter(tt.writers...) + + err := printer.Print(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("WriteString() error = %v, wantErr %v", err, tt.wantErr) + return + } + return + } + + for i, w := range tt.writers { + + stringer := w.(interface { + String() string + }) + + if got := stringer.String(); got != tt.input { + t.Errorf("Writer %d got = %v, want %v", i, got, tt.input) + } + } + }) + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 000000000..2be7127a9 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,8 @@ +package logger + +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) +} From c4816c25ec27f452f3338e2264626aa2fa15da68 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Thu, 19 Dec 2024 14:48:33 -0600 Subject: [PATCH 03/11] chore: set output for rootCmd Signed-off-by: nathan-nicholson --- cmd/root.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index f910a64c4..1fc1daff5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,7 +44,8 @@ func Execute() { SilenceErrors: true, } - printer := os.Stderr + rootCmd.SetErr(os.Stderr) + rootCmd.SetOut(os.Stdout) rootCmd.AddCommand( betaCmd, @@ -67,9 +68,9 @@ func Execute() { common.CheckForVersionUpdate() progressPrinter.GetInstance() if err := rootCmd.Execute(); err != nil { - fmt.Fprintf(printer, "Error occurred during command execution: %v\n", err) - fmt.Fprintln(printer, "If a detailed error message was available, please make the necessary corrections before retrying.") - fmt.Fprintln(printer, "You can re-run the last command to try the operation again.") + fmt.Printf("Error occurred during command execution: %v\n", err) + fmt.Println("If a detailed error message was available, please make the necessary corrections before retrying.") + fmt.Println("You can re-run the last command to try the operation again.") os.Exit(1) } } From af8f242df70851c3c0761737412f0ea596e9ae52 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Thu, 19 Dec 2024 19:49:18 -0600 Subject: [PATCH 04/11] chore: begin logging refactor * add logger interface * add error logging * add utility for making k1 directory that returns an error Signed-off-by: nathan-nicholson --- cmd/aws/command.go | 36 ++++++------------ cmd/aws/create.go | 50 +++++++++++++++++-------- cmd/aws/types.go | 12 ++++++ cmd/root.go | 6 +-- internal/common/logger.go | 8 ++++ internal/utilities/flags.go | 51 +++++++++++++------------ internal/utilities/utilities.go | 29 ++++++++++++-- internal/utilities/utilities_test.go | 56 ++++++++++++++++++++++++++++ main.go | 13 +++++-- 9 files changed, 185 insertions(+), 76 deletions(-) create mode 100644 cmd/aws/types.go create mode 100644 internal/common/logger.go create mode 100644 internal/utilities/utilities_test.go diff --git a/cmd/aws/command.go b/cmd/aws/command.go index c4d54fdb2..07314fca2 100644 --- a/cmd/aws/command.go +++ b/cmd/aws/command.go @@ -12,7 +12,6 @@ import ( "github.com/konstructio/kubefirst-api/pkg/constants" "github.com/konstructio/kubefirst/internal/common" - "github.com/konstructio/kubefirst/internal/logger" "github.com/spf13/cobra" ) @@ -45,25 +44,8 @@ var ( supportedGitProtocolOverride = []string{"https", "ssh"} ) -type Printer interface { - AddWriter(w io.Writer) - Print(s string) error -} - -type awsCommand struct { - logger logger.Logger - printer Printer -} - -func NewAwsCommand(logger logger.Logger, printer Printer) *awsCommand { - return &awsCommand{ - logger, - printer, - } -} - -func NewCommand() *cobra.Command { - awsCmd := &cobra.Command{ +func NewCommand(logger common.Logger, writer io.Writer) *cobra.Command { + cmd := &cobra.Command{ Use: "aws", Short: "kubefirst aws installation", Long: "kubefirst aws", @@ -73,19 +55,23 @@ func NewCommand() *cobra.Command { }, } + service := AwsService{ + logger, + writer, + } + // wire up new commands - awsCmd.AddCommand(Create(), Destroy(), Quota(), RootCredentials()) + cmd.AddCommand(Create(service), Destroy(), Quota(), RootCredentials()) - return awsCmd + return cmd } -func Create() *cobra.Command { +func Create(service AwsService) *cobra.Command { createCmd := &cobra.Command{ Use: "create", Short: "create the kubefirst platform running in aws", TraverseChildren: true, - RunE: createAws, - // PreRun: common.CheckDocker, + RunE: service.createAws, } awsDefaults := constants.GetCloudDefaults().Aws diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 80c194817..41c380903 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -27,47 +27,64 @@ import ( "github.com/spf13/viper" ) -func createAws(cmd *cobra.Command, _ []string) error { +func (s *AwsService) createAws(cmd *cobra.Command, _ []string) error { + fmt.Fprintln(s.writer, "Starting to create AWS cluster") + cliFlags, err := utilities.GetFlags(cmd, "aws") if err != nil { - progress.Error(err.Error()) - return nil + s.logger.Error("failed to get flags", "error", err) + return fmt.Errorf("failed to get flags: %w", err) } - progress.DisplayLogHints(40) + //TODO - Add progress steps + //progress.DisplayLogHints(40) isValid, catalogApps, err := catalog.ValidateCatalogApps(cliFlags.InstallCatalogApps) if !isValid { + s.logger.Error("invalid catalog apps", "error", err) return fmt.Errorf("invalid catalog apps: %w", err) } err = ValidateProvidedFlags(cliFlags.GitProvider) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to validate provided flags", "error", err) return fmt.Errorf("failed to validate provided flags: %w", err) } - utilities.CreateK1ClusterDirectory(cliFlags.ClusterName) + // Create k1 cluster directory + homePath, err := os.UserHomeDir() + + if err != nil { + s.logger.Error("failed to get user home directory", "error", err) + return fmt.Errorf("failed to get user home directory: %w", err) + } + + err = utilities.CreateK1ClusterDirectoryE(homePath, cliFlags.ClusterName) + + if err != nil { + s.logger.Error("failed to create k1 cluster directory", "error", err) + return fmt.Errorf("failed to create k1 cluster directory: %w", err) + } // If cluster setup is complete, return clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") if clusterSetupComplete { - err = fmt.Errorf("this cluster install process has already completed successfully") - progress.Error(err.Error()) + s.logger.Info("cluster install process has already completed successfully") + fmt.Fprintln(s.writer, "Cluster install process has already completed successfully") return nil } // Validate aws region config, err := awsinternal.NewAwsV2(cloudRegionFlag) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to validate AWS region", "error", err) return fmt.Errorf("failed to validate AWS region: %w", err) } awsClient := &awsinternal.Configuration{Config: config} creds, err := awsClient.Config.Credentials.Retrieve(aws.BackgroundContext()) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to retrieve AWS credentials", "error", err) return fmt.Errorf("failed to retrieve AWS credentials: %w", err) } @@ -75,18 +92,20 @@ func createAws(cmd *cobra.Command, _ []string) error { viper.Set("kubefirst.state-store-creds.secret-access-key-id", creds.SecretAccessKey) viper.Set("kubefirst.state-store-creds.token", creds.SessionToken) if err := viper.WriteConfig(); err != nil { + s.logger.Error("failed to write config", "error", err) + return fmt.Errorf("failed to write config: %w", err) } _, err = awsClient.CheckAvailabilityZones(cliFlags.CloudRegion) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to check availability zones", "error", err) return fmt.Errorf("failed to check availability zones: %w", err) } gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to validate Git credentials", "error", err) return fmt.Errorf("failed to validate Git credentials: %w", err) } @@ -105,13 +124,14 @@ func createAws(cmd *cobra.Command, _ []string) error { err = gitShim.InitializeGitProvider(&initGitParameters) if err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to initialize Git provider", "error", err) return fmt.Errorf("failed to initialize Git provider: %w", err) } } viper.Set(fmt.Sprintf("kubefirst-checks.%s-credentials", cliFlags.GitProvider), true) if err := viper.WriteConfig(); err != nil { + s.logger.Error("failed to write config", "error", err) return fmt.Errorf("failed to write config: %w", err) } @@ -124,12 +144,12 @@ func createAws(cmd *cobra.Command, _ []string) error { err = pkg.IsAppAvailable(fmt.Sprintf("%s/api/proxyHealth", cluster.GetConsoleIngressURL()), "kubefirst api") if err != nil { - progress.Error("unable to start kubefirst api") + s.logger.Error("failed to check kubefirst API availability", "error", err) return fmt.Errorf("failed to check kubefirst API availability: %w", err) } if err := provision.CreateMgmtCluster(gitAuth, cliFlags, catalogApps); err != nil { - progress.Error(err.Error()) + s.logger.Error("failed to create management cluster", "error", err) return fmt.Errorf("failed to create management cluster: %w", err) } diff --git a/cmd/aws/types.go b/cmd/aws/types.go new file mode 100644 index 000000000..677ab8b2e --- /dev/null +++ b/cmd/aws/types.go @@ -0,0 +1,12 @@ +package aws + +import ( + "io" + + "github.com/konstructio/kubefirst/internal/common" +) + +type AwsService struct { + logger common.Logger + writer io.Writer +} diff --git a/cmd/root.go b/cmd/root.go index 1fc1daff5..d06173f8c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,7 @@ import ( // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { +func Execute(logger common.Logger) { // rootCmd represents the base command when called without any subcommands rootCmd := &cobra.Command{ Use: "kubefirst", @@ -48,8 +48,7 @@ func Execute() { rootCmd.SetOut(os.Stdout) rootCmd.AddCommand( - betaCmd, - aws.NewCommand(), + aws.NewCommand(logger, os.Stderr), civo.NewCommand(), digitalocean.NewCommand(), k3d.NewCommand(), @@ -57,6 +56,7 @@ func Execute() { LaunchCommand(), LetsEncryptCommand(), TerraformCommand(), + betaCmd, infoCmd, versionCmd, resetCmd, diff --git a/internal/common/logger.go b/internal/common/logger.go new file mode 100644 index 000000000..c1bd2aac7 --- /dev/null +++ b/internal/common/logger.go @@ -0,0 +1,8 @@ +package common + +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) +} diff --git a/internal/utilities/flags.go b/internal/utilities/flags.go index c6ac1dd52..37b23f001 100644 --- a/internal/utilities/flags.go +++ b/internal/utilities/flags.go @@ -10,7 +10,6 @@ import ( "fmt" "strings" - "github.com/konstructio/kubefirst/internal/progress" "github.com/konstructio/kubefirst/internal/types" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -21,112 +20,112 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) alertsEmailFlag, err := cmd.Flags().GetString("alerts-email") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get alerts-email flag: %w", err) } cloudRegionFlag, err := cmd.Flags().GetString("cloud-region") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get cloud-region flag: %w", err) } clusterNameFlag, err := cmd.Flags().GetString("cluster-name") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get cluster-name flag: %w", err) } dnsProviderFlag, err := cmd.Flags().GetString("dns-provider") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get dns-provider flag: %w", err) } subdomainFlag, err := cmd.Flags().GetString("subdomain") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get subdomain flag: %w", err) } domainNameFlag, err := cmd.Flags().GetString("domain-name") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get domain-name flag: %w", err) } githubOrgFlag, err := cmd.Flags().GetString("github-org") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get github-org flag: %w", err) } githubOrgFlag = strings.ToLower(githubOrgFlag) gitlabGroupFlag, err := cmd.Flags().GetString("gitlab-group") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get gitlab-group flag: %w", err) } gitlabGroupFlag = strings.ToLower(gitlabGroupFlag) gitProviderFlag, err := cmd.Flags().GetString("git-provider") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get git-provider flag: %w", err) } gitProtocolFlag, err := cmd.Flags().GetString("git-protocol") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get git-protocol flag: %w", err) } gitopsTemplateURLFlag, err := cmd.Flags().GetString("gitops-template-url") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get gitops-template-url flag: %w", err) } gitopsTemplateBranchFlag, err := cmd.Flags().GetString("gitops-template-branch") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get gitops-template-branch flag: %w", err) } useTelemetryFlag, err := cmd.Flags().GetBool("use-telemetry") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get use-telemetry flag: %w", err) } nodeTypeFlag, err := cmd.Flags().GetString("node-type") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get node-type flag: %w", err) } installCatalogAppsFlag, err := cmd.Flags().GetString("install-catalog-apps") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get install-catalog-apps flag: %w", err) } nodeCountFlag, err := cmd.Flags().GetString("node-count") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get node-count flag: %w", err) } installKubefirstProFlag, err := cmd.Flags().GetBool("install-kubefirst-pro") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get install-kubefirst-pro flag: %w", err) } if cloudProvider == "aws" { ecrFlag, err := cmd.Flags().GetBool("ecr") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get ecr flag: %w", err) } @@ -136,7 +135,7 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "azure" { dnsAzureResourceGroup, err := cmd.Flags().GetString("dns-azure-resource-group") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get dns-azure-resource-group flag: %w", err) } cliFlags.DNSAzureRG = dnsAzureResourceGroup @@ -145,7 +144,7 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "google" { googleProject, err := cmd.Flags().GetString("google-project") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get google-project flag: %w", err) } @@ -155,35 +154,35 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "k3s" { k3sServersPrivateIps, err := cmd.Flags().GetStringSlice("servers-private-ips") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get servers-private-ips flag: %w", err) } cliFlags.K3sServersPrivateIPs = k3sServersPrivateIps k3sServersPublicIps, err := cmd.Flags().GetStringSlice("servers-public-ips") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get servers-public-ips flag: %w", err) } cliFlags.K3sServersPublicIPs = k3sServersPublicIps k3sSSHUserFlag, err := cmd.Flags().GetString("ssh-user") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get ssh-user flag: %w", err) } cliFlags.K3sSSHUser = k3sSSHUserFlag k3sSSHPrivateKeyFlag, err := cmd.Flags().GetString("ssh-privatekey") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get ssh-privatekey flag: %w", err) } cliFlags.K3sSSHPrivateKey = k3sSSHPrivateKeyFlag K3sServersArgsFlags, err := cmd.Flags().GetStringSlice("servers-args") if err != nil { - progress.Error(err.Error()) + return cliFlags, fmt.Errorf("failed to get servers-args flag: %w", err) } cliFlags.K3sServersArgs = K3sServersArgsFlags diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index 77b8c5137..b2d133222 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -35,13 +35,34 @@ func CreateK1ClusterDirectory(clusterName string) { return } + err = CreateK1ClusterDirectoryE(homePath, clusterName) + + if err != nil { + log.Info().Msg(err.Error()) + } + + log.Info().Msg("K1 cluster directory created") + +} + +func CreateK1ClusterDirectoryE(homePath, clusterName string) error { + k1Dir := fmt.Sprintf("%s/.k1/%s", homePath, clusterName) - if _, err := os.Stat(k1Dir); os.IsNotExist(err) { - err := os.MkdirAll(k1Dir, os.ModePerm) - if err != nil { - log.Info().Msgf("%q directory already exists, continuing", k1Dir) + + _, err := os.Stat(k1Dir) + + if err != nil { + if os.IsNotExist(err) { + err := os.MkdirAll(k1Dir, os.ModePerm) + if err != nil { + return fmt.Errorf("error creating directory: %w", err) + } + } else { + return fmt.Errorf("error checking directory: %w", err) } } + + return nil } func CreateClusterRecordFromRaw( diff --git a/internal/utilities/utilities_test.go b/internal/utilities/utilities_test.go new file mode 100644 index 000000000..f51e23892 --- /dev/null +++ b/internal/utilities/utilities_test.go @@ -0,0 +1,56 @@ +package utilities + +import ( + "fmt" + "os" + "testing" +) + +func TestCreateK1ClusterDirectoryE(t *testing.T) { + tests := []struct { + name string + homePath string + clusterName string + wantOk bool + wantErr bool + }{ + { + name: "successfully creates new directory", + homePath: t.TempDir(), + clusterName: "test-cluster", + wantOk: true, + wantErr: false, + }, + { + name: "empty cluster name", + homePath: t.TempDir(), + clusterName: "", + wantOk: true, + wantErr: false, + }, + { + name: "invalid home path", + homePath: "/nonexistent/path", + clusterName: "test-cluster", + wantOk: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := CreateK1ClusterDirectoryE(tt.homePath, tt.clusterName) + if (err != nil) != tt.wantErr { + t.Errorf("CreateK1ClusterDirectoryE() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + expectedPath := fmt.Sprintf("%s/.k1/%s", tt.homePath, tt.clusterName) + if _, err := os.Stat(expectedPath); os.IsNotExist(err) { + t.Errorf("Directory was not created at %s", expectedPath) + } + } + }) + } +} diff --git a/main.go b/main.go index e030707ed..04be31ccc 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ package main import ( "fmt" stdLog "log" + "log/slog" "os" "time" @@ -117,11 +118,17 @@ func main() { }(logFileObj) // setup default logging - // this Go standard log is active to keep compatibility with current code base stdLog.SetOutput(logFileObj) stdLog.SetPrefix("LOG: ") stdLog.SetFlags(stdLog.Ldate) + // Configure slog to write to the logfile + slogHandler := slog.NewTextHandler(logFileObj, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }) + + logger := slog.New(slogHandler) + log.Logger = zeroLog.New(logFileObj).With().Timestamp().Logger() viper.Set("k1-paths.logs-dir", logsFolder) @@ -137,11 +144,11 @@ func main() { progress.InitializeProgressTerminal() go func() { - cmd.Execute() + cmd.Execute(logger) }() progress.Progress.Run() } else { - cmd.Execute() + cmd.Execute(logger) } } From 9089cbcdeb419f88318455b21de6d94ac3ada88c Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:07:16 -0600 Subject: [PATCH 05/11] chore: remove empty space in conditionals Signed-off-by: nathan-nicholson --- internal/utilities/flags.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/internal/utilities/flags.go b/internal/utilities/flags.go index 37b23f001..7f4b29eb5 100644 --- a/internal/utilities/flags.go +++ b/internal/utilities/flags.go @@ -20,112 +20,94 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) alertsEmailFlag, err := cmd.Flags().GetString("alerts-email") if err != nil { - return cliFlags, fmt.Errorf("failed to get alerts-email flag: %w", err) } cloudRegionFlag, err := cmd.Flags().GetString("cloud-region") if err != nil { - return cliFlags, fmt.Errorf("failed to get cloud-region flag: %w", err) } clusterNameFlag, err := cmd.Flags().GetString("cluster-name") if err != nil { - return cliFlags, fmt.Errorf("failed to get cluster-name flag: %w", err) } dnsProviderFlag, err := cmd.Flags().GetString("dns-provider") if err != nil { - return cliFlags, fmt.Errorf("failed to get dns-provider flag: %w", err) } subdomainFlag, err := cmd.Flags().GetString("subdomain") if err != nil { - return cliFlags, fmt.Errorf("failed to get subdomain flag: %w", err) } domainNameFlag, err := cmd.Flags().GetString("domain-name") if err != nil { - return cliFlags, fmt.Errorf("failed to get domain-name flag: %w", err) } githubOrgFlag, err := cmd.Flags().GetString("github-org") if err != nil { - return cliFlags, fmt.Errorf("failed to get github-org flag: %w", err) } githubOrgFlag = strings.ToLower(githubOrgFlag) gitlabGroupFlag, err := cmd.Flags().GetString("gitlab-group") if err != nil { - return cliFlags, fmt.Errorf("failed to get gitlab-group flag: %w", err) } gitlabGroupFlag = strings.ToLower(gitlabGroupFlag) gitProviderFlag, err := cmd.Flags().GetString("git-provider") if err != nil { - return cliFlags, fmt.Errorf("failed to get git-provider flag: %w", err) } gitProtocolFlag, err := cmd.Flags().GetString("git-protocol") if err != nil { - return cliFlags, fmt.Errorf("failed to get git-protocol flag: %w", err) } gitopsTemplateURLFlag, err := cmd.Flags().GetString("gitops-template-url") if err != nil { - return cliFlags, fmt.Errorf("failed to get gitops-template-url flag: %w", err) } gitopsTemplateBranchFlag, err := cmd.Flags().GetString("gitops-template-branch") if err != nil { - return cliFlags, fmt.Errorf("failed to get gitops-template-branch flag: %w", err) } useTelemetryFlag, err := cmd.Flags().GetBool("use-telemetry") if err != nil { - return cliFlags, fmt.Errorf("failed to get use-telemetry flag: %w", err) } nodeTypeFlag, err := cmd.Flags().GetString("node-type") if err != nil { - return cliFlags, fmt.Errorf("failed to get node-type flag: %w", err) } installCatalogAppsFlag, err := cmd.Flags().GetString("install-catalog-apps") if err != nil { - return cliFlags, fmt.Errorf("failed to get install-catalog-apps flag: %w", err) } nodeCountFlag, err := cmd.Flags().GetString("node-count") if err != nil { - return cliFlags, fmt.Errorf("failed to get node-count flag: %w", err) } installKubefirstProFlag, err := cmd.Flags().GetBool("install-kubefirst-pro") if err != nil { - return cliFlags, fmt.Errorf("failed to get install-kubefirst-pro flag: %w", err) } if cloudProvider == "aws" { ecrFlag, err := cmd.Flags().GetBool("ecr") if err != nil { - return cliFlags, fmt.Errorf("failed to get ecr flag: %w", err) } @@ -135,7 +117,6 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "azure" { dnsAzureResourceGroup, err := cmd.Flags().GetString("dns-azure-resource-group") if err != nil { - return cliFlags, fmt.Errorf("failed to get dns-azure-resource-group flag: %w", err) } cliFlags.DNSAzureRG = dnsAzureResourceGroup @@ -144,7 +125,6 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "google" { googleProject, err := cmd.Flags().GetString("google-project") if err != nil { - return cliFlags, fmt.Errorf("failed to get google-project flag: %w", err) } @@ -154,35 +134,30 @@ func GetFlags(cmd *cobra.Command, cloudProvider string) (types.CliFlags, error) if cloudProvider == "k3s" { k3sServersPrivateIps, err := cmd.Flags().GetStringSlice("servers-private-ips") if err != nil { - return cliFlags, fmt.Errorf("failed to get servers-private-ips flag: %w", err) } cliFlags.K3sServersPrivateIPs = k3sServersPrivateIps k3sServersPublicIps, err := cmd.Flags().GetStringSlice("servers-public-ips") if err != nil { - return cliFlags, fmt.Errorf("failed to get servers-public-ips flag: %w", err) } cliFlags.K3sServersPublicIPs = k3sServersPublicIps k3sSSHUserFlag, err := cmd.Flags().GetString("ssh-user") if err != nil { - return cliFlags, fmt.Errorf("failed to get ssh-user flag: %w", err) } cliFlags.K3sSSHUser = k3sSSHUserFlag k3sSSHPrivateKeyFlag, err := cmd.Flags().GetString("ssh-privatekey") if err != nil { - return cliFlags, fmt.Errorf("failed to get ssh-privatekey flag: %w", err) } cliFlags.K3sSSHPrivateKey = k3sSSHPrivateKeyFlag K3sServersArgsFlags, err := cmd.Flags().GetStringSlice("servers-args") if err != nil { - return cliFlags, fmt.Errorf("failed to get servers-args flag: %w", err) } cliFlags.K3sServersArgs = K3sServersArgsFlags From 9bc31f859bec824884de2521d9549d85b0f28ca2 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:14:57 -0600 Subject: [PATCH 06/11] chore: add space to comment Signed-off-by: nathan-nicholson --- cmd/aws/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 41c380903..9e6e96a25 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -36,7 +36,7 @@ func (s *AwsService) createAws(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to get flags: %w", err) } - //TODO - Add progress steps + // TODO - Add progress steps //progress.DisplayLogHints(40) isValid, catalogApps, err := catalog.ValidateCatalogApps(cliFlags.InstallCatalogApps) From 25847615e32928dbce8eb6dfa8f7253b9f41280e Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:23:31 -0600 Subject: [PATCH 07/11] chore: rename AwsService to service Signed-off-by: nathan-nicholson --- cmd/aws/command.go | 4 ++-- cmd/aws/create.go | 2 +- cmd/aws/types.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/aws/command.go b/cmd/aws/command.go index 07314fca2..adc69baa8 100644 --- a/cmd/aws/command.go +++ b/cmd/aws/command.go @@ -55,7 +55,7 @@ func NewCommand(logger common.Logger, writer io.Writer) *cobra.Command { }, } - service := AwsService{ + service := Service{ logger, writer, } @@ -66,7 +66,7 @@ func NewCommand(logger common.Logger, writer io.Writer) *cobra.Command { return cmd } -func Create(service AwsService) *cobra.Command { +func Create(service Service) *cobra.Command { createCmd := &cobra.Command{ Use: "create", Short: "create the kubefirst platform running in aws", diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 9e6e96a25..859a1dcbf 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -27,7 +27,7 @@ import ( "github.com/spf13/viper" ) -func (s *AwsService) createAws(cmd *cobra.Command, _ []string) error { +func (s *Service) createAws(cmd *cobra.Command, _ []string) error { fmt.Fprintln(s.writer, "Starting to create AWS cluster") cliFlags, err := utilities.GetFlags(cmd, "aws") diff --git a/cmd/aws/types.go b/cmd/aws/types.go index 677ab8b2e..dc51ce6df 100644 --- a/cmd/aws/types.go +++ b/cmd/aws/types.go @@ -6,7 +6,7 @@ import ( "github.com/konstructio/kubefirst/internal/common" ) -type AwsService struct { +type Service struct { logger common.Logger writer io.Writer } From ed42c4f7b53c40908df0dfae7fe4cf10d172b00c Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:28:35 -0600 Subject: [PATCH 08/11] chore: more lint fixes Signed-off-by: nathan-nicholson --- cmd/aws/create.go | 2 +- internal/utilities/utilities.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 859a1dcbf..be018efa9 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -37,7 +37,7 @@ func (s *Service) createAws(cmd *cobra.Command, _ []string) error { } // TODO - Add progress steps - //progress.DisplayLogHints(40) + // progress.DisplayLogHints(40) isValid, catalogApps, err := catalog.ValidateCatalogApps(cliFlags.InstallCatalogApps) if !isValid { diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index b2d133222..aecfd968d 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -42,11 +42,9 @@ func CreateK1ClusterDirectory(clusterName string) { } log.Info().Msg("K1 cluster directory created") - } func CreateK1ClusterDirectoryE(homePath, clusterName string) error { - k1Dir := fmt.Sprintf("%s/.k1/%s", homePath, clusterName) _, err := os.Stat(k1Dir) From 541d8f4628f1eba97e311b5adb3fcfc763bc3888 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:33:44 -0600 Subject: [PATCH 09/11] chore: ran gofumpt Signed-off-by: nathan-nicholson --- cmd/aws/create.go | 2 -- internal/common/printer_test.go | 1 - internal/utilities/utilities.go | 2 -- 3 files changed, 5 deletions(-) diff --git a/cmd/aws/create.go b/cmd/aws/create.go index be018efa9..7c11d571f 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -53,14 +53,12 @@ func (s *Service) createAws(cmd *cobra.Command, _ []string) error { // Create k1 cluster directory homePath, err := os.UserHomeDir() - if err != nil { s.logger.Error("failed to get user home directory", "error", err) return fmt.Errorf("failed to get user home directory: %w", err) } err = utilities.CreateK1ClusterDirectoryE(homePath, cliFlags.ClusterName) - if err != nil { s.logger.Error("failed to create k1 cluster directory", "error", err) return fmt.Errorf("failed to create k1 cluster directory: %w", err) diff --git a/internal/common/printer_test.go b/internal/common/printer_test.go index fe30f2b84..ec7391dba 100644 --- a/internal/common/printer_test.go +++ b/internal/common/printer_test.go @@ -51,7 +51,6 @@ func TestAwsCommand_Print(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - printer := NewPrinter(tt.writers...) err := printer.Print(tt.input) diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index aecfd968d..c4afc8cfe 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -36,7 +36,6 @@ func CreateK1ClusterDirectory(clusterName string) { } err = CreateK1ClusterDirectoryE(homePath, clusterName) - if err != nil { log.Info().Msg(err.Error()) } @@ -48,7 +47,6 @@ func CreateK1ClusterDirectoryE(homePath, clusterName string) error { k1Dir := fmt.Sprintf("%s/.k1/%s", homePath, clusterName) _, err := os.Stat(k1Dir) - if err != nil { if os.IsNotExist(err) { err := os.MkdirAll(k1Dir, os.ModePerm) From dcb0deb9a50f212ada1c8d8c729da440a3556df4 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Mon, 30 Dec 2024 09:37:46 -0600 Subject: [PATCH 10/11] chore: delete unused printer struct Signed-off-by: nathan-nicholson --- internal/common/printer.go | 31 ------------- internal/common/printer_test.go | 78 --------------------------------- 2 files changed, 109 deletions(-) delete mode 100644 internal/common/printer.go delete mode 100644 internal/common/printer_test.go diff --git a/internal/common/printer.go b/internal/common/printer.go deleted file mode 100644 index c497050ed..000000000 --- a/internal/common/printer.go +++ /dev/null @@ -1,31 +0,0 @@ -package common - -import ( - "fmt" - "io" -) - -type Printer struct { - writers []io.Writer -} - -func NewPrinter(writers ...io.Writer) *Printer { - return &Printer{ - writers: writers, - } -} - -func (p *Printer) AddWriter(w io.Writer) { - p.writers = append(p.writers, w) -} - -func (p *Printer) Print(s string) error { - for _, w := range p.writers { - _, err := fmt.Fprint(w, s) - if err != nil { - return fmt.Errorf("failed to write to writer: %w", err) - } - } - - return nil -} diff --git a/internal/common/printer_test.go b/internal/common/printer_test.go deleted file mode 100644 index ec7391dba..000000000 --- a/internal/common/printer_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package common - -import ( - "bytes" - "io" - "testing" -) - -type FailingWriter struct{} - -func (f *FailingWriter) Write(p []byte) (n int, err error) { - return 0, io.ErrUnexpectedEOF -} - -func (f *FailingWriter) String() string { - return "" -} - -func TestAwsCommand_Print(t *testing.T) { - tests := []struct { - name string - input string - writers []io.Writer - wantErr bool - }{ - { - name: "write single string to single writer", - input: "test message", - writers: []io.Writer{&bytes.Buffer{}}, - wantErr: false, - }, - { - name: "write single string to multiple writers", - input: "test message", - writers: []io.Writer{&bytes.Buffer{}, &bytes.Buffer{}}, - wantErr: false, - }, - { - name: "write empty string", - input: "", - writers: []io.Writer{&bytes.Buffer{}}, - wantErr: false, - }, - { - name: "fail to write to writer", - input: "test message", - writers: []io.Writer{&FailingWriter{}}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - printer := NewPrinter(tt.writers...) - - err := printer.Print(tt.input) - - if tt.wantErr { - if err == nil { - t.Errorf("WriteString() error = %v, wantErr %v", err, tt.wantErr) - return - } - return - } - - for i, w := range tt.writers { - - stringer := w.(interface { - String() string - }) - - if got := stringer.String(); got != tt.input { - t.Errorf("Writer %d got = %v, want %v", i, got, tt.input) - } - } - }) - } -} From 23e5eef9e8a1bb555d168dd710147305dc15c712 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Tue, 31 Dec 2024 10:38:01 -0600 Subject: [PATCH 11/11] chore: extract options to struct among other things Signed-off-by: nathan-nicholson --- cmd/aws/command.go | 63 +++++++++++++++------- cmd/aws/create.go | 85 ++++++++++++++++------------- internal/cluster/cluster.go | 58 ++++++++++++++------ internal/cluster/cluster_test.go | 92 ++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 70 deletions(-) create mode 100644 internal/cluster/cluster_test.go diff --git a/cmd/aws/command.go b/cmd/aws/command.go index adc69baa8..c29da217f 100644 --- a/cmd/aws/command.go +++ b/cmd/aws/command.go @@ -44,6 +44,29 @@ var ( supportedGitProtocolOverride = []string{"https", "ssh"} ) +type options struct { + AlertsEmail string + CI bool + CloudRegion string + ClusterName string + ClusterType string + DNSProvider string + GitHubOrg string + GitLabGroup string + GitProvider string + GitProtocol string + GitopsTemplateURL string + GitopsTemplateBranch string + DomainName string + SubdomainName string + UseTelemetry bool + ECR bool + NodeType string + NodeCount string + InstallCatalogApps string + InstallKubefirstPro bool +} + func NewCommand(logger common.Logger, writer io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "aws", @@ -67,6 +90,8 @@ func NewCommand(logger common.Logger, writer io.Writer) *cobra.Command { } func Create(service Service) *cobra.Command { + var opts options + createCmd := &cobra.Command{ Use: "create", Short: "create the kubefirst platform running in aws", @@ -77,28 +102,28 @@ func Create(service Service) *cobra.Command { awsDefaults := constants.GetCloudDefaults().Aws // todo review defaults and update descriptions - createCmd.Flags().StringVar(&alertsEmailFlag, "alerts-email", "", "email address for let's encrypt certificate notifications (required)") + createCmd.Flags().StringVar(&opts.AlertsEmail, "alerts-email", "", "email address for let's encrypt certificate notifications (required)") createCmd.MarkFlagRequired("alerts-email") createCmd.Flags().BoolVar(&ciFlag, "ci", false, "if running kubefirst in ci, set this flag to disable interactive features") - createCmd.Flags().StringVar(&cloudRegionFlag, "cloud-region", "us-east-1", "the aws region to provision infrastructure in") - createCmd.Flags().StringVar(&clusterNameFlag, "cluster-name", "kubefirst", "the name of the cluster to create") - createCmd.Flags().StringVar(&clusterTypeFlag, "cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") - createCmd.Flags().StringVar(&nodeCountFlag, "node-count", awsDefaults.NodeCount, "the node count for the cluster") - createCmd.Flags().StringVar(&nodeTypeFlag, "node-type", awsDefaults.InstanceSize, "the instance size of the cluster to create") - createCmd.Flags().StringVar(&dnsProviderFlag, "dns-provider", "aws", fmt.Sprintf("the dns provider - one of: %q", supportedDNSProviders)) - createCmd.Flags().StringVar(&subdomainNameFlag, "subdomain", "", "the subdomain to use for DNS records (Cloudflare)") - createCmd.Flags().StringVar(&domainNameFlag, "domain-name", "", "the Route53/Cloudflare hosted zone name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") + createCmd.Flags().StringVar(&opts.CloudRegion, "cloud-region", "us-east-1", "the aws region to provision infrastructure in") + createCmd.Flags().StringVar(&opts.ClusterName, "cluster-name", "kubefirst", "the name of the cluster to create") + createCmd.Flags().StringVar(&opts.ClusterType, "cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") + createCmd.Flags().StringVar(&opts.NodeCount, "node-count", awsDefaults.NodeCount, "the node count for the cluster") + createCmd.Flags().StringVar(&opts.NodeType, "node-type", awsDefaults.InstanceSize, "the instance size of the cluster to create") + createCmd.Flags().StringVar(&opts.DNSProvider, "dns-provider", "aws", fmt.Sprintf("the dns provider - one of: %q", supportedDNSProviders)) + createCmd.Flags().StringVar(&opts.SubdomainName, "subdomain", "", "the subdomain to use for DNS records (Cloudflare)") + createCmd.Flags().StringVar(&opts.DomainName, "domain-name", "", "the Route53/Cloudflare hosted zone name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") createCmd.MarkFlagRequired("domain-name") - createCmd.Flags().StringVar(&gitProviderFlag, "git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) - createCmd.Flags().StringVar(&gitProtocolFlag, "git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) - createCmd.Flags().StringVar(&githubOrgFlag, "github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") - createCmd.Flags().StringVar(&gitlabGroupFlag, "gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") - createCmd.Flags().StringVar(&gitopsTemplateBranchFlag, "gitops-template-branch", "", "the branch to clone for the gitops-template repository") - createCmd.Flags().StringVar(&gitopsTemplateURLFlag, "gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") - createCmd.Flags().StringVar(&installCatalogApps, "install-catalog-apps", "", "comma separated values to install after provision") - createCmd.Flags().BoolVar(&useTelemetryFlag, "use-telemetry", true, "whether to emit telemetry") - createCmd.Flags().BoolVar(&ecrFlag, "ecr", false, "whether or not to use ecr vs the git provider") - createCmd.Flags().BoolVar(&installKubefirstProFlag, "install-kubefirst-pro", true, "whether or not to install kubefirst pro") + createCmd.Flags().StringVar(&opts.GitProvider, "git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) + createCmd.Flags().StringVar(&opts.GitProtocol, "git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) + createCmd.Flags().StringVar(&opts.GitHubOrg, "github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") + createCmd.Flags().StringVar(&opts.GitLabGroup, "gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") + createCmd.Flags().StringVar(&opts.GitopsTemplateBranch, "gitops-template-branch", "", "the branch to clone for the gitops-template repository") + createCmd.Flags().StringVar(&opts.GitopsTemplateURL, "gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") + createCmd.Flags().StringVar(&opts.InstallCatalogApps, "install-catalog-apps", "", "comma separated values to install after provision") + createCmd.Flags().BoolVar(&opts.UseTelemetry, "use-telemetry", true, "whether to emit telemetry") + createCmd.Flags().BoolVar(&opts.ECR, "ecr", false, "whether or not to use ecr vs the git provider") + createCmd.Flags().BoolVar(&opts.InstallKubefirstPro, "install-kubefirst-pro", true, "whether or not to install kubefirst pro") return createCmd } diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 7c11d571f..134af47d2 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -21,21 +21,14 @@ import ( "github.com/konstructio/kubefirst/internal/launch" "github.com/konstructio/kubefirst/internal/progress" "github.com/konstructio/kubefirst/internal/provision" + "github.com/konstructio/kubefirst/internal/types" "github.com/konstructio/kubefirst/internal/utilities" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) -func (s *Service) createAws(cmd *cobra.Command, _ []string) error { - fmt.Fprintln(s.writer, "Starting to create AWS cluster") - - cliFlags, err := utilities.GetFlags(cmd, "aws") - if err != nil { - s.logger.Error("failed to get flags", "error", err) - return fmt.Errorf("failed to get flags: %w", err) - } - +func (s *Service) createAwsCluster(cliFlags types.CliFlags) error { // TODO - Add progress steps // progress.DisplayLogHints(40) @@ -45,33 +38,6 @@ func (s *Service) createAws(cmd *cobra.Command, _ []string) error { return fmt.Errorf("invalid catalog apps: %w", err) } - err = ValidateProvidedFlags(cliFlags.GitProvider) - if err != nil { - s.logger.Error("failed to validate provided flags", "error", err) - return fmt.Errorf("failed to validate provided flags: %w", err) - } - - // Create k1 cluster directory - homePath, err := os.UserHomeDir() - if err != nil { - s.logger.Error("failed to get user home directory", "error", err) - return fmt.Errorf("failed to get user home directory: %w", err) - } - - err = utilities.CreateK1ClusterDirectoryE(homePath, cliFlags.ClusterName) - if err != nil { - s.logger.Error("failed to create k1 cluster directory", "error", err) - return fmt.Errorf("failed to create k1 cluster directory: %w", err) - } - - // If cluster setup is complete, return - clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") - if clusterSetupComplete { - s.logger.Info("cluster install process has already completed successfully") - fmt.Fprintln(s.writer, "Cluster install process has already completed successfully") - return nil - } - // Validate aws region config, err := awsinternal.NewAwsV2(cloudRegionFlag) if err != nil { @@ -154,6 +120,53 @@ func (s *Service) createAws(cmd *cobra.Command, _ []string) error { return nil } +func (s *Service) createAws(cmd *cobra.Command, _ []string) error { + fmt.Fprintln(s.writer, "Starting to create AWS cluster") + + cliFlags, err := utilities.GetFlags(cmd, "aws") + if err != nil { + s.logger.Error("failed to get flags", "error", err) + return fmt.Errorf("failed to get flags: %w", err) + } + + err = ValidateProvidedFlags(cliFlags.GitProvider) + if err != nil { + s.logger.Error("failed to validate provided flags", "error", err) + return fmt.Errorf("failed to validate provided flags: %w", err) + } + + // Create k1 cluster directory + homePath, err := os.UserHomeDir() + if err != nil { + s.logger.Error("failed to get user home directory", "error", err) + return fmt.Errorf("failed to get user home directory: %w", err) + } + + err = utilities.CreateK1ClusterDirectoryE(homePath, cliFlags.ClusterName) + if err != nil { + s.logger.Error("failed to create k1 cluster directory", "error", err) + return fmt.Errorf("failed to create k1 cluster directory: %w", err) + } + + // If cluster setup is complete, return + clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") + if clusterSetupComplete { + s.logger.Info("cluster install process has already completed successfully") + fmt.Fprintln(s.writer, "Cluster install process has already completed successfully") + return nil + } + + err = s.createAwsCluster(cliFlags) + if err != nil { + s.logger.Error("failed to create AWS cluster", "error", err) + return fmt.Errorf("failed to create AWS cluster: %w", err) + } + + fmt.Fprintln(s.writer, "AWS cluster creation complete") + + return nil +} + func ValidateProvidedFlags(gitProvider string) error { progress.AddStep("Validate provided flags") diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go index 02e2603da..e201bfe9e 100644 --- a/internal/cluster/cluster.go +++ b/internal/cluster/cluster.go @@ -21,18 +21,27 @@ import ( "github.com/konstructio/kubefirst/internal/types" ) -func GetConsoleIngressURL() string { - if strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" { // allow using local console running on port 3000 - return os.Getenv("K1_CONSOLE_REMOTE_URL") - } +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} - return "https://console.kubefirst.dev" +type ClusterClient struct { + hostURL string + httpClient HttpClient } -func CreateCluster(cluster apiTypes.ClusterDefinition) error { - customTransport := http.DefaultTransport.(*http.Transport).Clone() - httpClient := http.Client{Transport: customTransport} +func (c *ClusterClient) getProxyURL() string { + return fmt.Sprintf("%s/api/proxy", c.hostURL) +} + +func NewClusterClient(hostURL string, client HttpClient) *ClusterClient { + return &ClusterClient{ + hostURL: hostURL, + httpClient: client, + } +} +func (c *ClusterClient) CreateCluster(cluster apiTypes.ClusterDefinition) error { requestObject := types.ProxyCreateClusterRequest{ Body: cluster, URL: fmt.Sprintf("/cluster/%s", cluster.ClusterName), @@ -43,33 +52,52 @@ func CreateCluster(cluster apiTypes.ClusterDefinition) error { return fmt.Errorf("failed to marshal request object: %w", err) } - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/proxy", GetConsoleIngressURL()), bytes.NewReader(payload)) + req, err := http.NewRequest(http.MethodPost, c.getProxyURL(), bytes.NewReader(payload)) if err != nil { - log.Printf("error creating request: %s", err) return fmt.Errorf("failed to create request: %w", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") - res, err := httpClient.Do(req) + res, err := c.httpClient.Do(req) if err != nil { - log.Printf("error executing request: %s", err) return fmt.Errorf("failed to execute request: %w", err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { - log.Printf("unable to create cluster: %v", err) return fmt.Errorf("failed to read response body: %w", err) } if res.StatusCode != http.StatusAccepted { - log.Printf("unable to create cluster: %q %q", res.Status, body) return fmt.Errorf("unable to create cluster: API returned unexpected status code %q: %s", res.Status, body) } - log.Printf("Created cluster: %q", string(body)) + return nil +} + +func GetConsoleIngressURL() string { + if strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" { // allow using local console running on port 3000 + return os.Getenv("K1_CONSOLE_REMOTE_URL") + } + + return "https://console.kubefirst.dev" +} + +func CreateCluster(cluster apiTypes.ClusterDefinition) error { + customTransport := http.DefaultTransport.(*http.Transport).Clone() + httpClient := http.Client{Transport: customTransport} + + clusterClient := NewClusterClient(GetConsoleIngressURL(), &httpClient) + + err := clusterClient.CreateCluster(cluster) + if err != nil { + log.Printf("error creating cluster: %v", err) + return fmt.Errorf("failed to create cluster: %w", err) + } + + log.Printf("successfully initiated cluster creation: %q", cluster.ClusterName) return nil } diff --git a/internal/cluster/cluster_test.go b/internal/cluster/cluster_test.go new file mode 100644 index 000000000..84cd52717 --- /dev/null +++ b/internal/cluster/cluster_test.go @@ -0,0 +1,92 @@ +package cluster + +import ( + "bytes" + "io" + "net/http" + "strings" + "testing" + + apiTypes "github.com/konstructio/kubefirst-api/pkg/types" +) + +type mockHTTPClient struct { + DoFunc func(req *http.Request) (*http.Response, error) +} + +func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { + return m.DoFunc(req) +} + +func TestClusterClient_CreateCluster(t *testing.T) { + tests := []struct { + name string + cluster apiTypes.ClusterDefinition + mockResp *http.Response + mockErr error + wantErr bool + errContains string + }{ + { + name: "successful cluster creation", + cluster: apiTypes.ClusterDefinition{ + ClusterName: "test-cluster", + }, + mockResp: &http.Response{ + StatusCode: http.StatusAccepted, + Body: io.NopCloser(bytes.NewReader([]byte(`{"status":"accepted"}`))), + }, + wantErr: false, + }, + { + name: "error - unexpected status code", + cluster: apiTypes.ClusterDefinition{ + ClusterName: "test-cluster", + }, + mockResp: &http.Response{ + StatusCode: http.StatusBadRequest, + Status: "400 Bad Request", + Body: io.NopCloser(bytes.NewReader([]byte(`{"error":"bad request"}`))), + }, + wantErr: true, + errContains: "unexpected status code", + }, + { + name: "error - failed to make request", + cluster: apiTypes.ClusterDefinition{ + ClusterName: "test-cluster", + }, + mockErr: http.ErrHandlerTimeout, + wantErr: true, + errContains: "failed to execute request", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mockHTTPClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + if tt.mockErr != nil { + return nil, tt.mockErr + } + return tt.mockResp, nil + }, + } + + c := &ClusterClient{ + hostURL: "http://test.local", + httpClient: mockClient, + } + + err := c.CreateCluster(tt.cluster) + if (err != nil) != tt.wantErr { + t.Errorf("CreateCluster() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("CreateCluster() error = %v, should contain %v", err, tt.errContains) + } + }) + } +}