From 23e5eef9e8a1bb555d168dd710147305dc15c712 Mon Sep 17 00:00:00 2001 From: nathan-nicholson Date: Tue, 31 Dec 2024 10:38:01 -0600 Subject: [PATCH] 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) + } + }) + } +}