diff --git a/cmd/azure/command.go b/cmd/azure/command.go new file mode 100644 index 00000000..3eee4379 --- /dev/null +++ b/cmd/azure/command.go @@ -0,0 +1,140 @@ +/* +Copyright (C) 2021-2024, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ + +package azure + +import ( + "fmt" + + "github.com/konstructio/kubefirst-api/pkg/constants" + "github.com/konstructio/kubefirst/internal/common" + "github.com/konstructio/kubefirst/internal/progress" + "github.com/spf13/cobra" +) + +var ( + // Create + alertsEmailFlag string + ciFlag bool + cloudRegionFlag string + clusterNameFlag string + clusterTypeFlag string + dnsProviderFlag string + domainNameFlag string + subdomainNameFlag string + githubOrgFlag string + gitlabGroupFlag string + gitProviderFlag string + gitProtocolFlag string + gitopsTemplateURLFlag string + gitopsTemplateBranchFlag string + useTelemetryFlag bool + forceDestroyFlag bool + nodeTypeFlag string + nodeCountFlag string + installCatalogApps string + installKubefirstProFlag bool + + // RootCredentials + copyArgoCDPasswordToClipboardFlag bool + copyKbotPasswordToClipboardFlag bool + copyVaultPasswordToClipboardFlag bool + + // Supported providers + supportedDNSProviders = []string{"azure", "cloudflare"} + supportedGitProviders = []string{"github", "gitlab"} + + // Supported git providers + supportedGitProtocolOverride = []string{"https", "ssh"} +) + +func NewCommand() *cobra.Command { + azureCmd := &cobra.Command{ + Use: "azure", + Short: "kubefirst Azure installation", + Long: "kubefirst azure", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("To learn more about azure in kubefirst, run:") + fmt.Println(" kubefirst beta azure --help") + + if progress.Progress != nil { + progress.Progress.Quit() + } + }, + } + + // on error, doesnt show helper/usage + azureCmd.SilenceUsage = true + + // wire up new commands + azureCmd.AddCommand(Create(), Destroy(), RootCredentials()) + + return azureCmd +} + +func Create() *cobra.Command { + createCmd := &cobra.Command{ + Use: "create", + Short: "create the kubefirst platform running on GCP kubernetes", + TraverseChildren: true, + RunE: createAzure, + } + + azureDefaults := constants.GetCloudDefaults().Azure + + // todo review defaults and update descriptions + createCmd.Flags().StringVar(&alertsEmailFlag, "alerts-email", "", "email address for let's encrypt certificate notifications (required)") + createCmd.MarkFlagRequired("alerts-email") + createCmd.Flags().BoolVar(&ciFlag, "ci", false, "if running kubefirst in ci, set this flag to disable interactive features") + createCmd.Flags().StringVar(&cloudRegionFlag, "cloud-region", "eastus", "the GCP region to provision infrastructure in") + createCmd.Flags().StringVar(&clusterNameFlag, "cluster-name", "kubefirst", "the name of the cluster to create") + createCmd.Flags().StringVar(&clusterTypeFlag, "cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") + createCmd.Flags().StringVar(&nodeCountFlag, "node-count", azureDefaults.NodeCount, "the node count for the cluster") + createCmd.Flags().StringVar(&nodeTypeFlag, "node-type", azureDefaults.InstanceSize, "the instance size of the cluster to create") + createCmd.Flags().StringVar(&dnsProviderFlag, "dns-provider", "azure", fmt.Sprintf("the dns provider - one of: %s", supportedDNSProviders)) + createCmd.Flags().StringVar(&subdomainNameFlag, "subdomain", "", "the subdomain to use for DNS records (Cloudflare)") + createCmd.Flags().StringVar(&domainNameFlag, "domain-name", "", "the Azure DNS zone to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") + createCmd.MarkFlagRequired("domain-name") + createCmd.Flags().StringVar(&gitProviderFlag, "git-provider", "github", fmt.Sprintf("the git provider - one of: %s", supportedGitProviders)) + createCmd.Flags().StringVar(&gitProtocolFlag, "git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %s", supportedGitProtocolOverride)) + createCmd.Flags().StringVar(&githubOrgFlag, "github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") + createCmd.Flags().StringVar(&gitlabGroupFlag, "gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") + createCmd.Flags().StringVar(&gitopsTemplateBranchFlag, "gitops-template-branch", "", "the branch to clone for the gitops-template repository") + createCmd.Flags().StringVar(&gitopsTemplateURLFlag, "gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") + createCmd.Flags().StringVar(&installCatalogApps, "install-catalog-apps", "", "comma separated values to install after provision") + createCmd.Flags().BoolVar(&useTelemetryFlag, "use-telemetry", true, "whether to emit telemetry") + createCmd.Flags().BoolVar(&forceDestroyFlag, "force-destroy", false, "allows force destruction on objects (helpful for test environments, defaults to false)") + createCmd.Flags().BoolVar(&installKubefirstProFlag, "install-kubefirst-pro", true, "whether or not to install kubefirst pro") + + return createCmd +} + +func Destroy() *cobra.Command { + destroyCmd := &cobra.Command{ + Use: "destroy", + Short: "destroy the kubefirst platform", + Long: "destroy the kubefirst platform running in Azure and remove all resources", + RunE: common.Destroy, + } + + return destroyCmd +} + +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: common.GetRootCredentials, + } + + 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 +} diff --git a/cmd/azure/create.go b/cmd/azure/create.go new file mode 100644 index 00000000..a443decb --- /dev/null +++ b/cmd/azure/create.go @@ -0,0 +1,141 @@ +/* +Copyright (C) 2021-2024, Kubefirst + +This program is licensed under MIT. +See the LICENSE file for more details. +*/ +package azure + +import ( + "fmt" + "os" + "strings" + + internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" + utils "github.com/konstructio/kubefirst-api/pkg/utils" + "github.com/konstructio/kubefirst/internal/catalog" + "github.com/konstructio/kubefirst/internal/cluster" + "github.com/konstructio/kubefirst/internal/gitShim" + "github.com/konstructio/kubefirst/internal/launch" + "github.com/konstructio/kubefirst/internal/progress" + "github.com/konstructio/kubefirst/internal/provision" + "github.com/konstructio/kubefirst/internal/utilities" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Environment variables required for authentication. This should be a +// service principal - the Terraform provider docs detail how to create +// one +// @link https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret.html +var envvarSecrets = []string{ + "ARM_CLIENT_ID", + "ARM_CLIENT_SECRET", + "ARM_TENANT_ID", + "ARM_SUBSCRIPTION_ID", +} + +func createAzure(cmd *cobra.Command, args []string) error { + cliFlags, err := utilities.GetFlags(cmd, "azure") + if err != nil { + progress.Error(err.Error()) + return nil + } + + progress.DisplayLogHints(20) + + isValid, catalogApps, err := catalog.ValidateCatalogApps(cliFlags.InstallCatalogApps) + if !isValid { + return err + } + + err = ValidateProvidedFlags(cliFlags.GitProvider) + if err != nil { + progress.Error(err.Error()) + return nil + } + + // If cluster setup is complete, return + clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") + if clusterSetupComplete { + err = fmt.Errorf("this cluster install process has already completed successfully") + progress.Error(err.Error()) + return nil + } + + utilities.CreateK1ClusterDirectory(clusterNameFlag) + + gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) + if err != nil { + progress.Error(err.Error()) + return nil + } + + executionControl := viper.GetBool(fmt.Sprintf("kubefirst-checks.%s-credentials", cliFlags.GitProvider)) + if !executionControl { + newRepositoryNames := []string{"gitops", "metaphor"} + newTeamNames := []string{"admins", "developers"} + + initGitParameters := gitShim.GitInitParameters{ + GitProvider: gitProviderFlag, + GitToken: gitAuth.Token, + GitOwner: gitAuth.Owner, + Repositories: newRepositoryNames, + Teams: newTeamNames, + } + err = gitShim.InitializeGitProvider(&initGitParameters) + if err != nil { + progress.Error(err.Error()) + return nil + } + } + + viper.Set(fmt.Sprintf("kubefirst-checks.%s-credentials", cliFlags.GitProvider), true) + viper.WriteConfig() + + k3dClusterCreationComplete := viper.GetBool("launch.deployed") + isK1Debug := strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" + + if !k3dClusterCreationComplete && !isK1Debug { + launch.Up(nil, true, cliFlags.UseTelemetry) + } + + err = utils.IsAppAvailable(fmt.Sprintf("%s/api/proxyHealth", cluster.GetConsoleIngressURL()), "kubefirst api") + if err != nil { + progress.Error("unable to start kubefirst api") + } + + provision.CreateMgmtCluster(gitAuth, cliFlags, catalogApps) + + return nil +} + +func ValidateProvidedFlags(gitProvider string) error { + progress.AddStep("Validate provided flags") + + for _, env := range envvarSecrets { + if os.Getenv(env) == "" { + return fmt.Errorf("your %s is not set - please set and re-run your last command", env) + } + } + + switch gitProvider { + 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()) + } + } + + return nil +} diff --git a/cmd/beta.go b/cmd/beta.go index 9afd9155..e5154206 100644 --- a/cmd/beta.go +++ b/cmd/beta.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/konstructio/kubefirst/cmd/akamai" + "github.com/konstructio/kubefirst/cmd/azure" "github.com/konstructio/kubefirst/cmd/google" "github.com/konstructio/kubefirst/cmd/k3s" "github.com/konstructio/kubefirst/cmd/vultr" @@ -36,6 +37,7 @@ func init() { cobra.OnInitialize() betaCmd.AddCommand( akamai.NewCommand(), + azure.NewCommand(), k3s.NewCommand(), google.NewCommand(), vultr.NewCommand(), diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go index 2d942c4f..a562c7e8 100644 --- a/internal/utilities/utilities.go +++ b/internal/utilities/utilities.go @@ -107,6 +107,11 @@ func CreateClusterRecordFromRaw( 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 "azure": + cl.AzureAuth.ClientID = os.Getenv("ARM_CLIENT_ID") + cl.AzureAuth.ClientSecret = os.Getenv("ARM_CLIENT_SECRET") + cl.AzureAuth.TenantID = os.Getenv("ARM_TENANT_ID") + cl.AzureAuth.SubscriptionID = os.Getenv("ARM_SUBSCRIPTION_ID") case "digitalocean": cl.DigitaloceanAuth.Token = os.Getenv("DO_TOKEN") cl.DigitaloceanAuth.SpacesKey = os.Getenv("DO_SPACES_KEY") @@ -191,6 +196,11 @@ func CreateClusterDefinitionRecordFromRaw(gitAuth apiTypes.GitAuth, cliFlags typ 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 "azure": + cl.AzureAuth.ClientID = os.Getenv("ARM_CLIENT_ID") + cl.AzureAuth.ClientSecret = os.Getenv("ARM_CLIENT_SECRET") + cl.AzureAuth.TenantID = os.Getenv("ARM_TENANT_ID") + cl.AzureAuth.SubscriptionID = os.Getenv("ARM_SUBSCRIPTION_ID") case "civo": cl.CivoAuth.Token = os.Getenv("CIVO_TOKEN") case "digitalocean":