Skip to content

Commit

Permalink
chore: extract options to struct among other things
Browse files Browse the repository at this point in the history
Signed-off-by: nathan-nicholson <[email protected]>
  • Loading branch information
nathan-nicholson committed Dec 31, 2024
1 parent dcb0deb commit 23e5eef
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 70 deletions.
63 changes: 44 additions & 19 deletions cmd/aws/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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
}
Expand Down
85 changes: 49 additions & 36 deletions cmd/aws/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand Down Expand Up @@ -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")

Expand Down
58 changes: 43 additions & 15 deletions internal/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Check failure on line 24 in internal/cluster/cluster.go

View workflow job for this annotation

GitHub Actions / build

var-naming: type HttpClient should be HTTPClient (revive)
Do(req *http.Request) (*http.Response, error)
}

return "https://console.kubefirst.dev"
type ClusterClient struct {

Check failure on line 28 in internal/cluster/cluster.go

View workflow job for this annotation

GitHub Actions / build

exported: type name will be used as cluster.ClusterClient by other packages, and that stutters; consider calling this Client (revive)
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),
Expand All @@ -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
}
Expand Down
92 changes: 92 additions & 0 deletions internal/cluster/cluster_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 23e5eef

Please sign in to comment.