diff --git a/.gitignore b/.gitignore index 9cbec5349..aa55b0591 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ kubefirst.yaml __debug_* kubefirst launch.json +.envrc \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..e5cbebf43 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +default: help + +.PHONY: help +help: ## list makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +PHONY: fmt +fmt: ## format go files + gofumpt -w . + gci write . \ No newline at end of file diff --git a/cmd/akamai/create.go b/cmd/akamai/create.go index 6201effa7..9e218de8e 100644 --- a/cmd/akamai/create.go +++ b/cmd/akamai/create.go @@ -10,7 +10,6 @@ import ( "fmt" "os" - "github.com/rs/zerolog/log" "github.com/kubefirst/kubefirst/internal/catalog" "github.com/kubefirst/kubefirst/internal/cluster" "github.com/kubefirst/kubefirst/internal/gitShim" @@ -20,6 +19,7 @@ import ( "github.com/kubefirst/kubefirst/internal/utilities" "github.com/kubefirst/runtime/pkg" internalssh "github.com/kubefirst/runtime/pkg/ssh" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -49,7 +49,6 @@ func createAkamai(cmd *cobra.Command, args []string) error { utilities.CreateK1ClusterDirectory(clusterNameFlag) gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) - if err != nil { progress.Error(err.Error()) return nil diff --git a/cmd/aws/command.go b/cmd/aws/command.go index 6a384085b..66caca3c5 100644 --- a/cmd/aws/command.go +++ b/cmd/aws/command.go @@ -116,7 +116,6 @@ func Destroy() *cobra.Command { } return destroyCmd - } func Quota() *cobra.Command { diff --git a/cmd/aws/create.go b/cmd/aws/create.go index 2d53f4cd9..8b3db5e73 100644 --- a/cmd/aws/create.go +++ b/cmd/aws/create.go @@ -61,7 +61,6 @@ func createAws(cmd *cobra.Command, args []string) error { Config: awsinternal.NewAwsV2(cloudRegionFlag), } creds, err := awsClient.Config.Credentials.Retrieve(aws.BackgroundContext()) - if err != nil { progress.Error(err.Error()) return nil @@ -79,7 +78,6 @@ func createAws(cmd *cobra.Command, args []string) error { } gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) - if err != nil { progress.Error(err.Error()) return nil diff --git a/cmd/aws/quota.go b/cmd/aws/quota.go index 35378665e..cda35fe5c 100644 --- a/cmd/aws/quota.go +++ b/cmd/aws/quota.go @@ -35,7 +35,6 @@ func printAwsQuotaWarning(messageHeader string, output map[string][]awsinternal. // Write to logs, but also output to stdout return createAwsQuotaWarning.String() - } // evalAwsQuota provides an interface to the command-line @@ -53,7 +52,7 @@ func evalAwsQuota(cmd *cobra.Command, args []string) error { return err } - var messageHeader = fmt.Sprintf( + messageHeader := fmt.Sprintf( "AWS Quota Health\nRegion: %s\n\nIf you encounter issues deploying your kubefirst cluster, check these quotas and determine if you need to request a limit increase.", cloudRegionFlag, ) diff --git a/cmd/civo/create.go b/cmd/civo/create.go index 53864b535..770623766 100644 --- a/cmd/civo/create.go +++ b/cmd/civo/create.go @@ -49,7 +49,6 @@ func createCivo(cmd *cobra.Command, args []string) error { utilities.CreateK1ClusterDirectory(clusterNameFlag) gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) - if err != nil { progress.Error(err.Error()) return nil diff --git a/cmd/civo/quota.go b/cmd/civo/quota.go index 9458e5947..89673db49 100644 --- a/cmd/civo/quota.go +++ b/cmd/civo/quota.go @@ -132,7 +132,7 @@ func returnCivoQuotaEvaluation(cloudRegion string) (string, int, int, error) { } // Parse the entire message - var messageHeader = fmt.Sprintf("Civo Quota Health\nRegion: %s\n\nNote that if any of these are approaching their limits, you may want to increase them.", cloudRegion) + messageHeader := fmt.Sprintf("Civo Quota Health\nRegion: %s\n\nNote that if any of these are approaching their limits, you may want to increase them.", cloudRegion) sort.Strings(output) result := printCivoQuotaWarning(messageHeader, output) @@ -164,7 +164,6 @@ func printCivoQuotaWarning(messageHeader string, output []string) string { // Write to logs, but also output to stdout return createCivoQuotaWarning.String() - } // evalCivoQuota provides an interface to the command-line diff --git a/cmd/digitalocean/command.go b/cmd/digitalocean/command.go index 3ebd429f6..c413078e0 100644 --- a/cmd/digitalocean/command.go +++ b/cmd/digitalocean/command.go @@ -12,7 +12,6 @@ import ( "github.com/kubefirst/kubefirst-api/pkg/constants" "github.com/kubefirst/kubefirst/internal/common" "github.com/kubefirst/kubefirst/internal/progress" - "github.com/spf13/cobra" ) diff --git a/cmd/digitalocean/create.go b/cmd/digitalocean/create.go index 8d9a1eb79..e425b3e2e 100644 --- a/cmd/digitalocean/create.go +++ b/cmd/digitalocean/create.go @@ -55,7 +55,6 @@ func createDigitalocean(cmd *cobra.Command, args []string) error { utilities.CreateK1ClusterDirectory(clusterNameFlag) gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) - if err != nil { progress.Error(err.Error()) return nil diff --git a/cmd/info.go b/cmd/info.go index 5eead2c84..b16b7f6e5 100755 --- a/cmd/info.go +++ b/cmd/info.go @@ -20,7 +20,6 @@ var infoCmd = &cobra.Command{ Short: "provides general Kubefirst setup data", Long: `Provides machine data, files and folders paths`, Run: func(cmd *cobra.Command, args []string) { - config := configs.ReadConfig() content := ` @@ -49,7 +48,3 @@ var infoCmd = &cobra.Command{ progress.Success(content) }, } - -func init() { - rootCmd.AddCommand(infoCmd) -} diff --git a/cmd/k3d/command.go b/cmd/k3d/command.go deleted file mode 100644 index 51f2c65e7..000000000 --- a/cmd/k3d/command.go +++ /dev/null @@ -1,156 +0,0 @@ -/* -Copyright (C) 2021-2023, Kubefirst - -This program is licensed under MIT. -See the LICENSE file for more details. -*/ -package k3d - -import ( - "fmt" - - "github.com/kubefirst/kubefirst/internal/progress" - "github.com/spf13/cobra" -) - -var ( - // Create - applicationNameFlag string - applicationNamespaceFlag string - ciFlag bool - cloudRegionFlag string - clusterNameFlag string - clusterTypeFlag string - githubUserFlag string - githubOrgFlag string - gitlabGroupFlag string - gitProviderFlag string - gitProtocolFlag string - gitopsTemplateURLFlag string - gitopsTemplateBranchFlag string - useTelemetryFlag bool - installCatalogApps string - - // RootCredentials - copyArgoCDPasswordToClipboardFlag bool - copyKbotPasswordToClipboardFlag bool - copyVaultPasswordToClipboardFlag bool - - // Supported git providers - supportedGitProviders = []string{"github", "gitlab"} - - // Supported git providers - supportedGitProtocolOverride = []string{"https", "ssh"} -) - -func NewCommand() *cobra.Command { - k3dCmd := &cobra.Command{ - Use: "k3d", - Short: "kubefirst k3d installation", - Long: "kubefirst k3d", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("To learn more about k3d in kubefirst, run:") - fmt.Println(" kubefirst k3d --help") - - if progress.Progress != nil { - progress.Progress.Quit() - } - }, - } - - // wire up new commands - k3dCmd.AddCommand(Create(), Destroy(), MkCert(), RootCredentials(), UnsealVault()) - - return k3dCmd -} - -func LocalCommandAlias() *cobra.Command { - localCmd := &cobra.Command{ - Use: "local", - Short: "kubefirst local installation with k3d", - Long: "kubefirst local installation with k3d", - } - - // wire up new commands - localCmd.AddCommand(Create(), Destroy(), MkCert(), RootCredentials(), UnsealVault()) - - return localCmd -} - -func Create() *cobra.Command { - createCmd := &cobra.Command{ - Use: "create", - Short: "create the kubefirst platform running in k3d on your localhost", - TraverseChildren: true, - RunE: runK3d, - } - - // todo review defaults and update descriptions - createCmd.Flags().BoolVar(&ciFlag, "ci", false, "if running kubefirst in ci, set this flag to disable interactive features") - 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(&gitProviderFlag, "git-provider", "github", fmt.Sprintf("the git provider - one of: %s", supportedGitProviders)) - createCmd.Flags().StringVar(&gitProtocolFlag, "git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %s", supportedGitProtocolOverride)) - createCmd.Flags().StringVar(&githubUserFlag, "github-user", "", "the GitHub user for the new gitops and metaphor repositories - this cannot be used with --github-org") - createCmd.Flags().StringVar(&githubOrgFlag, "github-org", "", "the GitHub organization for the new gitops and metaphor repositories - this cannot be used with --github-user") - 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/kubefirst/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") - createCmd.Flags().StringVar(&installCatalogApps, "install-catalog-apps", "", "comma seperated values of catalog apps to install after provision") - createCmd.Flags().BoolVar(&useTelemetryFlag, "use-telemetry", true, "whether to emit telemetry") - - return createCmd -} - -func Destroy() *cobra.Command { - destroyCmd := &cobra.Command{ - Use: "destroy", - Short: "destroy the kubefirst platform", - Long: "deletes the GitHub resources, k3d resources, and local content to re-provision", - RunE: destroyK3d, - } - - return destroyCmd -} - -func MkCert() *cobra.Command { - mkCertCmd := &cobra.Command{ - Use: "mkcert", - Short: "create a single ssl certificate for a local application", - Long: "create a single ssl certificate for a local application", - RunE: mkCert, - } - - mkCertCmd.Flags().StringVar(&applicationNameFlag, "application", "", "the name of the application (required)") - mkCertCmd.MarkFlagRequired("application") - mkCertCmd.Flags().StringVar(&applicationNamespaceFlag, "namespace", "", "the application namespace (required)") - mkCertCmd.MarkFlagRequired("namespace") - - return mkCertCmd -} - -func RootCredentials() *cobra.Command { - authCmd := &cobra.Command{ - Use: "root-credentials", - Short: "retrieve root authentication information for platform components", - Long: "retrieve root authentication information for platform components", - RunE: getK3dRootCredentials, - } - - authCmd.Flags().BoolVar(©ArgoCDPasswordToClipboardFlag, "argocd", false, "copy the argocd password to the clipboard (optional)") - authCmd.Flags().BoolVar(©KbotPasswordToClipboardFlag, "kbot", false, "copy the kbot password to the clipboard (optional)") - authCmd.Flags().BoolVar(©VaultPasswordToClipboardFlag, "vault", false, "copy the vault password to the clipboard (optional)") - - return authCmd -} - -func UnsealVault() *cobra.Command { - unsealVaultCmd := &cobra.Command{ - Use: "unseal-vault", - Short: "check to see if an existing vault instance is sealed and, if so, unseal it", - Long: "check to see if an existing vault instance is sealed and, if so, unseal it", - RunE: unsealVault, - } - - return unsealVaultCmd -} diff --git a/cmd/k3d/create.go b/cmd/k3d/create.go deleted file mode 100644 index 544f12e64..000000000 --- a/cmd/k3d/create.go +++ /dev/null @@ -1,1415 +0,0 @@ -/* -Copyright (C) 2021-2023, Kubefirst - -This program is licensed under MIT. -See the LICENSE file for more details. -*/ -package k3d - -import ( - "context" - "encoding/base64" - "fmt" - "net/http" - "os" - "strconv" - "strings" - "time" - - "github.com/atotto/clipboard" - "github.com/dustin/go-humanize" - "github.com/rs/zerolog/log" - - argocdapi "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" - "github.com/go-git/go-git/v5" - githttps "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/kubefirst/kubefirst-api/pkg/handlers" - "github.com/kubefirst/kubefirst-api/pkg/reports" - "github.com/kubefirst/kubefirst-api/pkg/types" - utils "github.com/kubefirst/kubefirst-api/pkg/utils" - "github.com/kubefirst/kubefirst-api/pkg/wrappers" - "github.com/kubefirst/kubefirst/internal/catalog" - "github.com/kubefirst/kubefirst/internal/gitShim" - "github.com/kubefirst/kubefirst/internal/segment" - "github.com/kubefirst/kubefirst/internal/utilities" - "github.com/kubefirst/metrics-client/pkg/telemetry" - "github.com/kubefirst/runtime/configs" - "github.com/kubefirst/runtime/pkg" - "github.com/kubefirst/runtime/pkg/argocd" - "github.com/kubefirst/runtime/pkg/gitClient" - "github.com/kubefirst/runtime/pkg/github" - gitlab "github.com/kubefirst/runtime/pkg/gitlab" - "github.com/kubefirst/runtime/pkg/helpers" - "github.com/kubefirst/runtime/pkg/k3d" - "github.com/kubefirst/runtime/pkg/k8s" - "github.com/kubefirst/runtime/pkg/progressPrinter" - "github.com/kubefirst/runtime/pkg/services" - internalssh "github.com/kubefirst/runtime/pkg/ssh" - "github.com/kubefirst/runtime/pkg/terraform" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "github.com/spf13/cobra" - "github.com/spf13/viper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var ( - cancelContext context.CancelFunc -) - -func runK3d(cmd *cobra.Command, args []string) error { - ciFlag, err := cmd.Flags().GetBool("ci") - if err != nil { - return err - } - - clusterNameFlag, err := cmd.Flags().GetString("cluster-name") - if err != nil { - return err - } - - clusterTypeFlag, err := cmd.Flags().GetString("cluster-type") - if err != nil { - return err - } - - githubOrgFlag, err := cmd.Flags().GetString("github-org") - if err != nil { - return err - } - - githubUserFlag, err := cmd.Flags().GetString("github-user") - if err != nil { - return err - } - - gitlabGroupFlag, err := cmd.Flags().GetString("gitlab-group") - if err != nil { - return err - } - - gitProviderFlag, err := cmd.Flags().GetString("git-provider") - if err != nil { - return err - } - - gitProtocolFlag, err := cmd.Flags().GetString("git-protocol") - if err != nil { - return err - } - - gitopsTemplateURLFlag, err := cmd.Flags().GetString("gitops-template-url") - if err != nil { - return err - } - - gitopsTemplateBranchFlag, err := cmd.Flags().GetString("gitops-template-branch") - if err != nil { - return err - } - - installCatalogAppsFlag, err := cmd.Flags().GetString("install-catalog-apps") - if err != nil { - return err - } - - useTelemetryFlag, err := cmd.Flags().GetBool("use-telemetry") - if err != nil { - return err - } - - // // If cluster setup is complete, return - // clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") - // if clusterSetupComplete { - // return fmt.Errorf("this cluster install process has already completed successfully") - // } - - utilities.CreateK1ClusterDirectory(clusterNameFlag) - helpers.DisplayLogHints() - - isValid, catalogApps, err := catalog.ValidateCatalogApps(installCatalogAppsFlag) - if !isValid { - return err - } - - switch gitProviderFlag { - case "github": - key, err := internalssh.GetHostKey("github.com") - if err != nil { - return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy") - } else { - log.Info().Msgf("%s %s\n", "github.com", key.Type()) - } - case "gitlab": - key, err := internalssh.GetHostKey("gitlab.com") - if err != nil { - return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy") - } else { - log.Info().Msgf("%s %s\n", "gitlab.com", key.Type()) - } - } - - // Either user or org can be specified for github, not both - if githubOrgFlag != "" && githubUserFlag != "" { - return fmt.Errorf("only one of --github-user or --github-org can be supplied") - } - - // Check for existing port forwards before continuing - err = k8s.CheckForExistingPortForwards(8080, 8200, 9000, 9094) - if err != nil { - return fmt.Errorf("%s - this port is required to set up your kubefirst environment - please close any existing port forwards before continuing", err.Error()) - } - - // Verify Docker is running # TODO: reintroduce once we support more runtimes - // dcli := docker.DockerClientWrapper{ - // Client: docker.NewDockerClient(), - // } - // _, err = dcli.CheckDockerReady() - // if err != nil { - // return err - // } - - // Global context - var ctx context.Context - ctx, cancelContext = context.WithCancel(context.Background()) - - // Clients - httpClient := http.DefaultClient - - kubefirstTeam := os.Getenv("KUBEFIRST_TEAM") - if kubefirstTeam == "" { - kubefirstTeam = "false" - } - - // Store flags for application state maintenance - viper.Set("flags.cluster-name", clusterNameFlag) - viper.Set("flags.domain-name", k3d.DomainName) - viper.Set("flags.git-provider", gitProviderFlag) - viper.Set("flags.git-protocol", gitProtocolFlag) - viper.Set("kubefirst.cloud-provider", "k3d") - viper.WriteConfig() - - // Switch based on git provider, set params - var cGitHost, cGitOwner, cGitUser, cGitToken, containerRegistryHost string - var cGitlabOwnerGroupID int - switch gitProviderFlag { - case "github": - cGitHost = k3d.GithubHost - containerRegistryHost = "ghcr.io" - - // Attempt to retrieve session-scoped token for GitHub user - gitHubService := services.NewGitHubService(httpClient) - gitHubHandler := handlers.NewGitHubHandler(gitHubService) - - // - var existingToken string - if os.Getenv("GITHUB_TOKEN") != "" { - existingToken = os.Getenv("GITHUB_TOKEN") - } else if os.Getenv("GITHUB_TOKEN") == "" && viper.GetString("github.session_token") != "" { - existingToken = viper.GetString("github.session_token") - } - gitHubAccessToken, err := wrappers.AuthenticateGitHubUserWrapper(existingToken, gitHubHandler) - if err != nil { - log.Warn().Msgf(err.Error()) - } - - // Token will either be user-provided or generated by kubefirst invocation - cGitToken = gitHubAccessToken - - // Verify token scopes - err = github.VerifyTokenPermissions(cGitToken) - if err != nil { - return err - } - - log.Info().Msg("verifying github authentication") - githubUser, err := gitHubHandler.GetGitHubUser(cGitToken) - if err != nil { - return err - } - - // Owner is either an organization or a personal user's GitHub handle - if githubOrgFlag != "" { - cGitOwner = githubOrgFlag - } else if githubUserFlag != "" { - cGitOwner = githubUser - } else if githubOrgFlag == "" && githubUserFlag == "" { - cGitOwner = githubUser - } - cGitUser = githubUser - - viper.Set("flags.github-owner", cGitOwner) - viper.Set("github.session_token", cGitToken) - viper.WriteConfig() - case "gitlab": - if gitlabGroupFlag == "" { - return fmt.Errorf("please provide a gitlab group using the --gitlab-group flag") - } - - if os.Getenv("GITLAB_TOKEN") == "" { - return fmt.Errorf("GITLAB_TOKEN environment variable unset - please set it and try again") - } - - cGitToken = os.Getenv("GITLAB_TOKEN") - - // Verify token scopes - err = gitlab.VerifyTokenPermissions(cGitToken) - if err != nil { - return err - } - - gitlabClient, err := gitlab.NewGitLabClient(cGitToken, gitlabGroupFlag) - if err != nil { - return err - } - - cGitHost = k3d.GitlabHost - cGitOwner = gitlabClient.ParentGroupPath - cGitlabOwnerGroupID = gitlabClient.ParentGroupID - log.Info().Msgf("set gitlab owner to %s", cGitOwner) - - // Get authenticated user's name - user, _, err := gitlabClient.Client.Users.CurrentUser() - if err != nil { - return fmt.Errorf("unable to get authenticated user info - please make sure GITLAB_TOKEN env var is set %s", err.Error()) - } - cGitUser = user.Username - - containerRegistryHost = "registry.gitlab.com" - viper.Set("flags.gitlab-owner", gitlabGroupFlag) - viper.Set("flags.gitlab-owner-group-id", cGitlabOwnerGroupID) - viper.Set("gitlab.session_token", cGitToken) - viper.WriteConfig() - default: - log.Error().Msgf("invalid git provider option") - } - - // Ask for confirmation - var gitDestDescriptor string - switch gitProviderFlag { - case "github": - if githubOrgFlag != "" { - gitDestDescriptor = "Organization" - } - if githubUserFlag != "" { - gitDestDescriptor = "User" - } - if githubUserFlag == "" && githubOrgFlag == "" { - gitDestDescriptor = "User" - } - case "gitlab": - gitDestDescriptor = "Group" - } - - // todo - // Since it's possible to stop and restart, cGitOwner may need to be reset - //if cGitOwner == "" { - // switch gitProviderFlag { - // case "github": - // cGitOwner = viper.GetString("flags.github-owner") - // case "gitlab": - // cGitOwner = viper.GetString("flags.gitlab-owner") - // } - //} - // - //model, err := presentRecap(gitProviderFlag, gitDestDescriptor, cGitOwner) - //if err != nil { - // return err - //} - //_, err = tea.NewProgram(model).Run() - //if err != nil { - // return err - //} - - // Instantiate K3d config - config := k3d.GetConfig(clusterNameFlag, gitProviderFlag, cGitOwner, gitProtocolFlag) - switch gitProviderFlag { - case "github": - config.GithubToken = cGitToken - case "gitlab": - config.GitlabToken = cGitToken - } - - var sshPrivateKey, sshPublicKey string - - // todo placed in configmap in kubefirst namespace, included in telemetry - clusterId := viper.GetString("kubefirst.cluster-id") - if clusterId == "" { - clusterId = pkg.GenerateClusterID() - viper.Set("kubefirst.cluster-id", clusterId) - viper.WriteConfig() - } - - segClient := segment.InitClient(clusterId, clusterTypeFlag, gitProviderFlag) - - // Progress output - progressPrinter.AddTracker("preflight-checks", "Running preflight checks", 5) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - progressPrinter.IncrementTracker("preflight-checks", 1) - - // this branch flag value is overridden with a tag when running from a - // kubefirst binary for version compatibility - switch configs.K1Version { - case "development": - if strings.Contains(gitopsTemplateURLFlag, "https://github.com/kubefirst/gitops-template.git") && gitopsTemplateBranchFlag == "" { - gitopsTemplateBranchFlag = "main" - } - default: - switch gitopsTemplateURLFlag { - case "https://github.com/kubefirst/gitops-template.git": //default value - if gitopsTemplateBranchFlag == "" { - gitopsTemplateBranchFlag = configs.K1Version - } - case "https://github.com/kubefirst/gitops-template": // edge case for valid but incomplete url - if gitopsTemplateBranchFlag == "" { - gitopsTemplateBranchFlag = configs.K1Version - } - default: // not equal to our defaults - if gitopsTemplateBranchFlag == "" { //didn't supply the branch flag but they did supply the repo flag - return fmt.Errorf("must supply gitops-template-branch flag when gitops-template-url is overridden") - } - } - } - - log.Info().Msgf("kubefirst version configs.K1Version: %s ", configs.K1Version) - log.Info().Msgf("cloning gitops-template repo url: %s ", gitopsTemplateURLFlag) - log.Info().Msgf("cloning gitops-template repo branch: %s ", gitopsTemplateBranchFlag) - - atlantisWebhookSecret := viper.GetString("secrets.atlantis-webhook") - if atlantisWebhookSecret == "" { - atlantisWebhookSecret = pkg.Random(20) - viper.Set("secrets.atlantis-webhook", atlantisWebhookSecret) - viper.WriteConfig() - } - - atlantisNgrokAuthtoken := viper.GetString("secrets.atlantis-ngrok-authtoken") - if atlantisNgrokAuthtoken == "" { - atlantisNgrokAuthtoken = os.Getenv("NGROK_AUTHTOKEN") - viper.Set("secrets.atlantis-ngrok-authtoken", atlantisNgrokAuthtoken) - viper.WriteConfig() - } - - log.Info().Msg("checking authentication to required providers") - - // check disk - free, err := pkg.GetAvailableDiskSize() - if err != nil { - return err - } - - // convert available disk size to GB format - availableDiskSize := float64(free) / humanize.GByte - if availableDiskSize < pkg.MinimumAvailableDiskSize { - return fmt.Errorf( - "there is not enough space to proceed with the installation, a minimum of %d GB is required to proceed", - pkg.MinimumAvailableDiskSize, - ) - } - progressPrinter.IncrementTracker("preflight-checks", 1) - - // Objects to check for - // Repositories that will be created throughout the initialization process - newRepositoryNames := []string{"gitops", "metaphor"} - newTeamNames := []string{"admins", "developers"} - - // Check git credentials - executionControl := viper.GetBool(fmt.Sprintf("kubefirst-checks.%s-credentials", config.GitProvider)) - if !executionControl { - telemetry.SendEvent(segClient, telemetry.GitCredentialsCheckStarted, "") - if len(cGitToken) == 0 { - msg := fmt.Sprintf( - "please set a %s_TOKEN environment variable to continue", - strings.ToUpper(config.GitProvider), - ) - telemetry.SendEvent(segClient, telemetry.GitCredentialsCheckFailed, msg) - return fmt.Errorf(msg) - } - - initGitParameters := gitShim.GitInitParameters{ - GitProvider: gitProviderFlag, - GitToken: cGitToken, - GitOwner: cGitOwner, - Repositories: newRepositoryNames, - Teams: newTeamNames, - } - err = gitShim.InitializeGitProvider(&initGitParameters) - if err != nil { - return err - } - - viper.Set(fmt.Sprintf("kubefirst-checks.%s-credentials", config.GitProvider), true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.GitCredentialsCheckCompleted, "") - progressPrinter.IncrementTracker("preflight-checks", 1) - } else { - log.Info().Msg(fmt.Sprintf("already completed %s checks - continuing", config.GitProvider)) - progressPrinter.IncrementTracker("preflight-checks", 1) - } - // Swap tokens for git protocol - var gitopsRepoURL string - executionControl = viper.GetBool("kubefirst-checks.kbot-setup") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.KbotSetupStarted, "") - - log.Info().Msg("creating an ssh key pair for your new cloud infrastructure") - sshPrivateKey, sshPublicKey, err = utils.CreateSshKeyPair() - if err != nil { - telemetry.SendEvent(segClient, telemetry.KbotSetupFailed, err.Error()) - return err - } - log.Info().Msg("ssh key pair creation complete") - - viper.Set("kbot.private-key", sshPrivateKey) - viper.Set("kbot.public-key", sshPublicKey) - viper.Set("kbot.username", "kbot") - viper.Set("kubefirst-checks.kbot-setup", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.KbotSetupCompleted, "") - log.Info().Msg("kbot-setup complete") - progressPrinter.IncrementTracker("preflight-checks", 1) - } else { - log.Info().Msg("already setup kbot user - continuing") - progressPrinter.IncrementTracker("preflight-checks", 1) - } - - log.Info().Msg("validation and kubefirst cli environment check is complete") - - telemetry.SendEvent(segClient, telemetry.InitCompleted, "") - telemetry.SendEvent(segClient, telemetry.InitCompleted, "") - - // Swap tokens for git protocol - switch config.GitProtocol { - case "https": - gitopsRepoURL = config.DestinationGitopsRepoURL - default: - gitopsRepoURL = config.DestinationGitopsRepoGitURL - } - - gitopsDirectoryTokens := k3d.GitopsDirectoryValues{ - GithubOwner: cGitOwner, - GithubUser: cGitUser, - GitlabOwner: cGitOwner, - GitlabOwnerGroupID: cGitlabOwnerGroupID, - GitlabUser: cGitUser, - DomainName: k3d.DomainName, - AtlantisAllowList: fmt.Sprintf("%s/%s/*", cGitHost, cGitOwner), - AlertsEmail: "REMOVE_THIS_VALUE", - ClusterName: clusterNameFlag, - ClusterType: clusterTypeFlag, - GithubHost: k3d.GithubHost, - GitlabHost: k3d.GitlabHost, - ArgoWorkflowsIngressURL: fmt.Sprintf("https://argo.%s", k3d.DomainName), - VaultIngressURL: fmt.Sprintf("https://vault.%s", k3d.DomainName), - ArgocdIngressURL: fmt.Sprintf("https://argocd.%s", k3d.DomainName), - AtlantisIngressURL: fmt.Sprintf("https://atlantis.%s", k3d.DomainName), - MetaphorDevelopmentIngressURL: fmt.Sprintf("https://metaphor-development.%s", k3d.DomainName), - MetaphorStagingIngressURL: fmt.Sprintf("https://metaphor-staging.%s", k3d.DomainName), - MetaphorProductionIngressURL: fmt.Sprintf("https://metaphor-production.%s", k3d.DomainName), - KubefirstVersion: configs.K1Version, - KubefirstTeam: kubefirstTeam, - KubeconfigPath: config.Kubeconfig, - GitopsRepoURL: gitopsRepoURL, - GitProvider: config.GitProvider, - ClusterId: clusterId, - CloudProvider: k3d.CloudProvider, - } - - if useTelemetryFlag { - gitopsDirectoryTokens.UseTelemetry = "true" - } else { - gitopsDirectoryTokens.UseTelemetry = "false" - } - - //* generate http credentials for git auth over https - httpAuth := &githttps.BasicAuth{ - Username: cGitUser, - Password: cGitToken, - } - - if err != nil { - log.Info().Msgf("generate public keys failed: %s\n", err.Error()) - } - - //* download dependencies to `$HOME/.k1/tools` - if !viper.GetBool("kubefirst-checks.tools-downloaded") { - log.Info().Msg("installing kubefirst dependencies") - - err := k3d.DownloadTools(clusterNameFlag, config.GitProvider, cGitOwner, config.ToolsDir, config.GitProtocol) - if err != nil { - return err - } - - log.Info().Msg("download dependencies `$HOME/.k1/tools` complete") - viper.Set("kubefirst-checks.tools-downloaded", true) - viper.WriteConfig() - } else { - log.Info().Msg("already completed download of dependencies to `$HOME/.k1/tools` - continuing") - } - progressPrinter.IncrementTracker("preflight-checks", 1) - - metaphorTemplateTokens := k3d.MetaphorTokenValues{ - ClusterName: clusterNameFlag, - CloudRegion: cloudRegionFlag, - ContainerRegistryURL: fmt.Sprintf("%s/%s/metaphor", containerRegistryHost, cGitOwner), - DomainName: k3d.DomainName, - MetaphorDevelopmentIngressURL: fmt.Sprintf("metaphor-development.%s", k3d.DomainName), - MetaphorStagingIngressURL: fmt.Sprintf("metaphor-staging.%s", k3d.DomainName), - MetaphorProductionIngressURL: fmt.Sprintf("metaphor-production.%s", k3d.DomainName), - } - - //* git clone and detokenize the gitops repository - // todo improve this logic for removing `kubefirst clean` - // if !viper.GetBool("template-repo.gitops.cloned") || viper.GetBool("template-repo.gitops.removed") { - progressPrinter.IncrementTracker("preflight-checks", 1) - progressPrinter.IncrementTracker("preflight-checks", 1) - progressPrinter.AddTracker("cloning-and-formatting-git-repositories", "Cloning and formatting git repositories", 1) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - removeAtlantis := false - if viper.GetString("secrets.atlantis-ngrok-authtoken") == "" { - removeAtlantis = true - } - if !viper.GetBool("kubefirst-checks.gitops-ready-to-push") { - log.Info().Msg("generating your new gitops repository") - err := k3d.PrepareGitRepositories( - config.GitProvider, - clusterNameFlag, - clusterTypeFlag, - config.DestinationGitopsRepoURL, //default to https for git interactions when creating remotes - config.GitopsDir, - gitopsTemplateBranchFlag, - gitopsTemplateURLFlag, - config.DestinationMetaphorRepoURL, //default to https for git interactions when creating remotes - config.K1Dir, - &gitopsDirectoryTokens, - config.MetaphorDir, - &metaphorTemplateTokens, - gitProtocolFlag, - removeAtlantis, - ) - if err != nil { - return err - } - - // todo emit init telemetry end - viper.Set("kubefirst-checks.gitops-ready-to-push", true) - viper.WriteConfig() - progressPrinter.IncrementTracker("cloning-and-formatting-git-repositories", 1) - } else { - log.Info().Msg("already completed gitops repo generation - continuing") - progressPrinter.IncrementTracker("cloning-and-formatting-git-repositories", 1) - } - - progressPrinter.AddTracker("applying-git-terraform", fmt.Sprintf("Applying %s Terraform", config.GitProvider), 1) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - switch config.GitProvider { - case "github": - // //* create teams and repositories in github - executionControl = viper.GetBool("kubefirst-checks.terraform-apply-github") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyStarted, "") - - log.Info().Msg("Creating GitHub resources with Terraform") - - tfEntrypoint := config.GitopsDir + "/terraform/github" - tfEnvs := map[string]string{} - // tfEnvs = k3d.GetGithubTerraformEnvs(tfEnvs) - tfEnvs["GITHUB_TOKEN"] = cGitToken - tfEnvs["GITHUB_OWNER"] = cGitOwner - tfEnvs["TF_VAR_kbot_ssh_public_key"] = viper.GetString("kbot.public-key") - tfEnvs["AWS_ACCESS_KEY_ID"] = pkg.MinioDefaultUsername - tfEnvs["AWS_SECRET_ACCESS_KEY"] = pkg.MinioDefaultPassword - tfEnvs["TF_VAR_aws_access_key_id"] = pkg.MinioDefaultUsername - tfEnvs["TF_VAR_aws_secret_access_key"] = pkg.MinioDefaultPassword - // Erase public key to prevent it from being created if the git protocol argument is set to htps - switch config.GitProtocol { - case "https": - tfEnvs["TF_VAR_kbot_ssh_public_key"] = "" - } - err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs) - if err != nil { - msg := fmt.Sprintf("error creating github resources with terraform %s: %s", tfEntrypoint, err) - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyFailed, msg) - return fmt.Errorf(msg) - } - - log.Info().Msgf("created git repositories for github.com/%s", cGitOwner) - viper.Set("kubefirst-checks.terraform-apply-github", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyCompleted, "") - progressPrinter.IncrementTracker("applying-git-terraform", 1) - } else { - log.Info().Msg("already created GitHub Terraform resources") - progressPrinter.IncrementTracker("applying-git-terraform", 1) - } - case "gitlab": - // //* create teams and repositories in gitlab - executionControl = viper.GetBool("kubefirst-checks.terraform-apply-gitlab") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyStarted, "") - - log.Info().Msg("Creating GitLab resources with Terraform") - - tfEntrypoint := config.GitopsDir + "/terraform/gitlab" - tfEnvs := map[string]string{} - tfEnvs["GITLAB_TOKEN"] = cGitToken - tfEnvs["GITLAB_OWNER"] = gitlabGroupFlag - tfEnvs["TF_VAR_owner_group_id"] = strconv.Itoa(cGitlabOwnerGroupID) - tfEnvs["TF_VAR_kbot_ssh_public_key"] = viper.GetString("kbot.public-key") - tfEnvs["AWS_ACCESS_KEY_ID"] = pkg.MinioDefaultUsername - tfEnvs["AWS_SECRET_ACCESS_KEY"] = pkg.MinioDefaultPassword - tfEnvs["TF_VAR_aws_access_key_id"] = pkg.MinioDefaultUsername - tfEnvs["TF_VAR_aws_secret_access_key"] = pkg.MinioDefaultPassword - // Erase public key to prevent it from being created if the git protocol argument is set to htps - switch config.GitProtocol { - case "https": - tfEnvs["TF_VAR_kbot_ssh_public_key"] = "" - } - err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs) - if err != nil { - msg := fmt.Sprintf("error creating gitlab resources with terraform %s: %s", tfEntrypoint, err) - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyFailed, msg) - return fmt.Errorf(msg) - } - - log.Info().Msgf("created git projects and groups for gitlab.com/%s", gitlabGroupFlag) - viper.Set("kubefirst-checks.terraform-apply-gitlab", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.GitTerraformApplyCompleted, "") - progressPrinter.IncrementTracker("applying-git-terraform", 1) - } else { - log.Info().Msg("already created GitLab Terraform resources") - progressPrinter.IncrementTracker("applying-git-terraform", 1) - } - } - - //* push detokenized gitops-template repository content to new remote - progressPrinter.AddTracker("pushing-gitops-repos-upstream", "Pushing git repositories", 1) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - log.Info().Msgf("referencing gitops repository: %s", config.DestinationGitopsRepoGitURL) - log.Info().Msgf("referencing metaphor repository: %s", config.DestinationMetaphorRepoURL) - - executionControl = viper.GetBool("kubefirst-checks.gitops-repo-pushed") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.GitopsRepoPushStarted, "") - - gitopsRepo, err := git.PlainOpen(config.GitopsDir) - if err != nil { - log.Info().Msgf("error opening repo at: %s", config.GitopsDir) - } - - metaphorRepo, err := git.PlainOpen(config.MetaphorDir) - if err != nil { - log.Info().Msgf("error opening repo at: %s", config.MetaphorDir) - } - - err = utils.EvalSSHKey(&types.EvalSSHKeyRequest{ - GitProvider: gitProviderFlag, - GitlabGroupFlag: gitlabGroupFlag, - GitToken: cGitToken, - }) - if err != nil { - return err - } - - //Push to remotes and use https - // Push gitops repo to remote - err = gitopsRepo.Push( - &git.PushOptions{ - RemoteName: config.GitProvider, - Auth: httpAuth, - }, - ) - if err != nil { - msg := fmt.Sprintf("error pushing detokenized gitops repository to remote %s: %s", config.DestinationGitopsRepoGitURL, err) - telemetry.SendEvent(segClient, telemetry.GitopsRepoPushFailed, msg) - if !strings.Contains(msg, "already up-to-date") { - log.Panic().Msg(msg) - } - } - - // push metaphor repo to remote - err = metaphorRepo.Push( - &git.PushOptions{ - RemoteName: "origin", - Auth: httpAuth, - }, - ) - if err != nil { - msg := fmt.Sprintf("error pushing detokenized metaphor repository to remote %s: %s", config.DestinationMetaphorRepoURL, err) - telemetry.SendEvent(segClient, telemetry.GitopsRepoPushFailed, msg) - if !strings.Contains(msg, "already up-to-date") { - log.Panic().Msg(msg) - } - } - log.Info().Msgf("successfully pushed gitops and metaphor repositories to https://%s/%s", cGitHost, cGitOwner) - - // todo delete the local gitops repo and re-clone it - // todo that way we can stop worrying about which origin we're going to push to - viper.Set("kubefirst-checks.gitops-repo-pushed", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.GitopsRepoPushCompleted, "") - progressPrinter.IncrementTracker("pushing-gitops-repos-upstream", 1) // todo verify this tracker didnt lose one - } else { - log.Info().Msg("already pushed detokenized gitops repository content") - progressPrinter.IncrementTracker("pushing-gitops-repos-upstream", 1) - } - - //* create k3d resources - - progressPrinter.AddTracker("creating-k3d-cluster", "Creating k3d cluster", 1) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - if !viper.GetBool("kubefirst-checks.create-k3d-cluster") { - telemetry.SendEvent(segClient, telemetry.CloudTerraformApplyStarted, "") - - log.Info().Msg("Creating k3d cluster") - - err := k3d.ClusterCreate(clusterNameFlag, config.K1Dir, config.K3dClient, config.Kubeconfig) - if err != nil { - msg := fmt.Sprintf("error creating k3d resources with k3d client %s: %s", config.K3dClient, err) - viper.Set("kubefirst-checks.create-k3d-cluster-failed", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.CloudTerraformApplyFailed, msg) - return fmt.Errorf(msg) - } - - log.Info().Msg("successfully created k3d cluster") - viper.Set("kubefirst-checks.create-k3d-cluster", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.CloudTerraformApplyCompleted, "") - progressPrinter.IncrementTracker("creating-k3d-cluster", 1) - } else { - log.Info().Msg("already created k3d cluster resources") - progressPrinter.IncrementTracker("creating-k3d-cluster", 1) - } - - kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) - - // kubernetes.BootstrapSecrets - progressPrinter.AddTracker("bootstrapping-kubernetes-resources", "Bootstrapping Kubernetes resources", 2) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - executionControl = viper.GetBool("kubefirst-checks.k8s-secrets-created") - if !executionControl { - err := k3d.GenerateTLSSecrets(kcfg.Clientset, *config) - if err != nil { - return err - } - - err = k3d.AddK3DSecrets( - atlantisWebhookSecret, - viper.GetString("kbot.public-key"), - gitopsRepoURL, - viper.GetString("kbot.private-key"), - config.GitProvider, - cGitUser, - cGitOwner, - config.Kubeconfig, - cGitToken, - ) - if err != nil { - log.Info().Msg("Error adding kubernetes secrets for bootstrap") - return err - } - viper.Set("kubefirst-checks.k8s-secrets-created", true) - viper.WriteConfig() - progressPrinter.IncrementTracker("bootstrapping-kubernetes-resources", 1) - } else { - log.Info().Msg("already added secrets to k3d cluster") - progressPrinter.IncrementTracker("bootstrapping-kubernetes-resources", 1) - } - - // //* check for ssl restore - // log.Info().Msg("checking for tls secrets to restore") - // secretsFilesToRestore, err := ioutil.ReadDir(config.SSLBackupDir + "/secrets") - // if err != nil { - // log.Info().Msgf("%s", err) - // } - // if len(secretsFilesToRestore) != 0 { - // // todo would like these but requires CRD's and is not currently supported - // // add crds ( use execShellReturnErrors? ) - // // https://raw.githubusercontent.com/cert-manager/cert-manager/v1.11.0/deploy/crds/crd-clusterissuers.yaml - // // https://raw.githubusercontent.com/cert-manager/cert-manager/v1.11.0/deploy/crds/crd-certificates.yaml - // // add certificates, and clusterissuers - // log.Info().Msgf("found %d tls secrets to restore", len(secretsFilesToRestore)) - // ssl.Restore(config.SSLBackupDir, k3d.DomainName, config.Kubeconfig) - // } else { - // log.Info().Msg("no files found in secrets directory, continuing") - // } - - // Container registry authentication creation - containerRegistryAuth := gitShim.ContainerRegistryAuth{ - GitProvider: gitProviderFlag, - GitUser: cGitUser, - GitToken: cGitToken, - GitlabGroupFlag: gitlabGroupFlag, - GithubOwner: cGitOwner, - ContainerRegistryHost: containerRegistryHost, - Clientset: kcfg.Clientset, - } - containerRegistryAuthToken, err := gitShim.CreateContainerRegistrySecret(&containerRegistryAuth) - if err != nil { - return err - } - progressPrinter.IncrementTracker("bootstrapping-kubernetes-resources", 1) - - // k3d Readiness checks - progressPrinter.AddTracker("verifying-k3d-cluster-readiness", "Verifying Kubernetes cluster is ready", 3) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - // traefik - traefikDeployment, err := k8s.ReturnDeploymentObject( - kcfg.Clientset, - "app.kubernetes.io/name", - "traefik", - "kube-system", - 240, - ) - if err != nil { - log.Error().Msgf("error finding traefik deployment: %s", err) - return err - } - _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, traefikDeployment, 240) - if err != nil { - log.Error().Msgf("error waiting for traefik deployment ready state: %s", err) - return err - } - progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) - - // metrics-server - metricsServerDeployment, err := k8s.ReturnDeploymentObject( - kcfg.Clientset, - "k8s-app", - "metrics-server", - "kube-system", - 240, - ) - if err != nil { - log.Error().Msgf("error finding metrics-server deployment: %s", err) - return err - } - _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, metricsServerDeployment, 240) - if err != nil { - log.Error().Msgf("error waiting for metrics-server deployment ready state: %s", err) - return err - } - progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) - - time.Sleep(time.Second * 20) - - progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) - - progressPrinter.AddTracker("installing-argo-cd", "Installing and configuring Argo CD", 3) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - argoCDInstallPath := fmt.Sprintf("github.com:kubefirst/manifests/argocd/k3d?ref=%s", pkg.KubefirstManifestRepoRef) - - //* install argocd - executionControl = viper.GetBool("kubefirst-checks.argocd-install") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.ArgoCDInstallStarted, "") - - log.Info().Msgf("installing argocd") - - // Build and apply manifests - yamlData, err := kcfg.KustomizeBuild(argoCDInstallPath) - if err != nil { - return err - } - output, err := kcfg.SplitYAMLFile(yamlData) - if err != nil { - return err - } - err = kcfg.ApplyObjects("", output) - if err != nil { - telemetry.SendEvent(segClient, telemetry.ArgoCDInstallFailed, err.Error()) - return err - } - - viper.Set("kubefirst-checks.argocd-install", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.ArgoCDInstallCompleted, "") - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } else { - log.Info().Msg("argo cd already installed, continuing") - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } - - // Wait for ArgoCD to be ready - _, err = k8s.VerifyArgoCDReadiness(kcfg.Clientset, true, 300) - if err != nil { - log.Error().Msgf("error waiting for ArgoCD to become ready: %s", err) - return err - } - - var argocdPassword string - //* argocd pods are ready, get and set credentials - executionControl = viper.GetBool("kubefirst-checks.argocd-credentials-set") - if !executionControl { - log.Info().Msg("Setting argocd username and password credentials") - - argocd.ArgocdSecretClient = kcfg.Clientset.CoreV1().Secrets("argocd") - - argocdPassword = k8s.GetSecretValue(argocd.ArgocdSecretClient, "argocd-initial-admin-secret", "password") - if argocdPassword == "" { - log.Info().Msg("argocd password not found in secret") - return err - } - - viper.Set("components.argocd.password", argocdPassword) - viper.Set("components.argocd.username", "admin") - viper.WriteConfig() - log.Info().Msg("argocd username and password credentials set successfully") - log.Info().Msg("Getting an argocd auth token") - - // Test https to argocd - var argoCDToken string - // only the host, not the protocol - err := helpers.TestEndpointTLS(strings.Replace(k3d.ArgocdURL, "https://", "", 1)) - if err != nil { - argoCDStopChannel := make(chan struct{}, 1) - log.Info().Msgf("argocd not available via https, using http") - defer func() { - close(argoCDStopChannel) - }() - k8s.OpenPortForwardPodWrapper( - kcfg.Clientset, - kcfg.RestConfig, - "argocd-server", - "argocd", - 8080, - 8080, - argoCDStopChannel, - ) - argoCDHTTPURL := strings.Replace( - k3d.ArgocdURL, - "https://", - "http://", - 1, - ) + ":8080" - argoCDToken, err = argocd.GetArgocdTokenV2(httpClient, argoCDHTTPURL, "admin", argocdPassword) - if err != nil { - return err - } - } else { - argoCDToken, err = argocd.GetArgocdTokenV2(httpClient, k3d.ArgocdURL, "admin", argocdPassword) - if err != nil { - return err - } - } - - log.Info().Msg("argocd admin auth token set") - - viper.Set("components.argocd.auth-token", argoCDToken) - viper.Set("kubefirst-checks.argocd-credentials-set", true) - viper.WriteConfig() - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } else { - log.Info().Msg("argo credentials already set, continuing") - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } - - if configs.K1Version == "development" { - err := clipboard.WriteAll(argocdPassword) - if err != nil { - log.Error().Err(err).Msg("") - } - - if os.Getenv("SKIP_ARGOCD_LAUNCH") != "true" || !ciFlag { - err = pkg.OpenBrowser(pkg.ArgoCDLocalURLTLS) - if err != nil { - log.Error().Err(err).Msg("") - } - } - } - - //* argocd sync registry and start sync waves - executionControl = viper.GetBool("kubefirst-checks.argocd-create-registry") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.CreateRegistryStarted, "") - argocdClient, err := argocdapi.NewForConfig(kcfg.RestConfig) - if err != nil { - return err - } - - log.Info().Msg("applying the registry application to argocd") - registryApplicationObject := argocd.GetArgoCDApplicationObject(gitopsRepoURL, fmt.Sprintf("registry/%s", clusterNameFlag)) - - _, _ = argocdClient.ArgoprojV1alpha1().Applications("argocd").Create(context.Background(), registryApplicationObject, metav1.CreateOptions{}) - viper.Set("kubefirst-checks.argocd-create-registry", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.CreateRegistryCompleted, "") - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } else { - log.Info().Msg("argocd registry create already done, continuing") - progressPrinter.IncrementTracker("installing-argo-cd", 1) - } - - // Wait for Vault StatefulSet Pods to transition to Running - progressPrinter.AddTracker("configuring-vault", "Configuring Vault", 4) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - vaultStatefulSet, err := k8s.ReturnStatefulSetObject( - kcfg.Clientset, - "app.kubernetes.io/instance", - "vault", - "vault", - 120, - ) - if err != nil { - log.Error().Msgf("Error finding Vault StatefulSet: %s", err) - return err - } - _, err = k8s.WaitForStatefulSetReady(kcfg.Clientset, vaultStatefulSet, 120, true) - if err != nil { - log.Error().Msgf("Error waiting for Vault StatefulSet ready state: %s", err) - return err - } - progressPrinter.IncrementTracker("configuring-vault", 1) - - // Init and unseal vault - // We need to wait before we try to run any of these commands or there may be - // unexpected timeouts - time.Sleep(time.Second * 10) - progressPrinter.IncrementTracker("configuring-vault", 1) - - executionControl = viper.GetBool("kubefirst-checks.vault-initialized") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.VaultInitializationStarted, "") - - // Initialize and unseal Vault - vaultHandlerPath := "github.com:kubefirst/manifests.git/vault-handler/replicas-1" - - // Build and apply manifests - yamlData, err := kcfg.KustomizeBuild(vaultHandlerPath) - if err != nil { - return err - } - output, err := kcfg.SplitYAMLFile(yamlData) - if err != nil { - return err - } - err = kcfg.ApplyObjects("", output) - if err != nil { - return err - } - - // Wait for the Job to finish - job, err := k8s.ReturnJobObject(kcfg.Clientset, "vault", "vault-handler") - if err != nil { - return err - } - _, err = k8s.WaitForJobComplete(kcfg.Clientset, job, 240) - if err != nil { - msg := fmt.Sprintf("could not run vault unseal job: %s", err) - telemetry.SendEvent(segClient, telemetry.VaultInitializationFailed, msg) - log.Fatal().Msg(msg) - } - - viper.Set("kubefirst-checks.vault-initialized", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.VaultInitializationCompleted, "") - progressPrinter.IncrementTracker("configuring-vault", 1) - } else { - log.Info().Msg("vault is already initialized - skipping") - progressPrinter.IncrementTracker("configuring-vault", 1) - } - - minioStopChannel := make(chan struct{}, 1) - defer func() { - close(minioStopChannel) - }() - k8s.OpenPortForwardPodWrapper( - kcfg.Clientset, - kcfg.RestConfig, - "minio", - "minio", - 9000, - 9000, - minioStopChannel, - ) - - // Initialize minio client object. - minioClient, err := minio.New(pkg.MinioPortForwardEndpoint, &minio.Options{ - Creds: credentials.NewStaticV4(pkg.MinioDefaultUsername, pkg.MinioDefaultPassword, ""), - Secure: false, - Region: pkg.MinioRegion, - }) - if err != nil { - log.Info().Msgf("Error creating Minio client: %s", err) - } - - //define upload object - objectName := fmt.Sprintf("terraform/%s/terraform.tfstate", config.GitProvider) - filePath := config.K1Dir + fmt.Sprintf("/gitops/%s", objectName) - contentType := "xl.meta" - bucketName := "kubefirst-state-store" - log.Info().Msgf("BucketName: %s", bucketName) - - viper.Set("kubefirst.state-store.name", bucketName) - viper.Set("kubefirst.state-store.hostname", "minio-console.kubefirst.dev") - viper.Set("kubefirst.state-store-creds.access-key-id", pkg.MinioDefaultUsername) - viper.Set("kubefirst.state-store-creds.secret-access-key-id", pkg.MinioDefaultPassword) - - // Upload the zip file with FPutObject - info, err := minioClient.FPutObject(ctx, bucketName, objectName, filePath, minio.PutObjectOptions{ContentType: contentType}) - if err != nil { - log.Info().Msgf("Error uploading to Minio bucket: %s", err) - } - - log.Printf("Successfully uploaded %s to bucket %s\n", objectName, info.Bucket) - - progressPrinter.IncrementTracker("configuring-vault", 1) - - //* configure vault with terraform - //* vault port-forward - vaultStopChannel := make(chan struct{}, 1) - defer func() { - close(vaultStopChannel) - }() - k8s.OpenPortForwardPodWrapper( - kcfg.Clientset, - kcfg.RestConfig, - "vault-0", - "vault", - 8200, - 8200, - vaultStopChannel, - ) - - // Retrieve root token from init step - var vaultRootToken string - secData, err := k8s.ReadSecretV2(kcfg.Clientset, "vault", "vault-unseal-secret") - if err != nil { - return err - } - - vaultRootToken = secData["root-token"] - - // Parse k3d api endpoint from kubeconfig - // In this case, we need to get the IP of the in-cluster API server to provide to Vault - // to work with Kubernetes auth - kubernetesInClusterAPIService, err := k8s.ReadService(config.Kubeconfig, "default", "kubernetes") - if err != nil { - log.Error().Msgf("error looking up kubernetes api server service: %s", err) - return err - } - - err = helpers.TestEndpointTLS(strings.Replace(k3d.VaultURL, "https://", "", 1)) - if err != nil { - return fmt.Errorf( - "unable to reach vault over https - this is likely due to the mkcert certificate store missing. please install it via `%s -install`", config.MkCertClient, - ) - } - - //* configure vault with terraform - executionControl = viper.GetBool("kubefirst-checks.terraform-apply-vault") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.VaultTerraformApplyStarted, "") - - tfEnvs := map[string]string{} - var usernamePasswordString, base64DockerAuth string - - if config.GitProvider == "gitlab" { - usernamePasswordString = fmt.Sprintf("%s:%s", "container-registry-auth", containerRegistryAuthToken) - base64DockerAuth = base64.StdEncoding.EncodeToString([]byte(usernamePasswordString)) - - tfEnvs["TF_VAR_container_registry_auth"] = containerRegistryAuthToken - tfEnvs["TF_VAR_owner_group_id"] = strconv.Itoa(cGitlabOwnerGroupID) - } else { - usernamePasswordString = fmt.Sprintf("%s:%s", cGitUser, cGitToken) - base64DockerAuth = base64.StdEncoding.EncodeToString([]byte(usernamePasswordString)) - - } - - log.Info().Msg("configuring vault with terraform") - - tfEnvs["TF_VAR_email_address"] = "your@email.com" - tfEnvs[fmt.Sprintf("TF_VAR_%s_token", config.GitProvider)] = cGitToken - tfEnvs[fmt.Sprintf("TF_VAR_%s_user", config.GitProvider)] = cGitUser - tfEnvs["TF_VAR_vault_addr"] = k3d.VaultPortForwardURL - tfEnvs["TF_VAR_b64_docker_auth"] = base64DockerAuth - tfEnvs["TF_VAR_vault_token"] = vaultRootToken - tfEnvs["VAULT_ADDR"] = k3d.VaultPortForwardURL - tfEnvs["VAULT_TOKEN"] = vaultRootToken - tfEnvs["TF_VAR_atlantis_repo_webhook_secret"] = viper.GetString("secrets.atlantis-webhook") - tfEnvs["TF_VAR_kbot_ssh_private_key"] = viper.GetString("kbot.private-key") - tfEnvs["TF_VAR_kbot_ssh_public_key"] = viper.GetString("kbot.public-key") - tfEnvs["TF_VAR_kubernetes_api_endpoint"] = fmt.Sprintf("https://%s", kubernetesInClusterAPIService.Spec.ClusterIP) - tfEnvs[fmt.Sprintf("%s_OWNER", strings.ToUpper(config.GitProvider))] = viper.GetString(fmt.Sprintf("flags.%s-owner", config.GitProvider)) - tfEnvs["AWS_ACCESS_KEY_ID"] = pkg.MinioDefaultUsername - tfEnvs["AWS_SECRET_ACCESS_KEY"] = pkg.MinioDefaultPassword - tfEnvs["TF_VAR_aws_access_key_id"] = pkg.MinioDefaultUsername - tfEnvs["TF_VAR_aws_secret_access_key"] = pkg.MinioDefaultPassword - tfEnvs["TF_VAR_ngrok_authtoken"] = viper.GetString("secrets.atlantis-ngrok-authtoken") - // tfEnvs["TF_LOG"] = "DEBUG" - - tfEntrypoint := config.GitopsDir + "/terraform/vault" - err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs) - if err != nil { - telemetry.SendEvent(segClient, telemetry.VaultTerraformApplyStarted, err.Error()) - return err - } - - log.Info().Msg("vault terraform executed successfully") - viper.Set("kubefirst-checks.terraform-apply-vault", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.VaultTerraformApplyCompleted, "") - progressPrinter.IncrementTracker("configuring-vault", 1) - } else { - log.Info().Msg("already executed vault terraform") - progressPrinter.IncrementTracker("configuring-vault", 1) - } - - //* create users - progressPrinter.AddTracker("creating-users", "Creating users", 1) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - executionControl = viper.GetBool("kubefirst-checks.terraform-apply-users") - if !executionControl { - telemetry.SendEvent(segClient, telemetry.UsersTerraformApplyStarted, "") - - log.Info().Msg("applying users terraform") - - tfEnvs := map[string]string{} - tfEnvs["TF_VAR_email_address"] = "your@email.com" - tfEnvs[fmt.Sprintf("TF_VAR_%s_token", config.GitProvider)] = cGitToken - tfEnvs["TF_VAR_vault_addr"] = k3d.VaultPortForwardURL - tfEnvs["TF_VAR_vault_token"] = vaultRootToken - tfEnvs["VAULT_ADDR"] = k3d.VaultPortForwardURL - tfEnvs["VAULT_TOKEN"] = vaultRootToken - tfEnvs[fmt.Sprintf("%s_TOKEN", strings.ToUpper(config.GitProvider))] = cGitToken - tfEnvs[fmt.Sprintf("%s_OWNER", strings.ToUpper(config.GitProvider))] = cGitOwner - - tfEntrypoint := config.GitopsDir + "/terraform/users" - err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs) - if err != nil { - telemetry.SendEvent(segClient, telemetry.UsersTerraformApplyStarted, err.Error()) - return err - } - log.Info().Msg("executed users terraform successfully") - // progressPrinter.IncrementTracker("step-users", 1) - viper.Set("kubefirst-checks.terraform-apply-users", true) - viper.WriteConfig() - telemetry.SendEvent(segClient, telemetry.UsersTerraformApplyCompleted, "") - progressPrinter.IncrementTracker("creating-users", 1) - } else { - log.Info().Msg("already created users with terraform") - progressPrinter.IncrementTracker("creating-users", 1) - } - - //PostRun string replacement - progressPrinter.AddTracker("wrapping-up", "Wrapping up", 2) - progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) - - err = k3d.PostRunPrepareGitopsRepository(clusterNameFlag, - config.GitopsDir, - &gitopsDirectoryTokens, - ) - if err != nil { - log.Info().Msgf("Error detokenize post run: %s", err) - } - gitopsRepo, err := git.PlainOpen(config.GitopsDir) - if err != nil { - log.Info().Msgf("error opening repo at: %s", config.GitopsDir) - } - //check if file exists before rename - _, err = os.Stat(fmt.Sprintf("%s/terraform/%s/remote-backend.md", config.GitopsDir, config.GitProvider)) - if err == nil { - err = os.Rename(fmt.Sprintf("%s/terraform/%s/remote-backend.md", config.GitopsDir, config.GitProvider), fmt.Sprintf("%s/terraform/%s/remote-backend.tf", config.GitopsDir, config.GitProvider)) - if err != nil { - return err - } - } - viper.Set("kubefirst-checks.post-detokenize", true) - viper.WriteConfig() - - // Final gitops repo commit and push - err = gitClient.Commit(gitopsRepo, "committing initial detokenized gitops-template repo content post run") - if err != nil { - return err - } - err = gitopsRepo.Push(&git.PushOptions{ - RemoteName: config.GitProvider, - Auth: httpAuth, - }) - if err != nil { - log.Info().Msgf("Error pushing repo: %s", err) - } - - progressPrinter.IncrementTracker("wrapping-up", 1) - - // Wait for console Deployment Pods to transition to Running - argoDeployment, err := k8s.ReturnDeploymentObject( - kcfg.Clientset, - "app.kubernetes.io/instance", - "argo", - "argo", - 1200, - ) - if err != nil { - log.Error().Msgf("Error finding argo workflows Deployment: %s", err) - return err - } - _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, argoDeployment, 120) - if err != nil { - log.Error().Msgf("Error waiting for argo workflows Deployment ready state: %s", err) - return err - } - - // Set flags used to track status of active options - helpers.SetClusterStatusFlags(k3d.CloudProvider, config.GitProvider) - - cluster := utilities.CreateClusterRecordFromRaw(useTelemetryFlag, cGitOwner, cGitUser, cGitToken, cGitlabOwnerGroupID, gitopsTemplateURLFlag, gitopsTemplateBranchFlag, catalogApps) - - err = utilities.ExportCluster(cluster, kcfg) - if err != nil { - log.Error().Err(err).Msg("error exporting cluster object") - viper.Set("kubefirst.setup-complete", false) - viper.Set("kubefirst-checks.cluster-install-complete", false) - viper.WriteConfig() - return err - } else { - kubefirstDeployment, err := k8s.ReturnDeploymentObject( - kcfg.Clientset, - "app.kubernetes.io/instance", - "kubefirst", - "kubefirst", - 600, - ) - if err != nil { - log.Error().Msgf("Error finding kubefirst Deployment: %s", err) - return err - } - _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, kubefirstDeployment, 120) - if err != nil { - log.Error().Msgf("Error waiting for kubefirst Deployment ready state: %s", err) - return err - } - progressPrinter.IncrementTracker("wrapping-up", 1) - - err = pkg.OpenBrowser(pkg.KubefirstConsoleLocalURLTLS) - if err != nil { - log.Error().Err(err).Msg("") - } - - // Mark cluster install as complete - telemetry.SendEvent(segClient, telemetry.ClusterInstallCompleted, "") - viper.Set("kubefirst-checks.cluster-install-complete", true) - viper.WriteConfig() - - log.Info().Msg("kubefirst installation complete") - log.Info().Msg("welcome to your new kubefirst platform running in K3d") - time.Sleep(time.Second * 1) // allows progress bars to finish - - reports.LocalHandoffScreenV2(viper.GetString("components.argocd.password"), clusterNameFlag, gitDestDescriptor, cGitOwner, config, ciFlag) - - if ciFlag { - os.Exit(0) - } - } - - return nil -} diff --git a/cmd/k3d/k3d.go b/cmd/k3d/k3d.go new file mode 100644 index 000000000..1289d0794 --- /dev/null +++ b/cmd/k3d/k3d.go @@ -0,0 +1,36 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package k3d + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func NewK3DCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "k3d", + Short: "kubefirst k3d installation", + Long: "kubefirst k3d", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("To learn more about k3d in kubefirst, run:\tkubefirst k3d --help") + + return cmd.Help() + }, + } + + cmd.AddCommand( + NewK3dCreateCommand(), + NewK3dDestroyCommand(), + NewMkCertCommand(), + NewRootCredentialCommand(), + NewVaultUnsealCommand(), + ) + + return cmd +} diff --git a/cmd/k3d/k3d_create.go b/cmd/k3d/k3d_create.go new file mode 100644 index 000000000..f70282ee9 --- /dev/null +++ b/cmd/k3d/k3d_create.go @@ -0,0 +1,1642 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package k3d + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "os" + "path" + "strconv" + "strings" + "time" + + argocdapi "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" + "github.com/atotto/clipboard" + "github.com/go-git/go-git/v5" + githttps "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/kubefirst/kubefirst-api/pkg/handlers" + "github.com/kubefirst/kubefirst-api/pkg/reports" + "github.com/kubefirst/kubefirst-api/pkg/types" + utils "github.com/kubefirst/kubefirst-api/pkg/utils" + "github.com/kubefirst/kubefirst-api/pkg/wrappers" + "github.com/kubefirst/kubefirst/internal/catalog" + "github.com/kubefirst/kubefirst/internal/gitShim" + "github.com/kubefirst/kubefirst/internal/prechecks" + "github.com/kubefirst/kubefirst/internal/segment" + "github.com/kubefirst/kubefirst/internal/utilities" + "github.com/kubefirst/metrics-client/pkg/telemetry" + "github.com/kubefirst/runtime/configs" + "github.com/kubefirst/runtime/pkg" + "github.com/kubefirst/runtime/pkg/argocd" + "github.com/kubefirst/runtime/pkg/gitClient" + "github.com/kubefirst/runtime/pkg/github" + "github.com/kubefirst/runtime/pkg/gitlab" + "github.com/kubefirst/runtime/pkg/helpers" + "github.com/kubefirst/runtime/pkg/k3d" + "github.com/kubefirst/runtime/pkg/k8s" + "github.com/kubefirst/runtime/pkg/progressPrinter" + "github.com/kubefirst/runtime/pkg/services" + "github.com/kubefirst/runtime/pkg/terraform" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DISABLE_PRECHECKS = "KUBEFIRST_DISABLE_PRECHECKS" + GITLAB_TOKEN = "GITLAB_TOKEN" + GITHUB_TOKEN = "GITHUB_TOKEN" + NGROK_AUTH_TOKEN = "NGROK_AUTHTOKEN" +) + +var ( + // required portforwarding ports + portForwardingPorts = []int{8080, 8200, 9000, 9094} + + // Supported git providers + supportedGitProviders = []string{"github", "gitlab"} + + // Supported git providers + supportedGitProtocolOverride = []string{"https", "ssh"} +) + +type createOptions struct { + ci bool + + cloudRegion string + clusterName string + clusterType string + clusterId string + + containerRegistryHost string + + githubUser string + githubOrg string + + gitlabGroup string + gitlabGroupId int + + gitProvider string + gitProtocol string + gitToken string + gitHost string + gitUser string + gitOwner string + gitDescriptor string + + gitopsTemplateURL string + gitopsTemplateBranch string + gitopsRepoURL string + + useTelemetry bool + segClient telemetry.TelemetryEvent + + httpClient *http.Client + + installCatalogApps string + catalogApps []types.GitopsCatalogApp +} + +func defaultCreateOpts() *createOptions { + return &createOptions{ + clusterName: "kubefirst", + clusterType: "mgmt", + + gitProvider: "github", + gitProtocol: "ssh", + + gitopsTemplateURL: "https://github.com/kubefirst/gitops-template.git", + gitopsTemplateBranch: "main", + + useTelemetry: true, + + httpClient: http.DefaultClient, + + segClient: telemetry.TelemetryEvent{}, + } +} + +func NewK3dCreateCommand() *cobra.Command { + opts := defaultCreateOpts() + + createCmd := &cobra.Command{ + Use: "create", + Short: "create the kubefirst platform running in k3d on your localhost", + TraverseChildren: true, + SilenceErrors: true, + PreRunE: func(cmd *cobra.Command, args []string) error { + // example implementation of a pre check suite where we could validate & verify various required settings (Github access, env vars, commands, ...) + fmt.Printf("[PRECHECKS] Running prechecks (skip with %s)\n", DISABLE_PRECHECKS) + + // skip if intented + if _, ok := os.LookupEnv(DISABLE_PRECHECKS); ok { + return nil + } + + // invalid git provider + if !utilities.StringInSlice(opts.gitProvider, supportedGitProviders) { + return fmt.Errorf("\"%s\" is not a supported git provider", opts.gitProvider) + } + + // invalid git protocol + if !utilities.StringInSlice(opts.gitProtocol, supportedGitProtocolOverride) { + return fmt.Errorf("%s is not a support git protocol", opts.gitProtocol) + } + + // github specific prechecks + if strings.ToLower(opts.gitProvider) == "github" { + // enforce for GITHUB_TOKEN + if !prechecks.EnvVarExists(GITHUB_TOKEN) { + return fmt.Errorf("%s not set, but required when using GitHub (see https://docs.kubefirst.io/common/gitAuth?git_provider=github)", GITHUB_TOKEN) + } + + // github.com is available + if err := prechecks.URLIsAvailable("github.com:443"); err != nil { + return fmt.Errorf("github.com is not available: %s, try again", err.Error()) + } + + // check for known hosts of github.com if git provider is github + if err := prechecks.CheckKnownHosts("github.com"); err != nil && strings.ToLower(opts.gitProvider) == "github" { + return err + } + } + + // gitlab specific prechecks + if strings.ToLower(opts.gitProvider) == "gitlab" { + // enforce GITLAB_TOKEN if provider is gitlab + if !prechecks.EnvVarExists(GITLAB_TOKEN) { + return fmt.Errorf("%s not set, but required when using GitLab (see https://docs.kubefirst.io/common/gitAuth)", GITLAB_TOKEN) + } + + // gitlab.com is available + if err := prechecks.URLIsAvailable("gitlab.com:443"); err != nil { + return fmt.Errorf("gitlab.com is not available: %s, try again", err.Error()) + } + + // when gitlab, check for a specified group + if opts.gitlabGroup == "" { + return fmt.Errorf("a gitlab-group is required when using Gitlab") + } + + // check known hosts for gitlab + if err := prechecks.CheckKnownHosts("gitlab.com"); err != nil && strings.ToLower(opts.gitProvider) == "gitlab" { + return err + } + } + + // enforce NGROK_AUTH_TOKEN + if !prechecks.EnvVarExists(NGROK_AUTH_TOKEN) { + return fmt.Errorf("%s not set, but required (see https://docs.kubefirst.io/k3d/quick-start/install#local-atlantis-executions-optional)", NGROK_AUTH_TOKEN) + } + + // docker is installed + if err := prechecks.CommandExists("docker"); err != nil { + return fmt.Errorf("docker is not installed, but is required when using k3d") + } + + // check docker is running + if err := prechecks.CheckDockerIsRunning(); err != nil { + return fmt.Errorf("docker is not running, but is required when using k3d") + } + + // portforwarding ports are available + if err := k8s.CheckForExistingPortForwards(portForwardingPorts...); err != nil { + return fmt.Errorf("%s - this port is required to set up your kubefirst environment - please close any existing port forwards before continuing", err.Error()) + } + + // verify user has specified valid catalog apps to be installed + isValid, apps, err := catalog.ValidateCatalogApps(opts.installCatalogApps) + if !isValid || err != nil { + return err + } + + opts.catalogApps = apps + + // check available disk space + if err := prechecks.CheckAvailableDiskSize(); err != nil { + return fmt.Errorf("error checking available disk size: %s", err.Error()) + } + + fmt.Println("[PRECHECKS] all prechecks passed - continuing with k3d cluster bootstrapping") + + return nil + }, + RunE: opts.runK3d, + } + + // flags + createCmd.Flags().BoolVar(&opts.ci, "ci", opts.ci, "if running kubefirst in ci, set this flag to disable interactive features") + createCmd.Flags().StringVar(&opts.clusterName, "cluster-name", opts.clusterName, "the name of the cluster to create") + createCmd.Flags().StringVar(&opts.clusterType, "cluster-type", opts.clusterType, "the type of cluster to create (i.e. mgmt|workload)") + createCmd.Flags().StringVar(&opts.gitProvider, "git-provider", opts.gitProvider, fmt.Sprintf("the git provider - one of: %s", supportedGitProviders)) + createCmd.Flags().StringVar(&opts.gitProtocol, "git-protocol", opts.gitProtocol, fmt.Sprintf("the git protocol - one of: %s", supportedGitProtocolOverride)) + createCmd.Flags().StringVar(&opts.githubUser, "github-user", opts.githubUser, "the GitHub user for the new gitops and metaphor repositories - this cannot be used with --github-org") + createCmd.Flags().StringVar(&opts.githubOrg, "github-org", opts.githubOrg, "the GitHub organization for the new gitops and metaphor repositories - this cannot be used with --github-user") + createCmd.Flags().StringVar(&opts.gitlabGroup, "gitlab-group", opts.gitlabGroup, "the GitLab group for the new gitops and metaphor projects - required if using gitlab") + createCmd.Flags().StringVar(&opts.gitopsTemplateBranch, "gitops-template-branch", opts.gitopsTemplateBranch, "the branch to clone for the gitops-template repository") + createCmd.Flags().StringVar(&opts.gitopsTemplateURL, "gitops-template-url", opts.gitopsTemplateURL, "the fully qualified url to the gitops-template repository to clone") + createCmd.Flags().StringVar(&opts.installCatalogApps, "install-catalog-apps", opts.installCatalogApps, "comma seperated values of catalog apps to install after provision") + createCmd.Flags().BoolVar(&opts.useTelemetry, "use-telemetry", opts.useTelemetry, "whether to emit telemetry") + + // flag constraints + // only one of --github-user or --github-org can be supplied" + createCmd.MarkFlagsMutuallyExclusive("github-user", "github-org") + + return createCmd +} + +func (o *createOptions) runK3d(cmd *cobra.Command, args []string) error { + // gen clusterID if none exists + o.clusterId = viper.GetString("kubefirst.cluster-id") + if o.clusterId == "" { + o.clusterId = pkg.GenerateClusterID() + + viper.Set("kubefirst.cluster-id", o.clusterId) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + } + + // configure telemetry, AFAIK will the sending of the metrics event only disabled by setting the USE_TELEMETRY env var + // so we set that one manually to allow disabling it via the provided flag (see https://github.com/kubefirst/metrics-client/blob/main/pkg/telemetry/telemetry.go) + if o.useTelemetry { + if err := os.Setenv("USE_TELEMETRY", fmt.Sprintf("%v", o.useTelemetry)); err != nil { + log.Warn().Msgf("error setting USE_TELEMETRY env var: %s", err.Error()) + } + + o.segClient = segment.InitClient(o.clusterId, o.clusterType, o.gitProvider) + } + + // create k1 dir + utilities.CreateDirIfNotExists(path.Join("~", ".k1", o.clusterName)) + + // display logfile + helpers.DisplayLogHints() + + // Store flags for application state maintenance + viper.Set("flags.cluster-name", o.clusterName) + viper.Set("flags.domain-name", k3d.DomainName) + viper.Set("flags.git-provider", o.gitProvider) + viper.Set("flags.git-protocol", o.gitProtocol) + viper.Set("kubefirst.cloud-provider", "k3d") + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + // github specific auth settings + if strings.ToLower(o.gitProvider) == "github" { + if err := o.githubAuth(); err != nil { + return fmt.Errorf("error during github auth: %s", err.Error()) + } + } + + // gitlab specific auth settings + if strings.ToLower(o.gitProvider) == "gitlab" { + if err := o.gitlabAuth(); err != nil { + return fmt.Errorf("error during gitlab auth: %s", err.Error()) + } + } + + // Instantiate K3d config + config := k3d.GetConfig(o.clusterName, o.gitProvider, o.gitOwner, o.gitProtocol) + + // add token to the specified provider + // TODO: should be done in k3d.GetConfig() + if strings.ToLower(o.gitProvider) == "github" { + config.GithubToken = o.gitToken + } + + if strings.ToLower(o.gitProvider) == "gitlab" { + config.GitlabToken = o.gitToken + } + + // TODO: should be done in k3d.GetConfig() + o.gitopsRepoURL = config.DestinationGitopsRepoGitURL + if config.GitProtocol == "https" { + o.gitopsRepoURL = config.DestinationGitopsRepoURL + } + + progressPrinter.AddTracker("preflight-checks", "Running preflight checks", 5) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + progressPrinter.IncrementTracker("preflight-checks", 1) + + // this branch flag value is overridden with a tag when running from a kubefirst binary for version compatibility + // TODO: review + // if strings.Contains(o.gitopsTemplateURL, "https://github.com/kubefirst/gitops-template") && + // o.gitopsTemplateBranch == "" { + // o.gitopsTemplateBranch = configs.K1Version + // } + + log.Info().Msgf("kubefirst version configs.K1Version: %s ", configs.K1Version) + log.Info().Msgf("cloning gitops-template repo url: %s ", o.gitopsTemplateURL) + log.Info().Msgf("cloning gitops-template repo branch: %s ", o.gitopsTemplateBranch) + + log.Info().Msg("checking authentication to required providers") + progressPrinter.IncrementTracker("preflight-checks", 1) + + // Check git credentials + if !viper.GetBool(fmt.Sprintf("kubefirst-checks.%s-credentials", config.GitProvider)) { + if err := o.CheckGitCredentials(config); err != nil { + return fmt.Errorf("error checking git credentials: %s", err.Error()) + } + } + + log.Info().Msg(fmt.Sprintf("completed %s checks - continuing", config.GitProvider)) + + progressPrinter.IncrementTracker("preflight-checks", 1) + + // setup kbot + if !viper.GetBool("kubefirst-checks.kbot-setup") { + if err := o.setupKbot(); err != nil { + return fmt.Errorf("error setting up kbot: %s", err.Error()) + } + } + + log.Info().Msg("setup kbot user - continuing") + + progressPrinter.IncrementTracker("preflight-checks", 1) + + log.Info().Msg("validation and kubefirst cli environment check is complete") + + if e := telemetry.SendEvent(o.segClient, telemetry.InitCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + // download dependencies to `$HOME/.k1/tools` + if !viper.GetBool("kubefirst-checks.tools-downloaded") { + if err := o.downloadTools(config); err != nil { + return fmt.Errorf("error downloading tools: %s", err.Error()) + } + } + + log.Info().Msg("completed download of dependencies to `$HOME/.k1/tools` - continuing") + + progressPrinter.IncrementTracker("preflight-checks", 1) + + //* git clone and detokenize the gitops repository + // todo improve this logic for removing `kubefirst clean` + // if !viper.GetBool("template-repo.gitops.cloned") || viper.GetBool("template-repo.gitops.removed") { + + progressPrinter.IncrementTracker("preflight-checks", 1) + + // prepare repos + progressPrinter.AddTracker("cloning-and-formatting-git-repositories", "Cloning and formatting git repositories", 1) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + if !viper.GetBool("kubefirst-checks.gitops-ready-to-push") { + if err := o.SetupRepos(config); err != nil { + return fmt.Errorf("error setting up repositories: %s", err.Error()) + } + } + + log.Info().Msg("completed gitops repo generation - continuing") + + progressPrinter.IncrementTracker("cloning-and-formatting-git-repositories", 1) + + // run tf apply + progressPrinter.AddTracker("applying-git-terraform", fmt.Sprintf("Applying %s Terraform", config.GitProvider), 1) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + tfEnvs := map[string]string{ + "TF_VAR_kbot_ssh_public_key": viper.GetString("kbot.public-key"), + "TF_CLI_ARGS": "-no-color", // no color in tf output + "AWS_ACCESS_KEY_ID": pkg.MinioDefaultUsername, + "AWS_SECRET_ACCESS_KEY": pkg.MinioDefaultPassword, + "TF_VAR_aws_access_key_id": pkg.MinioDefaultUsername, + "TF_VAR_aws_secret_access_key": pkg.MinioDefaultPassword, + } + + if strings.ToLower(o.gitProvider) == "github" { + tfEnvs["GITHUB_TOKEN"] = o.gitToken + tfEnvs["GITHUB_OWNER"] = o.gitOwner + } + + if strings.ToLower(o.gitProvider) == "gitlab" { + tfEnvs["GITLAB_TOKEN"] = o.gitToken + tfEnvs["GITLAB_OWNER"] = o.gitOwner + tfEnvs["TF_VAR_owner_group_id"] = strconv.Itoa(o.gitlabGroupId) + } + + // apply terraform to create teams and repositories on the specified provider + if !viper.GetBool(fmt.Sprintf("kubefirst-checks.terraform-apply-%s", o.gitProvider)) { + if err := o.terraformApply(tfEnvs, config); err != nil { + return fmt.Errorf("error applying terraform: %s", err.Error()) + } + } + + // push detokenized gitops-template repository content to new remote + progressPrinter.AddTracker("pushing-gitops-repos-upstream", "Pushing git repositories", 1) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + log.Info().Msgf("referencing gitops repository: %s", config.DestinationGitopsRepoGitURL) + log.Info().Msgf("referencing metaphor repository: %s", config.DestinationMetaphorRepoURL) + + // push gitops and metaphor repo + if !viper.GetBool("kubefirst-checks.gitops-repo-pushed") { + if err := o.PushRepo(config.GitopsDir, config); err != nil { + return fmt.Errorf("error pushing gitops repo: %s", err.Error()) + } + + if err := o.PushRepo(config.MetaphorDir, config); err != nil { + return fmt.Errorf("error pushing metaphor repo: %s", err.Error()) + } + } + + log.Info().Msg("pushed detokenized gitops repository content") + + progressPrinter.IncrementTracker("pushing-gitops-repos-upstream", 1) + + // create k3d cluster + progressPrinter.AddTracker("creating-k3d-cluster", "Creating k3d cluster", 1) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + if !viper.GetBool("kubefirst-checks.create-k3d-cluster") { + if err := o.createK3dCluster(config); err != nil { + return fmt.Errorf("error creating k3d cluster: %s", err.Error()) + } + } + + log.Info().Msg("k3d cluster created") + progressPrinter.IncrementTracker("creating-k3d-cluster", 1) + + // create k8s resoures + progressPrinter.AddTracker("bootstrapping-kubernetes-resources", "Bootstrapping Kubernetes resources", 2) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) + + if !viper.GetBool("kubefirst-checks.k8s-secrets-created") { + if err := o.createK3dAtlantisWebhookSecret(kcfg, config); err != nil { + return fmt.Errorf("error creating k3d secrets: %s", err.Error()) + } + } + + log.Info().Msg("added secrets to k3d cluster") + + progressPrinter.IncrementTracker("bootstrapping-kubernetes-resources", 1) + + // //* check for ssl restore + // log.Info().Msg("checking for tls secrets to restore") + // secretsFilesToRestore, err := ioutil.ReadDir(config.SSLBackupDir + "/secrets") + // if err != nil { + // log.Info().Msgf("%s", err) + // } + // if len(secretsFilesToRestore) != 0 { + // // todo would like these but requires CRD's and is not currently supported + // // add crds ( use execShellReturnErrors? ) + // // https://raw.githubusercontent.com/cert-manager/cert-manager/v1.11.0/deploy/crds/crd-clusterissuers.yaml + // // https://raw.githubusercontent.com/cert-manager/cert-manager/v1.11.0/deploy/crds/crd-certificates.yaml + // // add certificates, and clusterissuers + // log.Info().Msgf("found %d tls secrets to restore", len(secretsFilesToRestore)) + // ssl.Restore(config.SSLBackupDir, k3d.DomainName, config.Kubeconfig) + // } else { + // log.Info().Msg("no files found in secrets directory, continuing") + // } + + progressPrinter.IncrementTracker("bootstrapping-kubernetes-resources", 1) + progressPrinter.AddTracker("verifying-k3d-cluster-readiness", "Verifying Kubernetes cluster is ready", 3) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + // traefik + traefikDeployment, err := k8s.ReturnDeploymentObject(kcfg.Clientset, "app.kubernetes.io/name", "traefik", "kube-system", 240) + if err != nil { + log.Error().Msgf("error finding traefik deployment: %s", err.Error()) + + return err + } + + if _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, traefikDeployment, 240); err != nil { + log.Error().Msgf("error waiting for traefik deployment ready state: %s", err.Error()) + + return err + } + + progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) + + // metrics-server + metricsServerDeployment, err := k8s.ReturnDeploymentObject(kcfg.Clientset, "k8s-app", "metrics-server", "kube-system", 240) + if err != nil { + log.Error().Msgf("error finding metrics-server deployment: %s", err.Error()) + + return err + } + + if _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, metricsServerDeployment, 240); err != nil { + log.Error().Msgf("error waiting for metrics-server deployment ready state: %s", err.Error()) + + return err + } + + progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) + + time.Sleep(time.Second * 20) + + progressPrinter.IncrementTracker("verifying-k3d-cluster-readiness", 1) + + // install argocd + progressPrinter.AddTracker("installing-argo-cd", "Installing and configuring Argo CD", 3) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + if !viper.GetBool("kubefirst-checks.argocd-install") { + if err := o.installArgoCD(kcfg); err != nil { + return fmt.Errorf("error installing ArgoCD: %s", err.Error()) + } + } + + log.Info().Msg("argo cd installed - continuing") + + progressPrinter.IncrementTracker("installing-argo-cd", 1) + + // Wait for ArgoCD to be ready + if _, err := k8s.VerifyArgoCDReadiness(kcfg.Clientset, true, 300); err != nil { + log.Error().Msgf("error waiting for ArgoCD to become ready: %s", err.Error()) + + return err + } + + //* argocd pods are ready, get and set credentials + if !viper.GetBool("kubefirst-checks.argocd-credentials-set") { + if err := o.argoCDCredentials(kcfg); err != nil { + return fmt.Errorf("error getting argocd credentials: %s", err.Error()) + } + } + + log.Info().Msg("argo credentials set, continuing") + + progressPrinter.IncrementTracker("installing-argo-cd", 1) + + //* argocd sync registry and start sync waves + if !viper.GetBool("kubefirst-checks.argocd-create-registry") { + if err := o.argoCDRegistry(kcfg); err != nil { + return fmt.Errorf("error setting up argocd regigstry: %s", err.Error()) + } + } + + log.Info().Msg("argocd registry create done, continuing") + + progressPrinter.IncrementTracker("installing-argo-cd", 1) + + // Wait for Vault StatefulSet Pods to transition to Running + progressPrinter.AddTracker("configuring-vault", "Configuring Vault", 4) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + vaultStatefulSet, err := k8s.ReturnStatefulSetObject(kcfg.Clientset, "app.kubernetes.io/instance", "vault", "vault", 120) + if err != nil { + log.Error().Msgf("Error finding Vault StatefulSet: %s", err.Error()) + + return err + } + + if _, err := k8s.WaitForStatefulSetReady(kcfg.Clientset, vaultStatefulSet, 120, true); err != nil { + log.Error().Msgf("Error waiting for Vault StatefulSet ready state: %s", err.Error()) + + return err + } + + progressPrinter.IncrementTracker("configuring-vault", 1) + + // Init and unseal vault + // We need to wait before we try to run any of these commands or there may be + // unexpected timeouts + time.Sleep(time.Second * 10) + + progressPrinter.IncrementTracker("configuring-vault", 1) + + if !viper.GetBool("kubefirst-checks.vault-initialized") { + if err := o.vaultInitialize(kcfg); err != nil { + return fmt.Errorf("error initializing Vault: %s", err.Error()) + } + } + + log.Info().Msg("vault is initialized") + + progressPrinter.IncrementTracker("configuring-vault", 1) + + if err := o.setupMinio(kcfg, config); err != nil { + return fmt.Errorf("error setting up minio: %s", err.Error()) + } + + // configure vault with terraform + progressPrinter.IncrementTracker("configuring-vault", 1) + + var vaultRootToken string + + if !viper.GetBool("kubefirst-checks.terraform-apply-vault") { + vaultRootToken, err = o.vaultConfigure(kcfg, config) + if err != nil { + return fmt.Errorf("error configuring Vault with terraform: %s", err.Error()) + } + } + + log.Info().Msg("executed vault terraform") + + progressPrinter.IncrementTracker("configuring-vault", 1) + + // creating users + progressPrinter.AddTracker("creating-users", "Creating users", 1) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + if !viper.GetBool("kubefirst-checks.terraform-apply-users") { + if err := o.vaultUsers(vaultRootToken, config); err != nil { + return fmt.Errorf("error creating users: %s", err.Error()) + } + } + + log.Info().Msg("created users with terraform") + + progressPrinter.IncrementTracker("creating-users", 1) + + // wrap up + progressPrinter.AddTracker("wrapping-up", "Wrapping up", 2) + progressPrinter.SetupProgress(progressPrinter.TotalOfTrackers(), false) + + if err := o.wrapUp(config); err != nil { + return fmt.Errorf("error commiting dekonzied files: %s", err.Error()) + } + + progressPrinter.IncrementTracker("wrapping-up", 1) + + // Wait for console Deployment Pods to transition to Running + argoDeployment, err := k8s.ReturnDeploymentObject(kcfg.Clientset, "app.kubernetes.io/instance", "argo", "argo", 1200) + if err != nil { + log.Error().Msgf("Error finding argo workflows Deployment: %s", err.Error()) + + return err + } + + if _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, argoDeployment, 120); err != nil { + log.Error().Msgf("Error waiting for argo workflows Deployment ready state: %s", err.Error()) + + return err + } + + // Set flags used to track status of active options + helpers.SetClusterStatusFlags(k3d.CloudProvider, config.GitProvider) + + cluster := utilities.CreateClusterRecordFromRaw(o.useTelemetry, o.gitOwner, o.gitUser, o.gitToken, o.gitlabGroupId, o.gitopsTemplateURL, o.gitopsTemplateBranch, o.catalogApps) + + if err := utilities.ExportCluster(cluster, kcfg); err != nil { + log.Error().Err(err).Msg("error exporting cluster object") + + viper.Set("kubefirst.setup-complete", false) + viper.Set("kubefirst-checks.cluster-install-complete", false) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return err + } + + kubefirstDeployment, err := k8s.ReturnDeploymentObject(kcfg.Clientset, "app.kubernetes.io/instance", "kubefirst", "kubefirst", 600) + if err != nil { + log.Error().Msgf("Error finding kubefirst Deployment: %s", err.Error()) + + return err + } + + if _, err = k8s.WaitForDeploymentReady(kcfg.Clientset, kubefirstDeployment, 120); err != nil { + log.Error().Msgf("Error waiting for kubefirst Deployment ready state: %s", err.Error()) + + return err + } + + progressPrinter.IncrementTracker("wrapping-up", 1) + + if pkg.OpenBrowser(pkg.KubefirstConsoleLocalURLTLS); err != nil { + log.Error().Err(err).Msg("") + } + + // Mark cluster install as complete + if e := telemetry.SendEvent(o.segClient, telemetry.ClusterInstallCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + viper.Set("kubefirst-checks.cluster-install-complete", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + log.Info().Msg("kubefirst installation complete") + log.Info().Msg("welcome to your new kubefirst platform running in K3d") + + time.Sleep(time.Second * 1) // allows progress bars to finish + + reports.LocalHandoffScreenV2(viper.GetString("components.argocd.password"), o.clusterName, o.gitDescriptor, o.gitOwner, config, o.ci) + + if o.ci { + os.Exit(0) + } + + return nil +} + +func (o *createOptions) githubAuth() error { + o.gitHost = k3d.GithubHost + o.containerRegistryHost = "ghcr.io" + + // Attempt to retrieve session-scoped token for GitHub user + gitHubService := services.NewGitHubService(o.httpClient) + gitHubHandler := handlers.NewGitHubHandler(gitHubService) + + ghToken := utilities.EnvOrDefault(GITHUB_TOKEN, viper.GetString("github.session_token")) + gitHubAccessToken, err := wrappers.AuthenticateGitHubUserWrapper(ghToken, gitHubHandler) + if err != nil { + log.Warn().Msgf(err.Error()) + } + + o.gitToken = gitHubAccessToken + + if err := github.VerifyTokenPermissions(o.gitToken); err != nil { + return err + } + + log.Info().Msg("verifying github authentication") + + githubUser, err := gitHubHandler.GetGitHubUser(o.gitToken) + if err != nil { + return err + } + + // Owner is either an organization or a personal user's GitHub handle + if o.githubOrg != "" { + o.gitOwner = o.githubOrg + o.gitDescriptor = "Organization" + } else if o.githubUser != "" { + o.gitOwner = githubUser + o.gitDescriptor = "User" + } else if o.githubOrg == "" && o.githubUser == "" { + o.gitOwner = githubUser + } + + o.githubUser = githubUser + o.gitUser = o.githubUser + + viper.Set("flags.github-owner", o.gitOwner) + viper.Set("github.session_token", o.gitToken) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return nil +} + +func (o *createOptions) gitlabAuth() error { + o.gitToken = os.Getenv(GITLAB_TOKEN) + + // Verify token scopes + if err := gitlab.VerifyTokenPermissions(o.gitToken); err != nil { + return err + } + + gitlabClient, err := gitlab.NewGitLabClient(o.gitToken, o.gitlabGroup) + if err != nil { + return err + } + + o.gitHost = k3d.GitlabHost + o.gitOwner = gitlabClient.ParentGroupPath + o.gitlabGroupId = gitlabClient.ParentGroupID + o.gitDescriptor = "Group" + + log.Info().Msgf("set gitlab owner to %s", o.gitOwner) + + // Get authenticated user's name + user, _, err := gitlabClient.Client.Users.CurrentUser() + if err != nil { + return fmt.Errorf("unable to get authenticated user info - please make sure GITLAB_TOKEN env var is set %s", err.Error()) + } + + o.gitUser = user.Username + o.containerRegistryHost = "registry.gitlab.com" + + viper.Set("flags.gitlab-owner", o.gitlabGroup) + viper.Set("flags.gitlab-owner-group-id", o.gitlabGroupId) + viper.Set("gitlab.session_token", o.gitToken) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return nil +} + +func (o *createOptions) CheckGitCredentials(config *k3d.K3dConfig) error { + newRepositoryNames := []string{"gitops", "metaphor"} + newTeamNames := []string{"admins", "developers"} + + if e := telemetry.SendEvent(o.segClient, telemetry.GitCredentialsCheckStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + if len(o.gitToken) == 0 { + msg := fmt.Sprintf("please set a %s_TOKEN environment variable to continue", strings.ToUpper(config.GitProvider)) + + if e := telemetry.SendEvent(o.segClient, telemetry.GitCredentialsCheckFailed, msg); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + } + + initGitParameters := gitShim.GitInitParameters{ + GitProvider: o.gitProvider, + GitToken: o.gitToken, + GitOwner: o.gitOwner, + Repositories: newRepositoryNames, + Teams: newTeamNames, + } + + if err := gitShim.InitializeGitProvider(&initGitParameters); err != nil { + return err + } + + viper.Set(fmt.Sprintf("kubefirst-checks.%s-credentials", config.GitProvider), true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.GitCredentialsCheckCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) setupKbot() error { + if e := telemetry.SendEvent(o.segClient, telemetry.KbotSetupStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + log.Info().Msg("creating an ssh key pair for your new cloud infrastructure") + + sshPrivateKey, sshPublicKey, err := utils.CreateSshKeyPair() + if err != nil { + log.Warn().Msgf("generate public keys failed: %s\n", err.Error()) + + if e := telemetry.SendEvent(o.segClient, telemetry.KbotSetupFailed, err.Error()); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + } + + log.Info().Msg("ssh key pair creation complete") + + viper.Set("kbot.private-key", sshPrivateKey) + viper.Set("kbot.public-key", sshPublicKey) + viper.Set("kbot.username", "kbot") + viper.Set("kubefirst-checks.kbot-setup", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.KbotSetupCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) downloadTools(config *k3d.K3dConfig) error { + log.Info().Msg("installing kubefirst dependencies") + + if err := k3d.DownloadTools(o.clusterName, config.GitProvider, o.gitOwner, config.ToolsDir, config.GitProtocol); err != nil { + return err + } + + log.Info().Msg("download dependencies `$HOME/.k1/tools` complete") + + viper.Set("kubefirst-checks.tools-downloaded", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return nil +} + +func (o *createOptions) terraformApply(tfEnvs map[string]string, config *k3d.K3dConfig) error { + if e := telemetry.SendEvent(o.segClient, telemetry.GitTerraformApplyStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + log.Info().Msgf("Creating %s resources with Terraform", o.gitProvider) + + tfEntrypoint := path.Join(config.GitopsDir, "terraform", o.gitProvider) + + // Erase public key to prevent it from being created if the git protocol argument is set to htps + if config.GitProtocol == "https" { + tfEnvs["TF_VAR_kbot_ssh_public_key"] = "" + } + + if err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs); err != nil { + if e := telemetry.SendEvent(o.segClient, telemetry.GitTerraformApplyFailed, fmt.Sprintf("error creating %s resources with terraform %s: %s", o.gitProvider, tfEntrypoint, err)); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + } + + log.Info().Msgf("created git repositories for %s.com/%s", o.gitProvider, config.MetaphorDir) + log.Info().Msgf("created git repositories for %s.com/%s", o.gitProvider, config.GitopsDir) + + viper.Set(fmt.Sprintf("kubefirst-checks.terraform-apply-%s", o.gitProvider), true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.GitTerraformApplyCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) k3dGitOpsDirectoryValues(config *k3d.K3dConfig) *k3d.GitopsDirectoryValues { + return &k3d.GitopsDirectoryValues{ + GithubOwner: o.gitOwner, + GithubUser: o.gitUser, + GitlabOwner: o.gitOwner, + GitlabOwnerGroupID: o.gitlabGroupId, + GitlabUser: o.gitUser, + DomainName: k3d.DomainName, + AtlantisAllowList: fmt.Sprintf("%s/%s/*", o.gitHost, o.gitOwner), + AlertsEmail: "REMOVE_THIS_VALUE", + ClusterName: o.clusterName, + ClusterType: o.clusterType, + GithubHost: k3d.GithubHost, + GitlabHost: k3d.GitlabHost, + ArgoWorkflowsIngressURL: fmt.Sprintf("https://argo.%s", k3d.DomainName), + VaultIngressURL: fmt.Sprintf("https://vault.%s", k3d.DomainName), + ArgocdIngressURL: fmt.Sprintf("https://argocd.%s", k3d.DomainName), + AtlantisIngressURL: fmt.Sprintf("https://atlantis.%s", k3d.DomainName), + MetaphorDevelopmentIngressURL: fmt.Sprintf("https://metaphor-development.%s", k3d.DomainName), + MetaphorStagingIngressURL: fmt.Sprintf("https://metaphor-staging.%s", k3d.DomainName), + MetaphorProductionIngressURL: fmt.Sprintf("https://metaphor-production.%s", k3d.DomainName), + KubefirstVersion: configs.K1Version, + KubefirstTeam: utilities.EnvOrDefault("KUBEFIRST_TEAM", "false"), + KubeconfigPath: config.Kubeconfig, + GitopsRepoURL: o.gitopsRepoURL, + GitProvider: config.GitProvider, + ClusterId: o.clusterId, + CloudProvider: k3d.CloudProvider, + UseTelemetry: fmt.Sprintf("%v", o.useTelemetry), + } +} + +func (o *createOptions) k3dMetaphorTokenValues() *k3d.MetaphorTokenValues { + return &k3d.MetaphorTokenValues{ + ClusterName: o.clusterName, + CloudRegion: o.cloudRegion, + ContainerRegistryURL: fmt.Sprintf("%s/%s/metaphor", o.containerRegistryHost, o.gitOwner), + DomainName: k3d.DomainName, + MetaphorDevelopmentIngressURL: fmt.Sprintf("metaphor-development.%s", k3d.DomainName), + MetaphorStagingIngressURL: fmt.Sprintf("metaphor-staging.%s", k3d.DomainName), + MetaphorProductionIngressURL: fmt.Sprintf("metaphor-production.%s", k3d.DomainName), + } +} + +func (o *createOptions) SetupRepos(config *k3d.K3dConfig) error { + var removeAtlantis bool + + log.Info().Msg("generating your new gitops repository") + + if viper.GetString("secrets.atlantis-ngrok-authtoken") == "" { + removeAtlantis = true + } + + if err := k3d.PrepareGitRepositories( + config.GitProvider, + o.clusterName, + o.clusterType, + config.DestinationGitopsRepoURL, // default to https for git interactions when creating remotes + config.GitopsDir, + o.gitopsTemplateBranch, + o.gitopsTemplateURL, + config.DestinationMetaphorRepoURL, // default to https for git interactions when creating remotes + config.K1Dir, + o.k3dGitOpsDirectoryValues(config), + config.MetaphorDir, + o.k3dMetaphorTokenValues(), + o.gitProtocol, + removeAtlantis, + ); err != nil { + return err + } + + // todo emit init telemetry end + viper.Set("kubefirst-checks.gitops-ready-to-push", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return nil +} + +func (o *createOptions) PushRepo(repo string, config *k3d.K3dConfig) error { + httpAuth := &githttps.BasicAuth{ + Username: o.gitUser, + Password: o.gitToken, + } + + if e := telemetry.SendEvent(o.segClient, telemetry.GitopsRepoPushStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + r, err := git.PlainOpen(repo) + if err != nil { + log.Info().Msgf("error opening repo at: %s", repo) + } + + if strings.ToLower(o.gitProvider) == "gitlab" { + if err := utils.EvalSSHKey(&types.EvalSSHKeyRequest{ + GitProvider: o.gitProvider, + GitlabGroupFlag: o.gitlabGroup, + GitToken: o.gitToken, + }); err != nil { + return err + } + } + + if err := r.Push(&git.PushOptions{ + RemoteName: config.GitProvider, + Auth: httpAuth, + }); err != nil { + msg := fmt.Sprintf("error pushing detokenized gitops repository to remote %s: %s", config.DestinationGitopsRepoGitURL, err) + + if e := telemetry.SendEvent(o.segClient, telemetry.GitopsRepoPushFailed, msg); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + if !strings.Contains(msg, "already up-to-date") { + log.Panic().Msg(msg) + } + } + + log.Info().Msgf("successfully pushed %s repository to https://%s/%s", repo, o.gitHost, o.gitOwner) + + // todo delete the local gitops repo and re-clone it + // todo that way we can stop worrying about which origin we're going to push to + viper.Set("kubefirst-checks.gitops-repo-pushed", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.GitopsRepoPushCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) createK3dCluster(config *k3d.K3dConfig) error { + log.Info().Msg("Creating k3d cluster") + + if e := telemetry.SendEvent(o.segClient, telemetry.CloudTerraformApplyStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + if err := k3d.ClusterCreate(o.clusterName, config.K1Dir, config.K3dClient, config.Kubeconfig); err != nil { + msg := fmt.Sprintf("error creating k3d resources with k3d client %s: %s", config.K3dClient, err) + + viper.Set("kubefirst-checks.create-k3d-cluster-failed", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.CloudTerraformApplyFailed, msg); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return fmt.Errorf(msg) + } + + viper.Set("kubefirst-checks.create-k3d-cluster", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.CloudTerraformApplyCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) createK3dAtlantisWebhookSecret(kcfg *k8s.KubernetesClient, config *k3d.K3dConfig) error { + if err := k3d.GenerateTLSSecrets(kcfg.Clientset, *config); err != nil { + return err + } + // gen & store atlantis secret if none exists + atlantisWebhookSecret := viper.GetString("secrets.atlantis-webhook") + if atlantisWebhookSecret == "" { + atlantisWebhookSecret = pkg.Random(20) + viper.Set("secrets.atlantis-webhook", atlantisWebhookSecret) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + } + + if err := k3d.AddK3DSecrets( + atlantisWebhookSecret, + viper.GetString("kbot.public-key"), + o.gitopsRepoURL, + viper.GetString("kbot.private-key"), + config.GitProvider, + o.gitUser, + o.gitOwner, + config.Kubeconfig, + o.gitToken, + ); err != nil { + log.Info().Msg("Error adding kubernetes secrets for bootstrap") + + return err + } + + viper.Set("kubefirst-checks.k8s-secrets-created", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + return nil +} + +func (o *createOptions) installArgoCD(kcfg *k8s.KubernetesClient) error { + argoCDInstallPath := fmt.Sprintf("github.com:kubefirst/manifests/argocd/k3d?ref=%s", pkg.KubefirstManifestRepoRef) + + if e := telemetry.SendEvent(o.segClient, telemetry.ArgoCDInstallStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + log.Info().Msgf("installing argocd") + + // Build and apply manifests + yamlData, err := kcfg.KustomizeBuild(argoCDInstallPath) + if err != nil { + return err + } + + output, err := kcfg.SplitYAMLFile(yamlData) + if err != nil { + return err + } + + if kcfg.ApplyObjects("", output) != nil { + if e := telemetry.SendEvent(o.segClient, telemetry.ArgoCDInstallFailed, err.Error()); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return err + } + + viper.Set("kubefirst-checks.argocd-install", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.ArgoCDInstallCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) argoCDCredentials(kcfg *k8s.KubernetesClient) error { + var argocdPassword, argoCDToken string + + log.Info().Msg("Setting argocd username and password credentials") + + argocd.ArgocdSecretClient = kcfg.Clientset.CoreV1().Secrets("argocd") + + argocdPassword = k8s.GetSecretValue(argocd.ArgocdSecretClient, "argocd-initial-admin-secret", "password") + if argocdPassword == "" { + log.Info().Msg("argocd password not found in secret") + + return fmt.Errorf("could not find argocd secret") + } + + viper.Set("components.argocd.password", argocdPassword) + viper.Set("components.argocd.username", "admin") + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + log.Info().Msg("argocd username and password credentials set successfully") + log.Info().Msg("Getting an argocd auth token") + + // only the host, not the protocol + argoCDURL := k3d.ArgocdURL + + if helpers.TestEndpointTLS(strings.Replace(k3d.ArgocdURL, "https://", "", 1)) != nil { + argoCDStopChannel := make(chan struct{}, 1) + + log.Info().Msgf("argocd not available via https, using http") + + defer func() { + close(argoCDStopChannel) + }() + + k8s.OpenPortForwardPodWrapper(kcfg.Clientset, kcfg.RestConfig, "argocd-server", "argocd", 8080, 8080, argoCDStopChannel) + + argoCDURL = strings.Replace(k3d.ArgocdURL, "https://", "http://", 1) + ":8080" + } + + argoCDToken, err := argocd.GetArgocdTokenV2(o.httpClient, argoCDURL, "admin", argocdPassword) + if err != nil { + return err + } + + log.Info().Msg("argocd admin auth token set") + + viper.Set("components.argocd.auth-token", argoCDToken) + viper.Set("kubefirst-checks.argocd-credentials-set", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if configs.K1Version == "development" { + if err := clipboard.WriteAll(argocdPassword); err != nil { + log.Error().Err(err).Msg("error copying argocd password to clipboard") + } + + if _, ok := os.LookupEnv("SKIP_ARGOCD_LAUNCH"); !ok || !o.ci { + if pkg.OpenBrowser(pkg.ArgoCDLocalURLTLS) != nil { + log.Error().Err(err).Msg("error opening argocd in browser") + } + } + } + + return nil +} + +func (o *createOptions) argoCDRegistry(kcfg *k8s.KubernetesClient) error { + if e := telemetry.SendEvent(o.segClient, telemetry.CreateRegistryStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + argocdClient, err := argocdapi.NewForConfig(kcfg.RestConfig) + if err != nil { + return err + } + + log.Info().Msg("applying the registry application to argocd") + + registryApplicationObject := argocd.GetArgoCDApplicationObject(o.gitopsRepoURL, fmt.Sprintf("registry/%s", o.clusterName)) + + _, _ = argocdClient.ArgoprojV1alpha1().Applications("argocd").Create(context.Background(), registryApplicationObject, metav1.CreateOptions{}) + + viper.Set("kubefirst-checks.argocd-create-registry", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.CreateRegistryCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) vaultInitialize(kcfg *k8s.KubernetesClient) error { + if e := telemetry.SendEvent(o.segClient, telemetry.VaultInitializationStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + // Initialize and unseal Vault + vaultHandlerPath := "github.com:kubefirst/manifests.git/vault-handler/replicas-1" + + // Build and apply manifests + yamlData, err := kcfg.KustomizeBuild(vaultHandlerPath) + if err != nil { + return err + } + + output, err := kcfg.SplitYAMLFile(yamlData) + if err != nil { + return err + } + + if err := kcfg.ApplyObjects("", output); err != nil { + return err + } + + // Wait for the Job to finish + job, err := k8s.ReturnJobObject(kcfg.Clientset, "vault", "vault-handler") + if err != nil { + return err + } + + _, err = k8s.WaitForJobComplete(kcfg.Clientset, job, 240) + if err != nil { + msg := fmt.Sprintf("could not run vault unseal job: %s", err.Error()) + + if e := telemetry.SendEvent(o.segClient, telemetry.VaultInitializationFailed, msg); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + log.Fatal().Msg(msg) + } + + viper.Set("kubefirst-checks.vault-initialized", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.VaultInitializationCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) containerRegistryAuth(kcfg *k8s.KubernetesClient) (string, error) { + containerRegistryAuth := gitShim.ContainerRegistryAuth{ + GitProvider: o.gitProvider, + GitUser: o.gitUser, + GitToken: o.gitToken, + GitlabGroupFlag: o.gitlabGroup, + GithubOwner: o.gitOwner, + ContainerRegistryHost: o.containerRegistryHost, + Clientset: kcfg.Clientset, + } + + containerRegistryAuthToken, err := gitShim.CreateContainerRegistrySecret(&containerRegistryAuth) + if err != nil { + return "", err + } + + return containerRegistryAuthToken, nil +} + +func (o *createOptions) vaultConfigure(kcfg *k8s.KubernetesClient, config *k3d.K3dConfig) (string, error) { + tfEnvs := map[string]string{} + var usernamePasswordString, base64DockerAuth string + var vaultRootToken string + + // get & store ngrok token + atlantisNgrokAuthtoken := viper.GetString("secrets.atlantis-ngrok-authtoken") + if atlantisNgrokAuthtoken == "" { + atlantisNgrokAuthtoken = os.Getenv("NGROK_AUTHTOKEN") + viper.Set("secrets.atlantis-ngrok-authtoken", atlantisNgrokAuthtoken) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + } + + // port forward + vaultStopChannel := make(chan struct{}, 1) + + defer func() { + close(vaultStopChannel) + }() + + k8s.OpenPortForwardPodWrapper(kcfg.Clientset, kcfg.RestConfig, "vault-0", "vault", 8200, 8200, vaultStopChannel) + + // container registry auth + registryAuthToken, err := o.containerRegistryAuth(kcfg) + if err != nil { + return "", fmt.Errorf("error authentication to container registry: %s", err.Error()) + } + + // Retrieve root token from init step + secData, err := k8s.ReadSecretV2(kcfg.Clientset, "vault", "vault-unseal-secret") + if err != nil { + return "", err + } + + vaultRootToken = secData["root-token"] + + // Parse k3d api endpoint from kubeconfig + // In this case, we need to get the IP of the in-cluster API server to provide to Vault + // to work with Kubernetes auth + kubernetesInClusterAPIService, err := k8s.ReadService(config.Kubeconfig, "default", "kubernetes") + if err != nil { + log.Error().Msgf("error looking up kubernetes api server service: %s", err.Error()) + + return "", err + } + + if err := helpers.TestEndpointTLS(strings.Replace(k3d.VaultURL, "https://", "", 1)); err != nil { + return "", fmt.Errorf("unable to reach vault over https - this is likely due to the mkcert certificate store missing. please install it via `%s -install`", config.MkCertClient) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.VaultTerraformApplyStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + usernamePasswordString = fmt.Sprintf("%s:%s", o.gitUser, o.gitToken) + base64DockerAuth = base64.StdEncoding.EncodeToString([]byte(usernamePasswordString)) + + if config.GitProvider == "gitlab" { + usernamePasswordString = fmt.Sprintf("%s:%s", "container-registry-auth", registryAuthToken) + base64DockerAuth = base64.StdEncoding.EncodeToString([]byte(usernamePasswordString)) + + tfEnvs["TF_VAR_container_registry_auth"] = registryAuthToken + tfEnvs["TF_VAR_owner_group_id"] = strconv.Itoa(o.gitlabGroupId) + } + + log.Info().Msg("configuring vault with terraform") + + tfEnvs["TF_CLI_ARGS"] = "-no-color" // no color in tf output + tfEnvs["TF_VAR_email_address"] = "your@email.com" + tfEnvs[fmt.Sprintf("TF_VAR_%s_token", config.GitProvider)] = o.gitToken + tfEnvs[fmt.Sprintf("TF_VAR_%s_user", config.GitProvider)] = o.gitUser + tfEnvs["TF_VAR_vault_addr"] = k3d.VaultPortForwardURL + tfEnvs["TF_VAR_b64_docker_auth"] = base64DockerAuth + tfEnvs["TF_VAR_vault_token"] = vaultRootToken + tfEnvs["VAULT_ADDR"] = k3d.VaultPortForwardURL + tfEnvs["VAULT_TOKEN"] = vaultRootToken + tfEnvs["TF_VAR_atlantis_repo_webhook_secret"] = viper.GetString("secrets.atlantis-webhook") + tfEnvs["TF_VAR_kbot_ssh_private_key"] = viper.GetString("kbot.private-key") + tfEnvs["TF_VAR_kbot_ssh_public_key"] = viper.GetString("kbot.public-key") + tfEnvs["TF_VAR_kubernetes_api_endpoint"] = fmt.Sprintf("https://%s", kubernetesInClusterAPIService.Spec.ClusterIP) + tfEnvs[fmt.Sprintf("%s_OWNER", strings.ToUpper(config.GitProvider))] = viper.GetString(fmt.Sprintf("flags.%s-owner", config.GitProvider)) + tfEnvs["AWS_ACCESS_KEY_ID"] = pkg.MinioDefaultUsername + tfEnvs["AWS_SECRET_ACCESS_KEY"] = pkg.MinioDefaultPassword + tfEnvs["TF_VAR_aws_access_key_id"] = pkg.MinioDefaultUsername + tfEnvs["TF_VAR_aws_secret_access_key"] = pkg.MinioDefaultPassword + tfEnvs["TF_VAR_ngrok_authtoken"] = viper.GetString("secrets.atlantis-ngrok-authtoken") + // tfEnvs["TF_LOG"] = "DEBUG" + + tfEntrypoint := config.GitopsDir + "/terraform/vault" + if err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs); err != nil { + if e := telemetry.SendEvent(o.segClient, telemetry.VaultTerraformApplyStarted, err.Error()); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return "", err + } + + log.Info().Msg("vault terraform executed successfully") + + viper.Set("kubefirst-checks.terraform-apply-vault", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.VaultTerraformApplyCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return vaultRootToken, nil +} + +func (o *createOptions) vaultUsers(vaultRootToken string, config *k3d.K3dConfig) error { + if e := telemetry.SendEvent(o.segClient, telemetry.UsersTerraformApplyStarted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + log.Info().Msg("applying users terraform") + + tfEnvs := map[string]string{ + "TF_VAR_email_address": "your@email.com", + "TF_CLI_ARGS": "-no-color", // no color in tf output + fmt.Sprintf("TF_VAR_%s_token", config.GitProvider): o.gitToken, + "TF_VAR_vault_addr": k3d.VaultPortForwardURL, + "TF_VAR_vault_token": vaultRootToken, + "VAULT_ADDR": k3d.VaultPortForwardURL, + "VAULT_TOKEN": vaultRootToken, + fmt.Sprintf("%s_TOKEN", strings.ToUpper(config.GitProvider)): o.gitToken, + fmt.Sprintf("%s_OWNER", strings.ToUpper(config.GitProvider)): o.gitOwner, + } + + tfEntrypoint := config.GitopsDir + "/terraform/users" + if err := terraform.InitApplyAutoApprove(config.TerraformClient, tfEntrypoint, tfEnvs); err != nil { + if e := telemetry.SendEvent(o.segClient, telemetry.UsersTerraformApplyStarted, err.Error()); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return err + } + + viper.Set("kubefirst-checks.terraform-apply-users", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + if e := telemetry.SendEvent(o.segClient, telemetry.UsersTerraformApplyCompleted, ""); e != nil { + log.Warn().Msgf("error sending telemetry event: %s", e.Error()) + } + + return nil +} + +func (o *createOptions) setupMinio(kcfg *k8s.KubernetesClient, config *k3d.K3dConfig) error { + minioStopChannel := make(chan struct{}, 1) + + ctx, _ := context.WithCancel(context.Background()) + + defer func() { + close(minioStopChannel) + }() + + k8s.OpenPortForwardPodWrapper(kcfg.Clientset, kcfg.RestConfig, "minio", "minio", 9000, 9000, minioStopChannel) + + // Initialize minio client object. + minioClient, err := minio.New(pkg.MinioPortForwardEndpoint, &minio.Options{ + Creds: credentials.NewStaticV4(pkg.MinioDefaultUsername, pkg.MinioDefaultPassword, ""), + Secure: false, + Region: pkg.MinioRegion, + }) + if err != nil { + log.Info().Msgf("Error creating Minio client: %s", err.Error()) + } + + // define upload object + objectName := fmt.Sprintf("terraform/%s/terraform.tfstate", config.GitProvider) + filePath := config.K1Dir + fmt.Sprintf("/gitops/%s", objectName) + contentType := "xl.meta" + bucketName := "kubefirst-state-store" + + log.Info().Msgf("BucketName: %s", bucketName) + + viper.Set("kubefirst.state-store.name", bucketName) + viper.Set("kubefirst.state-store.hostname", "minio-console.kubefirst.dev") + viper.Set("kubefirst.state-store-creds.access-key-id", pkg.MinioDefaultUsername) + viper.Set("kubefirst.state-store-creds.secret-access-key-id", pkg.MinioDefaultPassword) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + // Upload the zip file with FPutObject + info, err := minioClient.FPutObject(ctx, bucketName, objectName, filePath, minio.PutObjectOptions{ContentType: contentType}) + if err != nil { + log.Info().Msgf("Error uploading to Minio bucket: %s", err.Error()) + } + + log.Printf("Successfully uploaded %s to bucket %s\n", objectName, info.Bucket) + + return nil +} + +func (o *createOptions) wrapUp(config *k3d.K3dConfig) error { + httpAuth := &githttps.BasicAuth{ + Username: o.gitUser, + Password: o.gitToken, + } + + if err := k3d.PostRunPrepareGitopsRepository(o.clusterName, config.GitopsDir, o.k3dGitOpsDirectoryValues(config)); err != nil { + log.Info().Msgf("Error detokenize post run: %s", err.Error()) + } + + gitopsRepo, err := git.PlainOpen(config.GitopsDir) + if err != nil { + log.Info().Msgf("error opening repo at: %s", config.GitopsDir) + } + + // check if file exists before rename + if _, err := os.Stat(fmt.Sprintf("%s/terraform/%s/remote-backend.md", config.GitopsDir, config.GitProvider)); err == nil { + if err := os.Rename( + fmt.Sprintf("%s/terraform/%s/remote-backend.md", config.GitopsDir, config.GitProvider), + fmt.Sprintf("%s/terraform/%s/remote-backend.tf", config.GitopsDir, config.GitProvider)); err != nil { + return err + } + } + + viper.Set("kubefirst-checks.post-detokenize", true) + + if err := viper.WriteConfig(); err != nil { + log.Fatal().Msgf("cannot save state: %s", err.Error()) + } + + // Final gitops repo commit and push + if err := gitClient.Commit(gitopsRepo, "committing initial detokenized gitops-template repo content post run"); err != nil { + return err + } + + if err := gitopsRepo.Push(&git.PushOptions{ + RemoteName: config.GitProvider, + Auth: httpAuth, + }); err != nil { + log.Info().Msgf("Error pushing repo: %s", err.Error()) + } + + return nil +} diff --git a/cmd/k3d/k3d_create_test.go b/cmd/k3d/k3d_create_test.go new file mode 100644 index 000000000..f22c2543e --- /dev/null +++ b/cmd/k3d/k3d_create_test.go @@ -0,0 +1,79 @@ +package k3d + +import ( + "os" + "testing" + + "github.com/kubefirst/kubefirst/internal/logging" + "github.com/kubefirst/runtime/pkg/k3d" + "github.com/stretchr/testify/require" +) + +func TestPreChecks(t *testing.T) { + testCases := []struct { + name string + args []string + envs map[string]string + err bool + }{ + { + name: "invalid git provider - should fail", + args: []string{"--git-provider=invalid"}, + err: true, + }, + { + name: "github user and github org specified - should fail", + args: []string{"--github-user=invalid", "--github-org=invalid"}, + err: true, + }, + { + name: "gitlab provider without gitlab group - should fail", + args: []string{"--git-provider=gitlab"}, + err: true, + }, + { + name: "invalid git protocol - should fail", + args: []string{"--git-protocol=ftp"}, + err: true, + }, + { + name: "invalid catalog items, - should fail", + args: []string{"--install-catalog-apps=invalid"}, + envs: map[string]string{ + GITLAB_TOKEN: "abc", + }, + err: true, + }, + } + + for _, tc := range testCases { + cmd := NewK3dCreateCommand() + cmd.SetArgs(tc.args) + + for k, v := range tc.envs { + t.Setenv(k, v) + } + + require.Error(t, cmd.Execute(), tc.name) + } +} + +func TestInstallTools(t *testing.T) { + o := createOptions{} + config := k3d.GetConfig(o.clusterName, o.gitProvider, o.gitOwner, o.gitProtocol) + + // required for storing config state + logging.Init() + + require.NoError(t, o.downloadTools(config)) + + require.FileExists(t, config.TerraformClient) + require.FileExists(t, config.K3dClient) + require.FileExists(t, config.KubectlClient) + require.FileExists(t, config.MkCertClient) + + t.Cleanup(func() { + os.RemoveAll(config.K1Dir) + os.Remove("~/.kubefirst") + }) +} diff --git a/cmd/k3d/destroy.go b/cmd/k3d/k3d_destroy.go similarity index 96% rename from cmd/k3d/destroy.go rename to cmd/k3d/k3d_destroy.go index fe4ad3773..259568d77 100644 --- a/cmd/k3d/destroy.go +++ b/cmd/k3d/k3d_destroy.go @@ -26,6 +26,17 @@ import ( "github.com/spf13/viper" ) +func NewK3dDestroyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "destroy", + Short: "destroy the kubefirst platform", + Long: "deletes the GitHub resources, k3d resources, and local content to re-provision", + RunE: destroyK3d, + } + + return cmd +} + func destroyK3d(cmd *cobra.Command, args []string) error { helpers.DisplayLogHints() @@ -150,7 +161,7 @@ func destroyK3d(cmd *cobra.Command, args []string) error { // Before removing Terraform resources, remove any container registry repositories // since failing to remove them beforehand will result in an apply failure - var projectsForDeletion = []string{"gitops", "metaphor"} + projectsForDeletion := []string{"gitops", "metaphor"} for _, project := range projectsForDeletion { projectExists, err := gitlabClient.CheckProjectExists(project) if err != nil { diff --git a/cmd/k3d/k3d_local.go b/cmd/k3d/k3d_local.go new file mode 100644 index 000000000..e6a4ac2f6 --- /dev/null +++ b/cmd/k3d/k3d_local.go @@ -0,0 +1,29 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package k3d + +import ( + "github.com/spf13/cobra" +) + +func NewLocalCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "local", + Short: "kubefirst local installation with k3d", + Long: "kubefirst local installation with k3d", + } + + cmd.AddCommand( + NewK3dCreateCommand(), + NewK3dDestroyCommand(), + NewMkCertCommand(), + NewRootCredentialCommand(), + NewVaultUnsealCommand(), + ) + + return cmd +} diff --git a/cmd/k3d/k3d_mkcert.go b/cmd/k3d/k3d_mkcert.go new file mode 100644 index 000000000..f041b6de6 --- /dev/null +++ b/cmd/k3d/k3d_mkcert.go @@ -0,0 +1,82 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package k3d + +import ( + "fmt" + + "github.com/kubefirst/kubefirst/internal/prechecks" + "github.com/kubefirst/runtime/pkg/helpers" + "github.com/kubefirst/runtime/pkg/k3d" + "github.com/kubefirst/runtime/pkg/k8s" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type mkcertOptions struct { + application string + namespace string +} + +func NewMkCertCommand() *cobra.Command { + opts := &mkcertOptions{} + + cmd := &cobra.Command{ + Use: "mkcert", + Short: "create a single ssl certificate for a local application using mkcert (requires mkcert)", + Long: "create a single ssl certificate for a local application", + PreRunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("[PRECHECKS] Running prechecks") + + if err := prechecks.CommandExists("mkcert"); err != nil { + return fmt.Errorf("mkcert is not installed, but is required when using k3d") + } + + fmt.Println("[PRECHECKS] all prechecks passed - continuing") + + return nil + }, + RunE: opts.runMkcert, + } + + cmd.Flags().StringVar(&opts.application, "application", opts.application, "the name of the application (required)") + cmd.MarkFlagRequired("application") + + cmd.Flags().StringVar(&opts.namespace, "namespace", opts.namespace, "the application namespace (required)") + cmd.MarkFlagRequired("namespace") + + return cmd +} + +func (o *mkcertOptions) runMkcert(cmd *cobra.Command, args []string) error { + helpers.DisplayLogHints() + + flags := helpers.GetClusterStatusFlags() + if !flags.SetupComplete { + return fmt.Errorf("there doesn't appear to be an active k3d cluster") + } + + config := k3d.GetConfig( + viper.GetString("flags.cluster-name"), + flags.GitProvider, + viper.GetString(fmt.Sprintf("flags.%s-owner", flags.GitProvider)), + flags.GitProtocol, + ) + + kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) + + log.Infof("Generating certificate for %s.%s...", o.application, k3d.DomainName) + + if err := k3d.GenerateSingleTLSSecret(kcfg.Clientset, *config, o.application, o.namespace); err != nil { + return fmt.Errorf("error generating certificate for %s/%s: %s", o.application, o.namespace, err) + } + + log.Infof("Certificate generated. You can use it with an app by setting `tls.secretName: %s-tls` on a Traefik IngressRoute.", o.application) + + return nil +} diff --git a/cmd/k3d/k3d_root_credentials.go b/cmd/k3d/k3d_root_credentials.go new file mode 100644 index 000000000..b6cf05553 --- /dev/null +++ b/cmd/k3d/k3d_root_credentials.go @@ -0,0 +1,76 @@ +/* +Copyright (C) 2021-2023, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package k3d + +import ( + "fmt" + + "github.com/kubefirst/kubefirst-api/pkg/credentials" + "github.com/kubefirst/runtime/pkg/k3d" + "github.com/kubefirst/runtime/pkg/k8s" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type rootCredentialsOptions struct { + copyArgoCDPasswordToClipboard, copyKbotPasswordToClipboard, copyVaultPasswordToClipboard bool +} + +func NewRootCredentialCommand() *cobra.Command { + o := &rootCredentialsOptions{} + + cmd := &cobra.Command{ + Use: "root-credentials", + Short: "retrieve root authentication information for platform components", + Long: "retrieve root authentication information for platform components", + RunE: o.runK3dRootCredentials, + } + + cmd.Flags().BoolVar(&o.copyArgoCDPasswordToClipboard, "argocd", o.copyArgoCDPasswordToClipboard, "copy the argocd password to the clipboard (optional)") + cmd.Flags().BoolVar(&o.copyKbotPasswordToClipboard, "kbot", o.copyKbotPasswordToClipboard, "copy the kbot password to the clipboard (optional)") + cmd.Flags().BoolVar(&o.copyVaultPasswordToClipboard, "vault", o.copyVaultPasswordToClipboard, "copy the vault password to the clipboard (optional)") + + // flag constraints + cmd.MarkFlagsMutuallyExclusive("argocd", "kbot", "vault") + + return cmd +} + +func (o *rootCredentialsOptions) runK3dRootCredentials(cmd *cobra.Command, args []string) error { + domainName := k3d.DomainName + clusterName := viper.GetString("flags.cluster-name") + gitProvider := viper.GetString("flags.git-provider") + gitProtocol := viper.GetString("flags.git-protocol") + gitOwner := viper.GetString(fmt.Sprintf("flags.%s-owner", gitProvider)) + + opts := credentials.CredentialOptions{ + CopyArgoCDPasswordToClipboard: o.copyArgoCDPasswordToClipboard, + CopyKbotPasswordToClipboard: o.copyKbotPasswordToClipboard, + CopyVaultPasswordToClipboard: o.copyVaultPasswordToClipboard, + } + + // Determine if there are eligible installs + if _, err := credentials.EvalAuth(k3d.CloudProvider, gitProvider); err != nil { + return err + } + + // Determine if the Kubernetes cluster is available + if !viper.GetBool("kubefirst-checks.create-k3d-cluster") { + return fmt.Errorf("it looks like a kubernetes cluster has not been created yet - try again") + } + + // Instantiate kubernetes client + config := k3d.GetConfig(clusterName, gitProvider, gitOwner, gitProtocol) + + kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) + + if err := credentials.ParseAuthData(kcfg.Clientset, k3d.CloudProvider, gitProvider, domainName, &opts); err != nil { + return err + } + + return nil +} diff --git a/cmd/k3d/vault.go b/cmd/k3d/k3d_vault_unseal.go similarity index 58% rename from cmd/k3d/vault.go rename to cmd/k3d/k3d_vault_unseal.go index e88979cf4..6b41b5857 100644 --- a/cmd/k3d/vault.go +++ b/cmd/k3d/k3d_vault_unseal.go @@ -14,7 +14,6 @@ import ( "time" vaultapi "github.com/hashicorp/vault/api" - "github.com/kubefirst/kubefirst/internal/progress" "github.com/kubefirst/runtime/pkg/helpers" "github.com/kubefirst/runtime/pkg/k3d" "github.com/kubefirst/runtime/pkg/k8s" @@ -33,18 +32,30 @@ const ( secretThreshold = 3 ) -// unsealVault will attempt to unseal vaule again if it is currently unsealed -func unsealVault(cmd *cobra.Command, args []string) error { +func NewVaultUnsealCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "unseal-vault", + Short: "check to see if an existing vault instance is sealed and, if so, unseal it", + Long: "check to see if an existing vault instance is sealed and, if so, unseal it", + RunE: runUnsealVault, + } + + return cmd +} + +func runUnsealVault(cmd *cobra.Command, args []string) error { flags := helpers.GetClusterStatusFlags() if !flags.SetupComplete { return fmt.Errorf("there doesn't appear to be an active k3d cluster") } + config := k3d.GetConfig( viper.GetString("flags.cluster-name"), flags.GitProvider, viper.GetString(fmt.Sprintf("flags.%s-owner", flags.GitProvider)), flags.GitProtocol, ) + kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) // Vault api client @@ -64,56 +75,62 @@ func unsealVault(cmd *cobra.Command, args []string) error { return err } - switch health.Sealed { - case true: - node := "vault-0" - existingInitResponse, err := parseExistingVaultInitSecret(kcfg.Clientset) - if err != nil { - return err - } + if !health.Sealed { + return fmt.Errorf("vault is already unsealed") + } - sealStatusTracking := 0 - for i, shard := range existingInitResponse.Keys { - if i < secretThreshold { - log.Info().Msgf("passing unseal shard %v to %s", i+1, node) - deadline := time.Now().Add(60 * time.Second) - ctx, cancel := context.WithDeadline(context.Background(), deadline) - defer cancel() - // Try 5 times to pass unseal shard - for i := 0; i < 5; i++ { - _, err := vaultClient.Sys().UnsealWithContext(ctx, shard) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - continue - } - } - if i == 5 { - return fmt.Errorf("error passing unseal shard %v to %s: %s", i+1, node, err) + node := "vault-0" + existingInitResponse, err := parseExistingVaultInitSecret(kcfg.Clientset) + if err != nil { + return err + } + + sealStatusTracking := 0 + for i, shard := range existingInitResponse.Keys { + if i < secretThreshold { + log.Info().Msgf("passing unseal shard %v to %s", i+1, node) + + deadline := time.Now().Add(60 * time.Second) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + + defer cancel() + + // Try 5 times to pass unseal shard + for i := 0; i < 5; i++ { + if _, err := vaultClient.Sys().UnsealWithContext(ctx, shard); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + continue } } - // Wait for key acceptance - for i := 0; i < 10; i++ { - sealStatus, err := vaultClient.Sys().SealStatus() - if err != nil { - return fmt.Errorf("error retrieving health of %s: %s", node, err) - } - if sealStatus.Progress > sealStatusTracking || !sealStatus.Sealed { - log.Info().Msgf("shard accepted") - sealStatusTracking += 1 - break - } - log.Info().Msgf("waiting for node %s to accept unseal shard", node) - time.Sleep(time.Second * 6) + + if i == 5 { + return fmt.Errorf("error passing unseal shard %v to %s: %s", i+1, node, err) } } - } - fmt.Printf("vault unsealed\n") - case false: - return fmt.Errorf("vault is already unsealed") + // Wait for key acceptance + for i := 0; i < 10; i++ { + sealStatus, err := vaultClient.Sys().SealStatus() + if err != nil { + return fmt.Errorf("error retrieving health of %s: %s", node, err) + } + + if sealStatus.Progress > sealStatusTracking || !sealStatus.Sealed { + log.Info().Msgf("shard accepted") + + sealStatusTracking += 1 + + break + } + + log.Info().Msgf("waiting for node %s to accept unseal shard", node) + + time.Sleep(time.Second * 6) + } + } } - progress.Progress.Quit() + fmt.Printf("vault unsealed\n") return nil } @@ -139,5 +156,6 @@ func parseExistingVaultInitSecret(clientset *kubernetes.Clientset) (*vaultapi.In Keys: rkSlice, RootToken: secret["root-token"], } + return existingInitResponse, nil } diff --git a/cmd/k3d/mkcert.go b/cmd/k3d/mkcert.go deleted file mode 100644 index 80a829af5..000000000 --- a/cmd/k3d/mkcert.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright (C) 2021-2023, Kubefirst - -This program is licensed under MIT. -See the LICENSE file for more details. -*/ -package k3d - -import ( - "fmt" - - "github.com/kubefirst/kubefirst/internal/progress" - "github.com/kubefirst/runtime/pkg/helpers" - "github.com/kubefirst/runtime/pkg/k3d" - "github.com/kubefirst/runtime/pkg/k8s" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// mkCert creates a single certificate for a host for k3d -func mkCert(cmd *cobra.Command, args []string) error { - helpers.DisplayLogHints() - - appNameFlag, err := cmd.Flags().GetString("application") - if err != nil { - return err - } - - appNamespaceFlag, err := cmd.Flags().GetString("namespace") - if err != nil { - return err - } - - flags := helpers.GetClusterStatusFlags() - if !flags.SetupComplete { - return fmt.Errorf("there doesn't appear to be an active k3d cluster") - } - config := k3d.GetConfig( - viper.GetString("flags.cluster-name"), - flags.GitProvider, - viper.GetString(fmt.Sprintf("flags.%s-owner", flags.GitProvider)), - flags.GitProtocol, - ) - kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) - - log.Infof("Generating certificate for %s.%s...", appNameFlag, k3d.DomainName) - - err = k3d.GenerateSingleTLSSecret(kcfg.Clientset, *config, appNameFlag, appNamespaceFlag) - if err != nil { - return fmt.Errorf("error generating certificate for %s/%s: %s", appNameFlag, appNamespaceFlag, err) - } - - log.Infof("Certificate generated. You can use it with an app by setting `tls.secretName: %s-tls` on a Traefik IngressRoute.", appNameFlag) - progress.Progress.Quit() - - return nil -} diff --git a/cmd/k3d/root-credentials.go b/cmd/k3d/root-credentials.go deleted file mode 100644 index afec3409e..000000000 --- a/cmd/k3d/root-credentials.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2021-2023, Kubefirst - -This program is licensed under MIT. -See the LICENSE file for more details. -*/ -package k3d - -import ( - "fmt" - - "github.com/kubefirst/kubefirst-api/pkg/credentials" - "github.com/kubefirst/kubefirst/internal/progress" - "github.com/kubefirst/runtime/pkg/k3d" - "github.com/kubefirst/runtime/pkg/k8s" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -func getK3dRootCredentials(cmd *cobra.Command, args []string) error { - domainName := k3d.DomainName - clusterName := viper.GetString("flags.cluster-name") - gitProvider := viper.GetString("flags.git-provider") - gitProtocol := viper.GetString("flags.git-protocol") - gitOwner := viper.GetString(fmt.Sprintf("flags.%s-owner", gitProvider)) - - // Parse flags - a, err := cmd.Flags().GetBool("argocd") - if err != nil { - return err - } - k, err := cmd.Flags().GetBool("kbot") - if err != nil { - return err - } - v, err := cmd.Flags().GetBool("vault") - if err != nil { - return err - } - opts := credentials.CredentialOptions{ - CopyArgoCDPasswordToClipboard: a, - CopyKbotPasswordToClipboard: k, - CopyVaultPasswordToClipboard: v, - } - - // Determine if there are eligible installs - _, err = credentials.EvalAuth(k3d.CloudProvider, gitProvider) - if err != nil { - return err - } - - // Determine if the Kubernetes cluster is available - if !viper.GetBool("kubefirst-checks.create-k3d-cluster") { - return fmt.Errorf("it looks like a kubernetes cluster has not been created yet - try again") - } - - // Instantiate kubernetes client - config := k3d.GetConfig(clusterName, gitProvider, gitOwner, gitProtocol) - - kcfg := k8s.CreateKubeConfig(false, config.Kubeconfig) - - err = credentials.ParseAuthData(kcfg.Clientset, k3d.CloudProvider, gitProvider, domainName, &opts) - if err != nil { - return err - } - - progress.Progress.Quit() - - return nil -} diff --git a/cmd/k3s/create.go b/cmd/k3s/create.go index 846b0fc83..2ef883739 100644 --- a/cmd/k3s/create.go +++ b/cmd/k3s/create.go @@ -9,8 +9,6 @@ package k3s import ( "fmt" - "github.com/rs/zerolog/log" - "github.com/kubefirst/kubefirst/internal/catalog" "github.com/kubefirst/kubefirst/internal/cluster" "github.com/kubefirst/kubefirst/internal/gitShim" @@ -20,6 +18,7 @@ import ( "github.com/kubefirst/kubefirst/internal/utilities" "github.com/kubefirst/runtime/pkg" internalssh "github.com/kubefirst/runtime/pkg/ssh" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -29,7 +28,7 @@ func createK3s(cmd *cobra.Command, args []string) error { cliFlags, err := utilities.GetFlags(cmd, "k3s") if err != nil { progress.Error(err.Error()) - log.Fatal().Msgf("error collecting flags: ", err) + log.Fatal().Msgf("error collecting flags: %s", err.Error()) return nil } diff --git a/cmd/launch.go b/cmd/launch.go index 47c55b6df..a709c6322 100644 --- a/cmd/launch.go +++ b/cmd/launch.go @@ -13,10 +13,8 @@ import ( "github.com/spf13/cobra" ) -var ( - // additionalHelmFlags can optionally pass user-supplied flags to helm - additionalHelmFlags []string -) +// additionalHelmFlags can optionally pass user-supplied flags to helm +var additionalHelmFlags []string func LaunchCommand() *cobra.Command { launchCommand := &cobra.Command{ diff --git a/cmd/letsencrypt.go b/cmd/letsencrypt.go index d1d06339b..7e989c959 100644 --- a/cmd/letsencrypt.go +++ b/cmd/letsencrypt.go @@ -14,10 +14,8 @@ import ( "github.com/spf13/cobra" ) -var ( - // Certificate check - domainNameFlag string -) +// Certificate check +var domainNameFlag string func LetsEncryptCommand() *cobra.Command { letsEncryptCommand := &cobra.Command{ diff --git a/cmd/logs.go b/cmd/logs.go index 34eb311e7..9afb36d0a 100755 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -41,7 +41,3 @@ var logsCmd = &cobra.Command{ return nil }, } - -func init() { - rootCmd.AddCommand(logsCmd) -} diff --git a/cmd/reset.go b/cmd/reset.go index 7ecf09477..f898f22dd 100755 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -67,10 +67,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(raw interface{}) (map[string]bool, error) { diff --git a/cmd/root.go b/cmd/root.go index b85fbad8d..cbc381273 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,66 +7,113 @@ See the LICENSE file for more details. package cmd import ( + "context" "fmt" "os" + "os/signal" + "syscall" "github.com/kubefirst/kubefirst/cmd/aws" "github.com/kubefirst/kubefirst/cmd/civo" "github.com/kubefirst/kubefirst/cmd/digitalocean" "github.com/kubefirst/kubefirst/cmd/k3d" "github.com/kubefirst/kubefirst/internal/common" + "github.com/kubefirst/kubefirst/internal/logging" "github.com/kubefirst/kubefirst/internal/progress" "github.com/kubefirst/runtime/configs" - "github.com/kubefirst/runtime/pkg/progressPrinter" "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 +func NewRootCommand() *cobra.Command { + cmd := &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 docs.kubefirst.io.`, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // wire viper config for flags for all commands - return configs.InitializeViperConfig(cmd) - }, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("To learn more about kubefirst, run:") - fmt.Println(" kubefirst help") - progress.Progress.Quit() - }, -} + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return configs.InitializeViperConfig(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("To learn more about kubefirst, run:\n\tkubefirst help") -// 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/kubefirst/runtime/issues/525 - //Before removing next line, please read ticket above. - common.CheckForVersionUpdate() - progressPrinter.GetInstance() - err := rootCmd.Execute() - if err != nil { - fmt.Printf("\nIf a detailed error message was available, please make the necessary corrections before retrying.\nYou can re-run the last command to try the operation again.\n\n") - os.Exit(1) + return cmd.Help() + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + // gracefully quit bubbletea if in progress + if progress.Progress != nil { + progress.Progress.Quit() + } + + return nil + }, } -} -func init() { - cobra.OnInitialize() - rootCmd.SilenceUsage = true - rootCmd.AddCommand( + cmd.AddCommand( betaCmd, + infoCmd, + resetCmd, + versionCmd, aws.NewCommand(), civo.NewCommand(), digitalocean.NewCommand(), - k3d.NewCommand(), - k3d.LocalCommandAlias(), + k3d.NewK3DCommand(), + k3d.NewLocalCommand(), LaunchCommand(), LetsEncryptCommand(), TerraformCommand(), ) + + cobra.OnInitialize( + common.CheckForVersionUpdate, + logging.Init, + ) + + return cmd +} + +func Execute() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + progressPrinter.GetInstance() + progress.InitializeProgressTerminal(ctx) + + errCh := make(chan error, 2) + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := NewRootCommand().ExecuteContext(ctx); err != nil { + errCh <- fmt.Errorf("error executing command: %s", err.Error()) + } + + errCh <- nil + }() + + go func() { + if _, err := progress.Progress.Run(); err != nil { + errCh <- fmt.Errorf("error initializing TUI: %s", err.Error()) + } + + errCh <- nil + }() + + select { + case err := <-errCh: + if err != nil { + return err + } + case sig := <-signals: + fmt.Println("Received signal: ", sig) + cancel() + case <-ctx.Done(): + fmt.Println("Finished.") + } + + fmt.Println("Exiting.") + + return nil } diff --git a/cmd/version.go b/cmd/version.go index fb03f0a7e..24eea6c38 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -14,10 +14,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", diff --git a/cmd/vultr/command.go b/cmd/vultr/command.go index 1f81e71c4..39a75306f 100644 --- a/cmd/vultr/command.go +++ b/cmd/vultr/command.go @@ -49,7 +49,6 @@ var ( ) func NewCommand() *cobra.Command { - vultrCmd := &cobra.Command{ Use: "vultr", Short: "kubefirst Vultr installation", diff --git a/cmd/vultr/create.go b/cmd/vultr/create.go index 0b3a5c2b9..73d1f8d6a 100644 --- a/cmd/vultr/create.go +++ b/cmd/vultr/create.go @@ -20,7 +20,6 @@ import ( "github.com/kubefirst/runtime/pkg" internalssh "github.com/kubefirst/runtime/pkg/ssh" "github.com/rs/zerolog/log" - "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -56,7 +55,6 @@ func createVultr(cmd *cobra.Command, args []string) error { utilities.CreateK1ClusterDirectory(clusterNameFlag) gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) - if err != nil { progress.Error(err.Error()) return nil diff --git a/go.mod b/go.mod index 7010e9852..f16177e80 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.10.3 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d k8s.io/api v0.27.1 k8s.io/apimachinery v0.27.1 k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible @@ -147,6 +147,7 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.39.0 // indirect @@ -172,6 +173,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/crypto v0.20.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/tools v0.14.0 // indirect diff --git a/go.sum b/go.sum index a2bfb7304..ea81ce604 100644 --- a/go.sum +++ b/go.sum @@ -1094,8 +1094,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1106,7 +1106,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index f6d0c3cc7..a42feaa29 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -14,9 +14,7 @@ import ( "strings" git "github.com/google/go-github/v52/github" - apiTypes "github.com/kubefirst/kubefirst-api/pkg/types" - "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) @@ -72,6 +70,7 @@ func ValidateCatalogApps(catalogApps string) (bool, []apiTypes.GitopsCatalogApp, apps, err := ReadActiveApplications() if err != nil { log.Error().Msgf(fmt.Sprintf("Error getting gitops catalag applications: %s", err)) + return false, gitopsCatalogapps, err } diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go new file mode 100644 index 000000000..400077a45 --- /dev/null +++ b/internal/catalog/catalog_test.go @@ -0,0 +1,56 @@ +package catalog + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateCatalogApps(t *testing.T) { + testCases := []struct { + name string + app string + err bool + }{ + { + name: "valid app: tracetest", + app: "tracetest", + }, + { + name: "invalid app: doesnotexist", + err: true, + app: "doesnotexist", + }, + { + name: "valid multiple app: tracetest,yourls", + app: "tracetest,yourls", + }, + } + + for _, tc := range testCases { + + ok, items, err := ValidateCatalogApps(tc.app) + + // should not exist + if tc.err { + // should error + require.Error(t, err, tc.name) + + // app has not been found + require.False(t, ok, tc.name) + // exists + } else { + // should not error + require.NoError(t, err, tc.name) + + // name equals app name + for i, a := range strings.Split(tc.app, ",") { + require.Equal(t, a, items[i].Name, tc.name) + } + + // app has been found + require.True(t, ok, tc.name) + } + } +} diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go index 001214f21..d6ec8592f 100644 --- a/internal/cluster/cluster.go +++ b/internal/cluster/cluster.go @@ -15,15 +15,13 @@ import ( "os" "strings" - "github.com/rs/zerolog/log" - apiTypes "github.com/kubefirst/kubefirst-api/pkg/types" "github.com/kubefirst/kubefirst/internal/types" + "github.com/rs/zerolog/log" ) func GetConsoleIngresUrl() string { - - if strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" { //allow using local console running on port 3000 + if strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" { // allow using local console running on port 3000 return "http://localhost:3000" } diff --git a/internal/common/common.go b/internal/common/common.go index c24ac9f0c..b70de3cb6 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -20,7 +20,6 @@ import ( "github.com/kubefirst/kubefirst/internal/launch" "github.com/kubefirst/kubefirst/internal/progress" "github.com/kubefirst/runtime/configs" - "github.com/kubefirst/runtime/pkg/docker" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -63,7 +62,6 @@ func versionCheck() (res *CheckResponse, skip bool) { flatVersion := strings.ReplaceAll(configs.K1Version, "v", "") resp, err := http.Get("https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/k/kubefirst.rb") - if err != nil { fmt.Printf("checking for a newer version failed (cannot get Homebrew formula) with: %s", err) return nil, true @@ -187,15 +185,3 @@ https://docs.kubefirst.io/` + cloudProvider + `/deprovision return nil } - -// checkDocker makes sure Docker is running before all commands -func CheckDocker(cmd *cobra.Command, args []string) { - // Verify Docker is running - dcli := docker.DockerClientWrapper{ - Client: docker.NewDockerClient(), - } - _, err := dcli.CheckDockerReady() - if err != nil { - progress.Error(fmt.Sprintf("Docker must be running to use this command. Error checking Docker status: %s", err)) - } -} diff --git a/internal/gitShim/containerRegistryAuth.go b/internal/gitShim/containerRegistryAuth.go index d85653f28..7d988f113 100644 --- a/internal/gitShim/containerRegistryAuth.go +++ b/internal/gitShim/containerRegistryAuth.go @@ -71,7 +71,7 @@ func CreateContainerRegistrySecret(obj *ContainerRegistryAuth) (string, error) { } // Create argo workflows pull secret - var p = gitlab.DeployTokenCreateParameters{ + p := gitlab.DeployTokenCreateParameters{ Name: secretName, Username: secretName, Scopes: []string{"read_registry", "write_registry"}, diff --git a/internal/k3d/menu.go b/internal/k3d/menu.go index 2080c2c52..ec18ee7e0 100644 --- a/internal/k3d/menu.go +++ b/internal/k3d/menu.go @@ -17,8 +17,10 @@ import ( "github.com/charmbracelet/lipgloss" ) -const ListHeight = 14 -const DefaultWidth = 20 +const ( + ListHeight = 14 + DefaultWidth = 20 +) var ( TitleStyle = lipgloss.NewStyle().MarginLeft(2) diff --git a/internal/launch/cmd.go b/internal/launch/cmd.go index 8fd1bd07e..253003fff 100644 --- a/internal/launch/cmd.go +++ b/internal/launch/cmd.go @@ -87,7 +87,7 @@ func Up(additionalHelmFlags []string, inCluster bool, useTelemetry bool) { if err != nil { progress.Error(fmt.Sprintf("error while trying to download k3d: %s", err)) } - err = os.Chmod(k3dClient, 0755) + err = os.Chmod(k3dClient, 0o755) if err != nil { log.Fatal().Msg(err.Error()) } @@ -116,14 +116,13 @@ func Up(additionalHelmFlags []string, inCluster bool, useTelemetry bool) { helmTarDownload, err := os.Open(helmDownloadTarGzPath) if err != nil { progress.Error(fmt.Sprintf("could not read helm download content")) - } downloadManager.ExtractFileFromTarGz( helmTarDownload, fmt.Sprintf("%s-%s/helm", k3d.LocalhostOS, k3d.LocalhostARCH), helmClient, ) - err = os.Chmod(helmClient, 0755) + err = os.Chmod(helmClient, 0o755) if err != nil { log.Fatal().Msg(err.Error()) } @@ -148,7 +147,7 @@ func Up(additionalHelmFlags []string, inCluster bool, useTelemetry bool) { if err != nil { progress.Error(fmt.Sprintf("error while trying to download mkcert: %s", err)) } - err = os.Chmod(mkcertClient, 0755) + err = os.Chmod(mkcertClient, 0o755) if err != nil { progress.Error(err.Error()) } @@ -531,7 +530,6 @@ func ListClusters() { // DeleteCluster makes a request to the console API to delete a single cluster func DeleteCluster(managedClusterName string) { err := cluster.DeleteCluster(managedClusterName) - if err != nil { progress.Error(fmt.Sprintf("error: cluster %s not found\n", managedClusterName)) } diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 000000000..985349569 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,104 @@ +package logging + +import ( + "fmt" + stdLog "log" + "os" + "time" + + "github.com/kubefirst/kubefirst/internal/utilities" + "github.com/kubefirst/runtime/configs" + "github.com/kubefirst/runtime/pkg" + zeroLog "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// Init sets up log directory and logfile. +func Init() { + // user has not specified any action, no need to setup a logfile + if len(os.Args[1:]) == 0 { + return + } + + config := configs.ReadConfig() + + if err := pkg.SetupViper(config, true); err != nil { + stdLog.Panic(err) + } + + now := time.Now() + epoch := now.Unix() + logfileName := fmt.Sprintf("log_%d.log", epoch) + isProvision := utilities.StringInSlice("create", os.Args) + isLogs := utilities.StringInSlice("logs", os.Args) + + // don't create a new log file for logs, using the previous one + if isLogs { + logfileName = viper.GetString("k1-paths.log-file-name") + } + + // use cluster name as filename + if isProvision { + clusterName := fmt.Sprint(epoch) + for i := 1; i < len(os.Args); i++ { + arg := os.Args[i] + + // Check if the argument is "--cluster-name" + if arg == "--cluster-name" && i+1 < len(os.Args) { + // Get the value of the cluster name + clusterName = os.Args[i+1] + break + } + } + + logfileName = fmt.Sprintf("log_%s.log", clusterName) + } + + homePath, err := os.UserHomeDir() + if err != nil { + log.Info().Msg(err.Error()) + } + + //* create k1Dir if it doesn't exist + k1Dir := fmt.Sprintf("%s/.k1", homePath) + if _, err := os.Stat(k1Dir); os.IsNotExist(err) { + if err := os.MkdirAll(k1Dir, os.ModePerm); err != nil { + log.Info().Msgf("%s directory already exists, continuing", k1Dir) + } + } + + //* create log directory + logsFolder := fmt.Sprintf("%s/logs", k1Dir) + utilities.CreateDirIfNotExists(logsFolder) + + //* create session log file + logfile := fmt.Sprintf("%s/%s", logsFolder, logfileName) + logFileObj, err := pkg.OpenLogFile(logfile) + if err != nil { + stdLog.Panicf("unable to store log location, error is: %s - please verify the current user has write access to this directory", err) + } + + // // handle file close request + // defer func(logFileObj *os.File) { + // if err = logFileObj.Close(); err != nil { + // log.Print(err) + // } + // }(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) + + log.Logger = zeroLog.New(logFileObj).With().Timestamp().Logger() + + viper.Set("k1-paths.logs-dir", logsFolder) + viper.Set("k1-paths.log-file", logfile) + viper.Set("k1-paths.log-file-name", logfileName) + + if viper.WriteConfig() != nil { + stdLog.Panicf("unable to set log-file-location, error is: %s", err) + } +} diff --git a/internal/prechecks/precheck_test.go b/internal/prechecks/precheck_test.go new file mode 100644 index 000000000..1a1b987df --- /dev/null +++ b/internal/prechecks/precheck_test.go @@ -0,0 +1,46 @@ +package prechecks + +import ( + "testing" +) + +// test envvar exists +func TestEnvVarExists(t *testing.T) { + if EnvVarExists("TEST") { + t.Fatal("TEST env var should not exist") + } + + t.Setenv("ENV", "123") + + if !EnvVarExists("ENV") { + t.Fatal("ENV env var should exist") + } +} + +// test url is available +func TestURLIsAvailable(t *testing.T) { + if URLIsAvailable("google.com:443") != nil { + t.Fatal("Google should be reachable") + } +} + +// test file exists +func TestFileExists(t *testing.T) { + if FileExists("test") == nil { + t.Fatal("File should not exist") + } +} + +// test check available disk size +func TestCheckAvailableDiskSize(t *testing.T) { + if CheckAvailableDiskSize() != nil { + t.Fatal("Disk size should be available") + } +} + +// test command exists +func TestCommandExists(t *testing.T) { + if err := CommandExists("ls"); err != nil { + t.Fatal("ls command should exist") + } +} diff --git a/internal/prechecks/prechecks.go b/internal/prechecks/prechecks.go new file mode 100644 index 000000000..b22fd6b3c --- /dev/null +++ b/internal/prechecks/prechecks.go @@ -0,0 +1,91 @@ +package prechecks + +import ( + "fmt" + "net" + "os" + "os/exec" + "time" + + "github.com/dustin/go-humanize" + "github.com/kubefirst/runtime/pkg" + "github.com/kubefirst/runtime/pkg/docker" + "github.com/kubefirst/runtime/pkg/ssh" + "github.com/rs/zerolog/log" +) + +// RequireEnvVarExists returns true if the given env var name exists. +func EnvVarExists(n string) bool { + _, ok := os.LookupEnv(n) + + return ok +} + +// URLIsAvailable returns true if the given URL is reachable. +func URLIsAvailable(url string) error { + timeout := 3 * time.Second + + if _, err := net.DialTimeout("tcp", url, timeout); err != nil { + return err + } + + return nil +} + +// FileExists returns an error if the given file does not exist. +func FileExists(f string) error { + if _, err := os.Stat(f); err != nil { + return fmt.Errorf("%s does not exist - but is required", f) + } + + return nil +} + +// CheckAvailableDiskSize returns an error if not enough disk space is available. +func CheckAvailableDiskSize() error { + free, err := pkg.GetAvailableDiskSize() + if err != nil { + return err + } + + // convert available disk size to GB format + availableDiskSize := float64(free) / humanize.GByte + if availableDiskSize < pkg.MinimumAvailableDiskSize { + return fmt.Errorf("there is not enough space to proceed with the installation, a minimum of %d GB is required to proceed", pkg.MinimumAvailableDiskSize) + } + + return nil +} + +// CommandExists return true if the give command exists within the users $PATH variable. +func CommandExists(cmd string) error { + if _, err := exec.LookPath(cmd); err != nil { + return fmt.Errorf("%s not installed - is but required", cmd) + } + + return nil +} + +// CheckKnownHosts checks if the host is in the known_hosts file. +func CheckKnownHosts(url string) error { + key, err := ssh.GetHostKey(url) + if err != nil { + return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan %s >> ~/.ssh/known_hosts` to remedy", url) + } + + log.Info().Msgf("%s %s\n", url, key.Type()) + + return nil +} + +// CheckDockerIsRunning makes sure Docker is running. +func CheckDockerIsRunning() error { + dcli := docker.DockerClientWrapper{ + Client: docker.NewDockerClient(), + } + if _, err := dcli.CheckDockerReady(); err != nil { + return fmt.Errorf("docker must be running to use this command. Error checking Docker status: %s", err) + } + + return nil +} diff --git a/internal/progress/command.go b/internal/progress/command.go index 6a40888ef..a8b16b181 100644 --- a/internal/progress/command.go +++ b/internal/progress/command.go @@ -18,9 +18,7 @@ import ( func GetClusterInterval(clusterName string) tea.Cmd { return tea.Every(time.Second*10, func(t time.Time) tea.Msg { provisioningCluster, err := cluster.GetCluster(clusterName) - if err != nil { - } return CusterProvisioningMsg(provisioningCluster) diff --git a/internal/progress/progress.go b/internal/progress/progress.go index 7d86ba202..864f938fb 100644 --- a/internal/progress/progress.go +++ b/internal/progress/progress.go @@ -7,6 +7,7 @@ See the LICENSE file for more details. package progress import ( + "context" "fmt" tea "github.com/charmbracelet/bubbletea" @@ -23,8 +24,8 @@ func NewModel() progressModel { } // Bubbletea functions -func InitializeProgressTerminal() { - Progress = tea.NewProgram(NewModel()) +func InitializeProgressTerminal(ctx context.Context) { + Progress = tea.NewProgram(NewModel(), tea.WithContext(ctx)) } func (m progressModel) Init() tea.Cmd { diff --git a/internal/segment/segment.go b/internal/segment/segment.go index f2bbb5589..d1e08dc70 100644 --- a/internal/segment/segment.go +++ b/internal/segment/segment.go @@ -14,7 +14,6 @@ const ( ) func InitClient(clusterId, clusterType, gitProvider string) telemetry.TelemetryEvent { - machineID, _ := machineid.ID() c := telemetry.TelemetryEvent{ diff --git a/internal/utilities/cluster.go b/internal/utilities/cluster.go new file mode 100644 index 000000000..377a3fbc4 --- /dev/null +++ b/internal/utilities/cluster.go @@ -0,0 +1,234 @@ +package utilities + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" + "time" + + apiTypes "github.com/kubefirst/kubefirst-api/pkg/types" + "github.com/kubefirst/kubefirst/internal/progress" + "github.com/kubefirst/kubefirst/internal/types" + "github.com/kubefirst/runtime/configs" + "github.com/kubefirst/runtime/pkg/k8s" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "go.mongodb.org/mongo-driver/bson/primitive" + v1secret "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func CreateClusterRecordFromRaw( + useTelemetry bool, + gitOwner string, + gitUser string, + gitToken string, + gitlabOwnerGroupID int, + gitopsTemplateURL string, + gitopsTemplateBranch string, + catalogApps []apiTypes.GitopsCatalogApp, +) apiTypes.Cluster { + cloudProvider := viper.GetString("kubefirst.cloud-provider") + domainName := viper.GetString("flags.domain-name") + gitProvider := viper.GetString("flags.git-provider") + + kubefirstTeam := os.Getenv("KUBEFIRST_TEAM") + if kubefirstTeam == "" { + kubefirstTeam = "false" + } + + cl := apiTypes.Cluster{ + ID: primitive.NewObjectID(), + CreationTimestamp: fmt.Sprintf("%v", time.Now().UTC()), + UseTelemetry: useTelemetry, + Status: "provisioned", + AlertsEmail: viper.GetString("flags.alerts-email"), + ClusterName: viper.GetString("flags.cluster-name"), + CloudProvider: cloudProvider, + CloudRegion: viper.GetString("flags.cloud-region"), + DomainName: domainName, + ClusterID: viper.GetString("kubefirst.cluster-id"), + ClusterType: "mgmt", + GitopsTemplateURL: gitopsTemplateURL, + GitopsTemplateBranch: gitopsTemplateBranch, + GitProvider: gitProvider, + GitHost: fmt.Sprintf("%s.com", gitProvider), + GitProtocol: viper.GetString("flags.git-protocol"), + DnsProvider: viper.GetString("flags.dns-provider"), + GitlabOwnerGroupID: gitlabOwnerGroupID, + AtlantisWebhookSecret: viper.GetString("secrets.atlantis-webhook"), + AtlantisWebhookURL: fmt.Sprintf("https://atlantis.%s/events", domainName), + KubefirstTeam: kubefirstTeam, + ArgoCDAuthToken: viper.GetString("components.argocd.auth-token"), + ArgoCDPassword: viper.GetString("components.argocd.password"), + PostInstallCatalogApps: catalogApps, + GitAuth: apiTypes.GitAuth{ + Token: gitToken, + User: gitUser, + Owner: gitOwner, + PublicKey: viper.GetString("kbot.public-key"), + PrivateKey: viper.GetString("kbot.private-key"), + }, + CloudflareAuth: apiTypes.CloudflareAuth{ + Token: os.Getenv("CF_API_TOKEN"), + }, + } + + switch cloudProvider { + case "civo": + cl.CivoAuth.Token = os.Getenv("CIVO_TOKEN") + case "aws": + // ToDo: where to get credentials? + cl.AWSAuth.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") + cl.AWSAuth.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") + cl.AWSAuth.SessionToken = viper.GetString("kubefirst.state-store-creds.token") + case "digitalocean": + cl.DigitaloceanAuth.Token = os.Getenv("DO_TOKEN") + cl.DigitaloceanAuth.SpacesKey = os.Getenv("DO_SPACES_KEY") + cl.DigitaloceanAuth.SpacesSecret = os.Getenv("DO_SPACES_SECRET") + case "vultr": + cl.VultrAuth.Token = os.Getenv("VULTR_API_KEY") + } + + cl.StateStoreCredentials.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") + cl.StateStoreCredentials.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") + cl.StateStoreCredentials.SessionToken = viper.GetString("kubefirst.state-store-creds.token") + cl.StateStoreCredentials.Name = viper.GetString("kubefirst.state-store-creds.name") + cl.StateStoreCredentials.ID = viper.GetString("kubefirst.state-store-creds.id") + + cl.StateStoreDetails.ID = viper.GetString("kubefirst.state-store.id") + cl.StateStoreDetails.Name = viper.GetString("kubefirst.state-store.name") + cl.StateStoreDetails.Hostname = viper.GetString("kubefirst.state-store.hostname") + cl.StateStoreDetails.AWSArtifactsBucket = viper.GetString("kubefirst.artifacts-bucket") + cl.StateStoreDetails.AWSStateStoreBucket = viper.GetString("kubefirst.state-store-bucket") + + return cl +} + +func CreateClusterDefinitionRecordFromRaw(gitAuth apiTypes.GitAuth, cliFlags types.CliFlags, catalogApps []apiTypes.GitopsCatalogApp) apiTypes.ClusterDefinition { + cloudProvider := viper.GetString("kubefirst.cloud-provider") + domainName := viper.GetString("flags.domain-name") + gitProvider := viper.GetString("flags.git-provider") + + kubefirstTeam := os.Getenv("KUBEFIRST_TEAM") + if kubefirstTeam == "" { + kubefirstTeam = "false" + } + + stringToIntNodeCount, err := strconv.Atoi(cliFlags.NodeCount) + if err != nil { + log.Info().Msg("Unable to convert node count to type string") + } + + cl := apiTypes.ClusterDefinition{ + AdminEmail: viper.GetString("flags.alerts-email"), + ClusterName: viper.GetString("flags.cluster-name"), + CloudProvider: cloudProvider, + CloudRegion: viper.GetString("flags.cloud-region"), + DomainName: domainName, + SubdomainName: cliFlags.SubDomainName, + Type: "mgmt", + NodeType: cliFlags.NodeType, + NodeCount: stringToIntNodeCount, + GitopsTemplateURL: cliFlags.GitopsTemplateURL, + GitopsTemplateBranch: cliFlags.GitopsTemplateBranch, + GitProvider: gitProvider, + GitProtocol: viper.GetString("flags.git-protocol"), + DnsProvider: viper.GetString("flags.dns-provider"), + LogFileName: viper.GetString("k1-paths.log-file-name"), + PostInstallCatalogApps: catalogApps, + GitAuth: apiTypes.GitAuth{ + Token: gitAuth.Token, + User: gitAuth.User, + Owner: gitAuth.Owner, + PublicKey: viper.GetString("kbot.public-key"), + PrivateKey: viper.GetString("kbot.private-key"), + }, + CloudflareAuth: apiTypes.CloudflareAuth{ + APIToken: os.Getenv("CF_API_TOKEN"), + }, + } + + if cl.GitopsTemplateBranch == "" { + cl.GitopsTemplateBranch = configs.K1Version + + if configs.K1Version == "development" { + cl.GitopsTemplateBranch = "main" + } + } + + switch cloudProvider { + case "akamai": + cl.AkamaiAuth.Token = os.Getenv("LINODE_TOKEN") + case "aws": + // ToDo: where to get credentials? + cl.AWSAuth.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") + cl.AWSAuth.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") + cl.AWSAuth.SessionToken = viper.GetString("kubefirst.state-store-creds.token") + cl.ECR = cliFlags.Ecr + case "civo": + cl.CivoAuth.Token = os.Getenv("CIVO_TOKEN") + case "digitalocean": + cl.DigitaloceanAuth.Token = os.Getenv("DO_TOKEN") + cl.DigitaloceanAuth.SpacesKey = os.Getenv("DO_SPACES_KEY") + cl.DigitaloceanAuth.SpacesSecret = os.Getenv("DO_SPACES_SECRET") + case "vultr": + cl.VultrAuth.Token = os.Getenv("VULTR_API_KEY") + case "k3s": + cl.K3sAuth.K3sServersPrivateIps = viper.GetStringSlice("flags.servers-private-ips") + cl.K3sAuth.K3sServersPublicIps = viper.GetStringSlice("flags.servers-public-ips") + cl.K3sAuth.K3sSshUser = viper.GetString("flags.ssh-user") + cl.K3sAuth.K3sSshPrivateKey = viper.GetString("flags.ssh-privatekey") + cl.K3sAuth.K3sServersArgs = viper.GetStringSlice("flags.servers-args") + case "google": + jsonFilePath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") + + jsonFile, err := os.Open(jsonFilePath) + if err != nil { + progress.Error("Unable to read GOOGLE_APPLICATION_CREDENTIALS file") + } + + jsonContent, _ := ioutil.ReadAll(jsonFile) + + cl.GoogleAuth.KeyFile = string(jsonContent) + cl.GoogleAuth.ProjectId = cliFlags.GoogleProject + } + + return cl +} + +func ExportCluster(cluster apiTypes.Cluster, kcfg *k8s.KubernetesClient) error { + cluster.Status = "provisioned" + cluster.InProgress = false + + if viper.GetBool("kubefirst-checks.secret-export-state") { + return nil + } + + time.Sleep(time.Second * 10) + + bytes, err := json.Marshal(cluster) + if err != nil { + log.Error().Msg(err.Error()) + return err + } + + secretValuesMap, _ := ParseJSONToMap(string(bytes)) + + secret := &v1secret.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "kubefirst-initial-state", Namespace: "kubefirst"}, + Data: secretValuesMap, + } + + err = k8s.CreateSecretV2(kcfg.Clientset, secret) + if err != nil { + return fmt.Errorf(fmt.Sprintf("unable to save secret to management cluster. %s", err)) + } + + viper.Set("kubefirst-checks.secret-export-state", true) + viper.WriteConfig() + + return nil +} diff --git a/internal/utilities/flags.go b/internal/utilities/flags.go index c7d15270e..cdd39f330 100644 --- a/internal/utilities/flags.go +++ b/internal/utilities/flags.go @@ -11,7 +11,6 @@ import ( "github.com/kubefirst/kubefirst/internal/progress" "github.com/kubefirst/kubefirst/internal/types" - "github.com/spf13/cobra" "github.com/spf13/viper" ) diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index fbe03dd6e..156d2cbc6 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -7,28 +7,24 @@ See the LICENSE file for more details. package utilities import ( - "bufio" "encoding/json" "fmt" - "io/ioutil" - "net/http" "os" - "strconv" - "time" - apiTypes "github.com/kubefirst/kubefirst-api/pkg/types" - "github.com/kubefirst/kubefirst/internal/progress" - "github.com/kubefirst/kubefirst/internal/types" - "github.com/kubefirst/runtime/configs" - "github.com/kubefirst/runtime/pkg/k8s" "github.com/rs/zerolog/log" - "github.com/spf13/viper" - "go.mongodb.org/mongo-driver/bson/primitive" - v1secret "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// CreateK1ClusterDirectory +// String in slice returns true if the string is in the slice. +func StringInSlice(s string, slice []string) bool { + for _, e := range slice { + if e == s { + return true + } + } + + return false +} + func CreateK1ClusterDirectory(clusterName string) { // Create k1 dir if it doesn't exist homePath, err := os.UserHomeDir() @@ -44,252 +40,24 @@ func CreateK1ClusterDirectory(clusterName string) { } } -func CreateClusterRecordFromRaw( - useTelemetry bool, - gitOwner string, - gitUser string, - gitToken string, - gitlabOwnerGroupID int, - gitopsTemplateURL string, - gitopsTemplateBranch string, - catalogApps []apiTypes.GitopsCatalogApp, -) apiTypes.Cluster { - cloudProvider := viper.GetString("kubefirst.cloud-provider") - domainName := viper.GetString("flags.domain-name") - gitProvider := viper.GetString("flags.git-provider") - - kubefirstTeam := os.Getenv("KUBEFIRST_TEAM") - if kubefirstTeam == "" { - kubefirstTeam = "false" - } - - cl := apiTypes.Cluster{ - ID: primitive.NewObjectID(), - CreationTimestamp: fmt.Sprintf("%v", time.Now().UTC()), - UseTelemetry: useTelemetry, - Status: "provisioned", - AlertsEmail: viper.GetString("flags.alerts-email"), - ClusterName: viper.GetString("flags.cluster-name"), - CloudProvider: cloudProvider, - CloudRegion: viper.GetString("flags.cloud-region"), - DomainName: domainName, - ClusterID: viper.GetString("kubefirst.cluster-id"), - ClusterType: "mgmt", - GitopsTemplateURL: gitopsTemplateURL, - GitopsTemplateBranch: gitopsTemplateBranch, - GitProvider: gitProvider, - GitHost: fmt.Sprintf("%s.com", gitProvider), - GitProtocol: viper.GetString("flags.git-protocol"), - DnsProvider: viper.GetString("flags.dns-provider"), - GitlabOwnerGroupID: gitlabOwnerGroupID, - AtlantisWebhookSecret: viper.GetString("secrets.atlantis-webhook"), - AtlantisWebhookURL: fmt.Sprintf("https://atlantis.%s/events", domainName), - KubefirstTeam: kubefirstTeam, - ArgoCDAuthToken: viper.GetString("components.argocd.auth-token"), - ArgoCDPassword: viper.GetString("components.argocd.password"), - PostInstallCatalogApps: catalogApps, - GitAuth: apiTypes.GitAuth{ - Token: gitToken, - User: gitUser, - Owner: gitOwner, - PublicKey: viper.GetString("kbot.public-key"), - PrivateKey: viper.GetString("kbot.private-key"), - }, - CloudflareAuth: apiTypes.CloudflareAuth{ - Token: os.Getenv("CF_API_TOKEN"), - }, - } - - switch cloudProvider { - case "civo": - cl.CivoAuth.Token = os.Getenv("CIVO_TOKEN") - case "aws": - // ToDo: where to get credentials? - cl.AWSAuth.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") - cl.AWSAuth.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") - cl.AWSAuth.SessionToken = viper.GetString("kubefirst.state-store-creds.token") - case "digitalocean": - cl.DigitaloceanAuth.Token = os.Getenv("DO_TOKEN") - cl.DigitaloceanAuth.SpacesKey = os.Getenv("DO_SPACES_KEY") - cl.DigitaloceanAuth.SpacesSecret = os.Getenv("DO_SPACES_SECRET") - case "vultr": - cl.VultrAuth.Token = os.Getenv("VULTR_API_KEY") - } - - cl.StateStoreCredentials.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") - cl.StateStoreCredentials.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") - cl.StateStoreCredentials.SessionToken = viper.GetString("kubefirst.state-store-creds.token") - cl.StateStoreCredentials.Name = viper.GetString("kubefirst.state-store-creds.name") - cl.StateStoreCredentials.ID = viper.GetString("kubefirst.state-store-creds.id") - - cl.StateStoreDetails.ID = viper.GetString("kubefirst.state-store.id") - cl.StateStoreDetails.Name = viper.GetString("kubefirst.state-store.name") - cl.StateStoreDetails.Hostname = viper.GetString("kubefirst.state-store.hostname") - cl.StateStoreDetails.AWSArtifactsBucket = viper.GetString("kubefirst.artifacts-bucket") - cl.StateStoreDetails.AWSStateStoreBucket = viper.GetString("kubefirst.state-store-bucket") - - return cl -} - -func CreateClusterDefinitionRecordFromRaw(gitAuth apiTypes.GitAuth, cliFlags types.CliFlags, catalogApps []apiTypes.GitopsCatalogApp) apiTypes.ClusterDefinition { - cloudProvider := viper.GetString("kubefirst.cloud-provider") - domainName := viper.GetString("flags.domain-name") - gitProvider := viper.GetString("flags.git-provider") - - kubefirstTeam := os.Getenv("KUBEFIRST_TEAM") - if kubefirstTeam == "" { - kubefirstTeam = "false" - } - - stringToIntNodeCount, err := strconv.Atoi(cliFlags.NodeCount) - if err != nil { - log.Info().Msg("Unable to convert node count to type string") - } - - cl := apiTypes.ClusterDefinition{ - AdminEmail: viper.GetString("flags.alerts-email"), - ClusterName: viper.GetString("flags.cluster-name"), - CloudProvider: cloudProvider, - CloudRegion: viper.GetString("flags.cloud-region"), - DomainName: domainName, - SubdomainName: cliFlags.SubDomainName, - Type: "mgmt", - NodeType: cliFlags.NodeType, - NodeCount: stringToIntNodeCount, - GitopsTemplateURL: cliFlags.GitopsTemplateURL, - GitopsTemplateBranch: cliFlags.GitopsTemplateBranch, - GitProvider: gitProvider, - GitProtocol: viper.GetString("flags.git-protocol"), - DnsProvider: viper.GetString("flags.dns-provider"), - LogFileName: viper.GetString("k1-paths.log-file-name"), - PostInstallCatalogApps: catalogApps, - GitAuth: apiTypes.GitAuth{ - Token: gitAuth.Token, - User: gitAuth.User, - Owner: gitAuth.Owner, - PublicKey: viper.GetString("kbot.public-key"), - PrivateKey: viper.GetString("kbot.private-key"), - }, - CloudflareAuth: apiTypes.CloudflareAuth{ - APIToken: os.Getenv("CF_API_TOKEN"), - }, - } - - if cl.GitopsTemplateBranch == "" { - cl.GitopsTemplateBranch = configs.K1Version - - if configs.K1Version == "development" { - cl.GitopsTemplateBranch = "main" - } - } - - switch cloudProvider { - case "akamai": - cl.AkamaiAuth.Token = os.Getenv("LINODE_TOKEN") - case "aws": - // ToDo: where to get credentials? - cl.AWSAuth.AccessKeyID = viper.GetString("kubefirst.state-store-creds.access-key-id") - cl.AWSAuth.SecretAccessKey = viper.GetString("kubefirst.state-store-creds.secret-access-key-id") - cl.AWSAuth.SessionToken = viper.GetString("kubefirst.state-store-creds.token") - cl.ECR = cliFlags.Ecr - case "civo": - cl.CivoAuth.Token = os.Getenv("CIVO_TOKEN") - case "digitalocean": - cl.DigitaloceanAuth.Token = os.Getenv("DO_TOKEN") - cl.DigitaloceanAuth.SpacesKey = os.Getenv("DO_SPACES_KEY") - cl.DigitaloceanAuth.SpacesSecret = os.Getenv("DO_SPACES_SECRET") - case "vultr": - cl.VultrAuth.Token = os.Getenv("VULTR_API_KEY") - case "k3s": - cl.K3sAuth.K3sServersPrivateIps = viper.GetStringSlice("flags.servers-private-ips") - cl.K3sAuth.K3sServersPublicIps = viper.GetStringSlice("flags.servers-public-ips") - cl.K3sAuth.K3sSshUser = viper.GetString("flags.ssh-user") - cl.K3sAuth.K3sSshPrivateKey = viper.GetString("flags.ssh-privatekey") - cl.K3sAuth.K3sServersArgs = viper.GetStringSlice("flags.servers-args") - case "google": - jsonFilePath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") - - jsonFile, err := os.Open(jsonFilePath) +// CreateDirIfNotExists creates a directory if it doesn't exists already. +func CreateDirIfNotExists(d string) { + if _, err := os.Stat(d); os.IsNotExist(err) { + err := os.MkdirAll(d, os.ModePerm) if err != nil { - progress.Error("Unable to read GOOGLE_APPLICATION_CREDENTIALS file") + log.Info().Msgf("%s directory already exists, continuing", d) } - defer jsonFile.Close() - - jsonContent, _ := ioutil.ReadAll(jsonFile) - - cl.GoogleAuth.KeyFile = string(jsonContent) - cl.GoogleAuth.ProjectId = cliFlags.GoogleProject } - - return cl } -func ExportCluster(cluster apiTypes.Cluster, kcfg *k8s.KubernetesClient) error { - cluster.Status = "provisioned" - cluster.InProgress = false - - if viper.GetBool("kubefirst-checks.secret-export-state") { - return nil - } - - time.Sleep(time.Second * 10) - - bytes, err := json.Marshal(cluster) - if err != nil { - log.Error().Msg(err.Error()) - return err +// EnvOrDefault returns the value of the specified env var or the default value. +func EnvOrDefault(env, def string) string { + v, ok := os.LookupEnv(env) + if !ok { + return def } - secretValuesMap, _ := ParseJSONToMap(string(bytes)) - - secret := &v1secret.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "kubefirst-initial-state", Namespace: "kubefirst"}, - Data: secretValuesMap, - } - - err = k8s.CreateSecretV2(kcfg.Clientset, secret) - - if err != nil { - return fmt.Errorf(fmt.Sprintf("unable to save secret to management cluster. %s", err)) - } - - viper.Set("kubefirst-checks.secret-export-state", true) - viper.WriteConfig() - - return nil -} - -func ConsumeStream(url string) { - client := &http.Client{} - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - fmt.Println("Error creating request:", err) - return - } - - resp, err := client.Do(req) - if err != nil { - fmt.Println("Error making request:", err) - return - } - - if resp.StatusCode != http.StatusOK { - fmt.Println("Error response:", resp.Status) - return - } - - // Read and print the streamed data until done signal is received - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - data := scanner.Text() - log.Info().Msgf(data) - } - - if err := scanner.Err(); err != nil { - log.Error().Msgf("Error reading response: %s", err.Error()) - return - } + return v } func ParseJSONToMap(jsonStr string) (map[string][]byte, error) { diff --git a/internal/utilities/utilities_test.go b/internal/utilities/utilities_test.go new file mode 100644 index 000000000..15e9c0e8d --- /dev/null +++ b/internal/utilities/utilities_test.go @@ -0,0 +1,48 @@ +package utilities + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnvOrDefault(t *testing.T) { + require.Equal(t, EnvOrDefault("INVALID", "default"), "default") + + t.Setenv("ENV", "123") + + require.Equal(t, EnvOrDefault("ENV", "default"), "123") +} + +func TestStringInSlice(t *testing.T) { + require.Equal(t, StringInSlice("test", []string{"test", "test2"}), true) + require.Equal(t, StringInSlice("test", []string{"test2"}), false) +} + +func TestCreateDirIfNotExists(t *testing.T) { + CreateDirIfNotExists("test") + + require.DirExists(t, "test") + + t.Cleanup(func() { + os.Remove("test") + }) +} + +func TestParseJSONToMap(t *testing.T) { + str := `{ + "key": "value" +}` + + b, _ := json.Marshal("value") + exp := map[string][]byte{ + "key": b, + } + + res, err := ParseJSONToMap(str) + + require.NoError(t, err) + require.Equal(t, exp, res) +} diff --git a/main.go b/main.go index 8dd0e833d..1c292292b 100644 --- a/main.go +++ b/main.go @@ -8,136 +8,16 @@ package main import ( "fmt" - stdLog "log" "os" - "time" - - "golang.org/x/exp/slices" - - zeroLog "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/kubefirst/kubefirst/cmd" - "github.com/kubefirst/kubefirst/internal/progress" - "github.com/kubefirst/runtime/configs" - "github.com/kubefirst/runtime/pkg" - "github.com/spf13/viper" ) func main() { - argsWithProg := os.Args - - bubbleTeaBlacklist := []string{"completion", "help", "--help", "-h", "quota", "logs"} - canRunBubbleTea := true - - if argsWithProg != nil { - for _, arg := range argsWithProg { - isBlackListed := slices.Contains(bubbleTeaBlacklist, arg) - - if isBlackListed { - canRunBubbleTea = false - } - } - } - - config := configs.ReadConfig() - if err := pkg.SetupViper(config, true); err != nil { - stdLog.Panic(err) - } - - now := time.Now() - epoch := now.Unix() - logfileName := fmt.Sprintf("log_%d.log", epoch) - - isProvision := slices.Contains(argsWithProg, "create") - isLogs := slices.Contains(argsWithProg, "logs") - - // don't create a new log file for logs, using the previous one - if isLogs { - logfileName = viper.GetString("k1-paths.log-file-name") - } - - // use cluster name as filename - if isProvision { - clusterName := fmt.Sprint(epoch) - for i := 1; i < len(os.Args); i++ { - arg := os.Args[i] - - // Check if the argument is "--cluster-name" - if arg == "--cluster-name" && i+1 < len(os.Args) { - // Get the value of the cluster name - clusterName = os.Args[i+1] - break - } - } - - logfileName = fmt.Sprintf("log_%s.log", clusterName) - } - - homePath, err := os.UserHomeDir() - if err != nil { - log.Info().Msg(err.Error()) - } - - k1Dir := fmt.Sprintf("%s/.k1", homePath) + if err := cmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "[ERROR] %v.\n", err) + fmt.Fprintf(os.Stderr, "\nIf a detailed error message was available, please make the necessary corrections before retrying.\nYou can re-run the last command to try the operation again.\n\n") - //* create k1Dir if it doesn't exist - if _, err := os.Stat(k1Dir); os.IsNotExist(err) { - err := os.MkdirAll(k1Dir, os.ModePerm) - if err != nil { - log.Info().Msgf("%s directory already exists, continuing", k1Dir) - } + os.Exit(1) } - - //* create log directory - logsFolder := fmt.Sprintf("%s/logs", k1Dir) - _ = os.Mkdir(logsFolder, 0700) - if err != nil { - log.Fatal().Msgf("error creating logs directory: %s", err) - } - - //* create session log file - logfile := fmt.Sprintf("%s/%s", logsFolder, logfileName) - logFileObj, err := pkg.OpenLogFile(logfile) - if err != nil { - stdLog.Panicf("unable to store log location, error is: %s - please verify the current user has write access to this directory", err) - } - - // handle file close request - defer func(logFileObj *os.File) { - err = logFileObj.Close() - if err != nil { - log.Print(err) - } - }(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) - - log.Logger = zeroLog.New(logFileObj).With().Timestamp().Logger() - - viper.Set("k1-paths.logs-dir", logsFolder) - viper.Set("k1-paths.log-file", logfile) - viper.Set("k1-paths.log-file-name", logfileName) - - err = viper.WriteConfig() - if err != nil { - stdLog.Panicf("unable to set log-file-location, error is: %s", err) - } - - if canRunBubbleTea { - progress.InitializeProgressTerminal() - - go func() { - cmd.Execute() - }() - - progress.Progress.Run() - } else { - cmd.Execute() - } - }