diff --git a/go/deploy-cli/.editorconfig b/go/deploy-cli/.editorconfig new file mode 100644 index 000000000..2d5a9b7bb --- /dev/null +++ b/go/deploy-cli/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +#insert_final_newline = true +trim_trailing_whitespace = true diff --git a/go/deploy-cli/.gitignore b/go/deploy-cli/.gitignore new file mode 100644 index 000000000..874a87cd9 --- /dev/null +++ b/go/deploy-cli/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go hugo -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# VsCode + +.vsscode/* +.vscode/ + +# goreleaser dist folder +dist/ + +# IntelliJ +.idea +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches diff --git a/go/deploy-cli/.goreleaser.yaml b/go/deploy-cli/.goreleaser.yaml new file mode 100644 index 000000000..59608abfa --- /dev/null +++ b/go/deploy-cli/.goreleaser.yaml @@ -0,0 +1,48 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: ["linux", "darwin", "windows"] + goarch: ["386", "amd64", "arm64"] + binary: deploy-cli + ldflags: + - -s -w -X "main.version={{.Version}}" + +universal_binaries: + - replace: true + +archives: + - formats: [tar.gz] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + formats: [zip] + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/go/deploy-cli/README.md b/go/deploy-cli/README.md new file mode 100644 index 000000000..298155283 --- /dev/null +++ b/go/deploy-cli/README.md @@ -0,0 +1,34 @@ +# undercloud-deploy-cli + +Undercloud Deploy cli helps quickly generate undercloud-deploy config files. + + +## How to use. + +> Please make sure that you have kubeseal binary installed your system https://github.com/bitnami-labs/sealed-secrets + +* Export all these env + +```sh +export UC_DEPLOY="" +export DEPLOY_NAME="" +export UC_DEPLOY_GIT_URL=git@github.com:RSS-Engineering/undercloud-deploy.git +export UC_DEPLOY_SSH_FILE= +export DNS_ZONE=.dev.undercloud.rackspace.net +export UC_DEPLOY_EMAIL="" +export UC_AIO=yes +``` + +* Run + +``` +go run *.go init +go run *.go help +``` + +* Commit all changes to the undercloud-deploy repo in your branch + + +## Contributing + +If you find any issues or have suggestions for improvements, please open an issue on the [GitHub repository](https://github.com/rackerlabs/understack). diff --git a/go/deploy-cli/cmd/argocd/secrets.go b/go/deploy-cli/cmd/argocd/secrets.go new file mode 100644 index 000000000..ae856cee1 --- /dev/null +++ b/go/deploy-cli/cmd/argocd/secrets.go @@ -0,0 +1,110 @@ +package argocd + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + "github.com/gookit/goutil/fsutil" + "github.com/spf13/cobra" +) + +//go:embed templates/argoCluster.tmpl +var argoClusterTemplate string + +//go:embed templates/argoSecretDeployRepo.tmpl +var argoSecretDeployRepoTemplate string + +// Constants for file paths and template names +const ( + clusterSecretFile = "secret-%s-cluster.yaml" + secretDeployRepoFile = "secret-deploy-repo.yaml" + argoNamespace = "argocd" +) + +var ArgoCMD = &cobra.Command{ + Use: "argocd-secrets", + Short: "Generate ArgoCD secrets", + Long: "Generate repository and cluster secrets for ArgoCD deployment", + Run: func(cmd *cobra.Command, args []string) { + if err := GenerateSecrets(); err != nil { + log.Fatal("Failed to generate secrets", "error", err) + os.Exit(1) + } + }, +} + +func init() { + cmd.RootCmd.AddCommand(ArgoCMD) +} + +// GenerateSecrets orchestrates the generation of all ArgoCD secrets +func GenerateSecrets() error { + basePath := helpers.GetManifestPathToService("argocd") + + if err := generateDeployRepoSecret(basePath); err != nil { + return fmt.Errorf("deploy repo secret generation failed: %w", err) + } + + if err := generateClusterSecret(basePath); err != nil { + return fmt.Errorf("cluster secret generation failed: %w", err) + } + + helpers.UpdateKustomizeFile(basePath) + return nil +} + +// generateDeployRepoSecret generates the repository deployment secret +func generateDeployRepoSecret(basePath string) error { + vars := map[string]any{ + "Config": `{"tlsClientConfig":{"insecure":false}}`, + "Name": envutil.Getenv("UC_DEPLOY"), + "Server": "https://kubernetes.default.svc", + "DEPLOY_NAME": envutil.Getenv("DEPLOY_NAME"), + "UC_DEPLOY_GIT_URL": envutil.Getenv("UC_DEPLOY_GIT_URL"), + "DNS_ZONE": envutil.Getenv("DNS_ZONE"), + } + + result, err := helpers.TemplateHelper(argoSecretDeployRepoTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering failed: %w", err) + } + + outputFilePath := filepath.Join(basePath, fmt.Sprintf(clusterSecretFile, envutil.Getenv("DEPLOY_NAME"))) + + return writeToFile(outputFilePath, result) +} + +// generateClusterSecret generates the cluster secret +func generateClusterSecret(basePath string) error { + vars := map[string]any{ + "DEPLOY_NAME": envutil.Getenv("DEPLOY_NAME"), + "Type": "git", + "UC_DEPLOY_GIT_URL": envutil.Getenv("UC_DEPLOY_GIT_URL"), + "DNS_ZONE": envutil.Getenv("DNS_ZONE"), + "UC_DEPLOY_SSH_FILE": envutil.Getenv("UC_DEPLOY_SSH_FILE"), + } + + result, err := helpers.TemplateHelper(argoClusterTemplate, vars) + if err != nil { + return fmt.Errorf("template rendering failed: %w", err) + } + + outputFilePath := filepath.Join(basePath, secretDeployRepoFile) + return writeToFile(outputFilePath, result) +} + +// writeToFile writes content to a file with proper permissions +func writeToFile(filePath string, content string) error { + err := fsutil.WriteFile(filePath, content, os.ModePerm) + if err != nil { + return fmt.Errorf("file write failed: %w", err) + } + return nil +} diff --git a/go/deploy-cli/cmd/argocd/templates/argoCluster.tmpl b/go/deploy-cli/cmd/argocd/templates/argoCluster.tmpl new file mode 100644 index 000000000..f1d48e8db --- /dev/null +++ b/go/deploy-cli/cmd/argocd/templates/argoCluster.tmpl @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .DEPLOY_NAME }}-repo + namespace: argocd + labels: + argocd.argoproj.io/secret-type: repo-creds +data: + sshPrivateKey: {{ .UC_DEPLOY_SSH_FILE }} + type: {{ .Type | b64enc }} + url: {{ .UC_DEPLOY_GIT_URL | b64enc }} diff --git a/go/deploy-cli/cmd/argocd/templates/argoSecretDeployRepo.tmpl b/go/deploy-cli/cmd/argocd/templates/argoSecretDeployRepo.tmpl new file mode 100644 index 000000000..06a9944c5 --- /dev/null +++ b/go/deploy-cli/cmd/argocd/templates/argoSecretDeployRepo.tmpl @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Secret +data: + config: {{ .Config | b64enc }} + name: {{ .Name | b64enc }} + server: {{ .Server | b64enc }} +metadata: + name: {{ .DEPLOY_NAME }}-cluster + namespace: argocd + labels: + argocd.argoproj.io/secret-type: cluster + understack.rackspace.com/argocd: enabled + annotations: + uc_repo_git_url: "https://github.com/rackerlabs/understack.git" + uc_repo_ref: "HEAD" + uc_deploy_git_url: "{{ .UC_DEPLOY_GIT_URL }}" + uc_deploy_ref: "HEAD" + dns_zone: "{{ .DNS_ZONE }}" diff --git a/go/deploy-cli/cmd/certManager/secrets.go b/go/deploy-cli/cmd/certManager/secrets.go new file mode 100644 index 000000000..44248d632 --- /dev/null +++ b/go/deploy-cli/cmd/certManager/secrets.go @@ -0,0 +1,60 @@ +package certManager + +import ( + _ "embed" + "fmt" + "os" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + "github.com/gookit/goutil/fsutil" + + "github.com/spf13/cobra" +) + +//go:embed templates/clusterIssuer.tmpl +var clusterIssuerTemplate string + +func init() { + cmd.RootCmd.AddCommand(CertManager) +} + +var CertManager = &cobra.Command{ + Use: "certmanager-secrets", + Short: "Generate certmanager-secrets secrets", + Long: "", + Run: certManagerGen, +} + +func certManagerGen(cmd *cobra.Command, args []string) { + err := clusterIssuer() + if err != nil { + log.Error("certManagerGen failed", "error", err) + } +} + +// credGen prints out the cli version number +func clusterIssuer() error { + vars := map[string]any{ + "UC_DEPLOY_EMAIL": envutil.Getenv("UC_DEPLOY_EMAIL"), + "DNS_ZONE": envutil.Getenv("DNS_ZONE"), + } + + result, err := helpers.TemplateHelper(string(clusterIssuerTemplate), vars) + if err != nil { + return fmt.Errorf("template rendering failed: %w", err) + } + + outputFilePath := helpers.GetManifestPathToService("cert-manager") + "/cluster-issuer.yaml" + + if err := fsutil.WriteFile(outputFilePath, result, os.ModePerm); err != nil { + log.Fatal("error in kustomization.yaml file", "err", err) + os.Exit(1) + } + helpers.UpdateKustomizeFile(helpers.GetManifestPathToService("cert-manager")) + + return nil +} diff --git a/go/deploy-cli/cmd/certManager/templates/clusterIssuer.tmpl b/go/deploy-cli/cmd/certManager/templates/clusterIssuer.tmpl new file mode 100644 index 000000000..7b0f37c55 --- /dev/null +++ b/go/deploy-cli/cmd/certManager/templates/clusterIssuer.tmpl @@ -0,0 +1,27 @@ +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: understack-cluster-issuer + annotations: + argocd.argoproj.io/sync-wave: "5" +spec: + acme: + email: {{ .UC_DEPLOY_EMAIL }} + privateKeySecretRef: + name: letsencrypt-prod + server: https://acme-v02.api.letsencrypt.org/directory + solvers: + - http01: + ingress: + ingressClassName: nginx + selector: + matchLabels: + authorizeWith: http + - dns01: + webhook: + groupName: acme.undercloud.rackspace.net + solverName: rackspace + config: + authSecretRef: cert-manager-webhook-rackspace-creds + domainName: {{ .DNS_ZONE }} diff --git a/go/deploy-cli/cmd/dex/secrets.go b/go/deploy-cli/cmd/dex/secrets.go new file mode 100644 index 000000000..af01433de --- /dev/null +++ b/go/deploy-cli/cmd/dex/secrets.go @@ -0,0 +1,61 @@ +package dex + +import ( + "fmt" + "path/filepath" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + + "github.com/spf13/cobra" +) + +func init() { + cmd.RootCmd.AddCommand(Dex) +} + +var Dex = &cobra.Command{ + Use: "dex-secrets", + Short: "Create dex secret for nautobot, argo, argocd, keystone, grafana", + Long: "Create dex secret for nautobot, argo, argocd, keystone, grafana", + Run: generateDexSecrets, +} + +func generateDexSecrets(cmd *cobra.Command, args []string) { + if err := generateDexServiceSecrets(); err != nil { + log.Error("Failed to generate secrets for dex", "err", err) + } +} + +// credGen prints out the cli version number +func generateDexServiceSecrets() error { + clients := []string{"nautobot", "argo", "argocd", "keystone", "grafana"} + + manifestPath := helpers.GetManifestPathToService("dex") + + for _, client := range clients { + + config := helpers.SecretConfig{ + Name: fmt.Sprintf("%s-sso", client), + Namespace: "dex", + Data: map[string]string{ + "client-secret": helpers.GenerateRandomString(32), + "client-id": client, + "issuer": fmt.Sprintf("https://dex.%s", envutil.Getenv("DNS_ZONE")), + }, + } + + outputFilePath := manifestPath + fmt.Sprintf("/secret-%s-sso-dex.yaml", client) + + if err := helpers.CreateKubeSealSecretFile(config, outputFilePath); err != nil { + return err + } + } + + helpers.UpdateKustomizeFile(filepath.Join(envutil.Getenv("UC_DEPLOY"), envutil.Getenv("DEPLOY_NAME"), "manifests", "dex")) + + return nil +} diff --git a/go/deploy-cli/cmd/helmConfig/helmConfig.go b/go/deploy-cli/cmd/helmConfig/helmConfig.go new file mode 100644 index 000000000..aec7be095 --- /dev/null +++ b/go/deploy-cli/cmd/helmConfig/helmConfig.go @@ -0,0 +1,222 @@ +package helmConfig + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/gookit/goutil/envutil" + "github.com/gookit/goutil/fsutil" + "github.com/spf13/cobra" +) + +func init() { + cmd.RootCmd.AddCommand(HelmConfig) +} + +var HelmConfig = &cobra.Command{ + Use: "helm-config", + Short: "Create helm config for individual services", + Long: "", + Run: helmConfigGen, +} + +func helmConfigGen(cmd *cobra.Command, args []string) { + dex() + glance() + ironic() + rook() +} + +func dex() error { + template := `config: + staticClients: + - id: nautobot + secretEnv: NAUTOBOT_SSO_CLIENT_SECRET + name: "Undercloud Nautobot" + redirectURIs: + - "https://nautobot.{{ .DNS_ZONE }}/complete/oidc/" + - id: argo + secretEnv: ARGO_SSO_CLIENT_SECRET + name: "Undercloud Argo" + redirectURIs: + - "https://workflows.{{ .DNS_ZONE }}/oauth2/callback" + - id: argocd + secretEnv: ARGOCD_SSO_CLIENT_SECRET + name: "Undercloud ArgoCD" + redirectURIs: + - "https://argocd.{{ .DNS_ZONE }}/auth/callback"` + + vars := map[string]any{ + "DNS_ZONE": envutil.Getenv("DNS_ZONE"), + } + + data, err := helpers.TemplateHelper(template, vars) + if err != nil { + return fmt.Errorf("failed to render template: %w", err) + } + + filePath := filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "helm", + "dex.yaml", + ) + + if err := fsutil.WriteFile(filePath, data, os.ModePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", filePath, err) + } + return nil +} + +func glance() error { + template := `volume: + class_name: csi-cinder-sc-delete + size: 20Gi` + + filePath := filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "helm", + "glance.yaml", + ) + + if err := fsutil.WriteFile(filePath, template, os.ModePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", filePath, err) + } + + return nil +} + +func ironic() error { + template := `labels: + conductor: + node_selector_key: "understack.node.cluster.x-k8s.io/ironic-role" + node_selector_value: conductor + + conductor: + initContainers: + - name: create-tmpdir + image: docker.io/openstackhelm/heat:2024.2-ubuntu_jammy + imagePullPolicy: IfNotPresent + command: [bash] + args: + - "-c" + - "mkdir -p /var/lib/openstack-helm/tmp" + volumeMounts: + - name: pod-data + mountPath: /var/lib/openstack-helm` + + filePath := filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "helm", + "ironic.yaml", + ) + + if err := fsutil.WriteFile(filePath, template, os.ModePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", filePath, err) + } + + return nil +} + +func rook() error { + template := `cephClusterSpec: + mon: + # Set the number of mons to be started. Generally recommended to be 3. + # For highest availability, an odd number of mons should be specified. + count: 2 + # The mons should be on unique nodes. For production, at least 3 nodes are recommended for this reason. + # Mons should only be allowed on the same node for test environments where data loss is acceptable. + allowMultiplePerNode: false + + mgr: + # When higher availability of the mgr is needed, increase the count to 2. + # In that case, one mgr will be active and one in standby. When Ceph updates which + # mgr is active, Rook will update the mgr services to match the active mgr. + count: 2 + allowMultiplePerNode: false + + storage: + useAllDevices: false + useAllNodes: true + deviceFilter: "vdb" + resources: + mgr: + limits: + memory: "1Gi" + requests: + cpu: "0" + memory: "512Mi" + mon: + limits: + memory: "2Gi" + requests: + cpu: "0" + memory: "1Gi" + osd: + limits: + memory: "4Gi" + requests: + cpu: "0" + memory: "4Gi" + prepareosd: + # limits: It is not recommended to set limits on the OSD prepare job + # since it's a one-time burst for memory that must be allowed to + # complete without an OOM kill. Note however that if a k8s + # limitRange guardrail is defined external to Rook, the lack of + # a limit here may result in a sync failure, in which case a + # limit should be added. 1200Mi may suffice for up to 15Ti + # OSDs ; for larger devices 2Gi may be required. + # cf. https://github.com/rook/rook/pull/11103 + requests: + cpu: "0" + memory: "50Mi" + mgr-sidecar: + limits: + memory: "100Mi" + requests: + cpu: "0" + memory: "40Mi" + crashcollector: + limits: + memory: "60Mi" + requests: + cpu: "0" + memory: "60Mi" + logcollector: + limits: + memory: "1Gi" + requests: + cpu: "0" + memory: "100Mi" + cleanup: + limits: + memory: "1Gi" + requests: + cpu: "0" + memory: "100Mi" + exporter: + limits: + memory: "128Mi" + requests: + cpu: "0" + memory: "50Mi"` + + filePath := filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "helm", + "rook-cluster.yaml", + ) + + if err := fsutil.WriteFile(filePath, template, os.ModePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", filePath, err) + } + + return nil +} diff --git a/go/deploy-cli/cmd/init/init.go b/go/deploy-cli/cmd/init/init.go new file mode 100644 index 000000000..faf597f8b --- /dev/null +++ b/go/deploy-cli/cmd/init/init.go @@ -0,0 +1,84 @@ +package ironic + +import ( + "fmt" + "os" + "os/exec" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/argocd" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/certManager" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/dex" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/helmConfig" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/node" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/openstack" + "github.com/rackerlabs/understack/go/deploy-cli/cmd/other" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + "github.com/spf13/cobra" +) + +func init() { + cmd.RootCmd.AddCommand(Init) +} + +var Init = &cobra.Command{ + Use: "init", + Short: "Run all the init the steps required", + Long: "Run all the init the steps required", + Run: initRun, +} + +func initRun(cmd *cobra.Command, args []string) { + + log.Info("using envs", + "UC_DEPLOY", envutil.Getenv("UC_DEPLOY"), + "DEPLOY_NAME", envutil.Getenv("DEPLOY_NAME"), + "UC_DEPLOY_GIT_URL", envutil.Getenv("UC_DEPLOY_GIT_URL"), + "UC_DEPLOY_SSH_FILE", envutil.Getenv("UC_DEPLOY_SSH_FILE"), + "DNS_ZONE", envutil.Getenv("DNS_ZONE"), + "UC_DEPLOY_EMAIL", envutil.Getenv("UC_DEPLOY_EMAIL"), + "UC_AIO", envutil.Getenv("UC_AIO"), + ) + + var errors []string + + // Check if kubeseal is installed + _, err := exec.LookPath("kubeseal") + if err != nil { + errors = append(errors, "kubeseal is not installed on system, please install kubeseal binary") + } + + // If there are any errors, report them all and exit + if len(errors) > 0 { + for _, errMsg := range errors { + log.Warn(errMsg) + } + os.Exit(1) + } + + fmt.Println(envutil.Getenv("UC_DEPLOY")) + fmt.Println(envutil.Getenv("DEPLOY_NAME")) + + log.Info("== Node Update") + node.Node.Run(cmd, args) + + log.Info("== Node ArgoCd") + argocd.ArgoCMD.Run(cmd, args) + + log.Info("== Node Cert Manager") + certManager.CertManager.Run(cmd, args) + + log.Info("== Running Dex") + dex.Dex.Run(cmd, args) + + log.Info("== Running For Other Services") + other.Other.Run(cmd, args) + + log.Info("== Running Openstack") + openstack.Openstack.Run(cmd, args) + + log.Info("== Creating Helm Configs") + helmConfig.HelmConfig.Run(cmd, args) +} diff --git a/go/deploy-cli/cmd/node/node.go b/go/deploy-cli/cmd/node/node.go new file mode 100644 index 000000000..44fc23e61 --- /dev/null +++ b/go/deploy-cli/cmd/node/node.go @@ -0,0 +1,62 @@ +package node + +import ( + "context" + "os" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + cmd.RootCmd.AddCommand(Node) +} + +var Node = &cobra.Command{ + Use: "node-update", + Short: "Will update k8s cluster node with labels and tags", + Long: "Will update k8s cluster node with labels and tags", + Run: updateNode, +} + +func updateNode(cmd *cobra.Command, args []string) { + if err := labelNodes(); err != nil { + log.Error("Failed to label nodes", "err", err) + os.Exit(1) + } +} + +func labelNodes() error { + clientset := helpers.KubeClientSet() + + nodes, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return err + } + + for _, node := range nodes.Items { + if _, exists := node.Labels["openstack-control-plane"]; exists { + log.Info("Node already labeled. Skipping", node.Name, node.Labels["openstack-control-plane"]) + continue + } + + log.Info("Labeling node " + node.Name) + + node.Labels["openstack-control-plane"] = "enabled" + + // Update the node + _, err := clientset.CoreV1().Nodes().Update(context.TODO(), &node, metav1.UpdateOptions{}) + if err != nil { + log.Error("Failed to label node", node.Name, err) + } else { + log.Info("Successfully labeled node", node.Name) + } + } + + return nil +} diff --git a/go/deploy-cli/cmd/openstack/secrets.go b/go/deploy-cli/cmd/openstack/secrets.go new file mode 100644 index 000000000..d0ef845e2 --- /dev/null +++ b/go/deploy-cli/cmd/openstack/secrets.go @@ -0,0 +1,50 @@ +package openstack + +import ( + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + + "github.com/spf13/cobra" +) + +func init() { + cmd.RootCmd.AddCommand(Openstack) +} + +var Openstack = &cobra.Command{ + Use: "openstack-secrets", + Short: "Generate openstack-secrets", + Long: "Generate openstack-secrets", + Run: openStackGen, +} + +func openStackGen(cmd *cobra.Command, args []string) { + if err := generateMariaDBSecret(); err != nil { + log.Errorf("Failed to generate MariaDB secret: %v", err) + } +} + +// credGen prints out the cli version number +func generateMariaDBSecret() error { + filePath := helpers.GetManifestPathToService("openstack") + "/secret-mariadb.yaml" + + config := helpers.SecretConfig{ + Name: "mariadb", + Namespace: "openstack", + Data: map[string]string{ + "password": helpers.GenerateRandomString(32), + "root-password": helpers.GenerateRandomString(32), + }, + } + + if err := helpers.CreateKubeSealSecretFile(config, filePath); err != nil { + log.Warn("Failed to create sealed secret", "error", err) + return err + } + + helpers.UpdateKustomizeFile(helpers.GetManifestPathToService("openstack")) + + return nil +} diff --git a/go/deploy-cli/cmd/other/openstack.go b/go/deploy-cli/cmd/other/openstack.go new file mode 100644 index 000000000..ba37b8167 --- /dev/null +++ b/go/deploy-cli/cmd/other/openstack.go @@ -0,0 +1,166 @@ +package other + +var SecretOpenStackTemplate = ` +# The purpose of this file is to serve as a template for OpenStack Helm +# based endpoints configuration so that OpenStack Helm populates the +# correct URLs in the configs that it generates for the various services +--- +endpoints: + # 'identity' endpoints are for keystone access + identity: + auth: + # this is the 'admin' user created in keystone by the initial start + # and used by the other services to create their service accounts + # and endpoint in the service catalog. + admin: + password: "{{ .KEYSTONE_ADMIN_PASSWORD }}" + # this user is the service account that glance uses + glance: + password: "{{ .GLANCE_KEYSTONE_PASSWORD }}" + # this user is the service account that ironic uses + ironic: + password: "{{ .IRONIC_KEYSTONE_PASSWORD }}" + # this user is the service account that neutron uses + neutron: + password: "{{ .NEUTRON_KEYSTONE_PASSWORD }}" + # this user is the service account that nova uses + nova: + password: "{{ .NOVA_KEYSTONE_PASSWORD }}" + # this user is the service account that placement uses + placement: + password: "{{ .PLACEMENT_KEYSTONE_PASSWORD }}" + + # set our public facing URL + host_fqdn_override: + public: + host: keystone.{{ .DNS_ZONE }} + + # 'oslo_cache' is the memcache layer + oslo_cache: + auth: + # this is used for encrypting / protecting the memcache tokens + memcache_secret_key: "{{ .MEMCACHE_SECRET_KEY }}" + + # 'oslo_db' is for MariaDB + oslo_db: + auth: + # this is what the keystone service uses to connect to MariaDB + keystone: + password: "{{ .KEYSTONE_DB_PASSWORD }}" + # this is what the glance service uses to connect to MariaDB + glance: + password: "{{ .GLANCE_DB_PASSWORD }}" + # this is what the ironic service uses to connect to MariaDB + ironic: + password: "{{ .IRONIC_DB_PASSWORD }}" + # this is what the neutron service uses to connect to MariaDB + neutron: + password: "{{ .NEUTRON_DB_PASSWORD }}" + # this is what the nova service uses to connect to MariaDB + nova: + password: "{{ .NOVA_DB_PASSWORD }}" + # this is what the placement service uses to connect to MariaDB + placement: + password: "{{ .PLACEMENT_DB_PASSWORD }}" + # this is what the horizon dashboard service uses to connect to MariaDB + horizon: + password: "{{ .HORIZON_DB_PASSWORD }}" + + # 'oslo_db_api' is for MariaDB specific for nova + oslo_db_api: + auth: + nova: + password: "{{ .NOVA_DB_PASSWORD }}" + + # 'oslo_db_cell0' is for MariaDB specific for nova + oslo_db_cell0: + auth: + nova: + password: "{{ .NOVA_DB_PASSWORD }}" + + # 'oslo_messaging' is for RabbitMQ + oslo_messaging: + auth: + # this is what the keystone service uses to connect to RabbitMQ + keystone: + password: "{{ .KEYSTONE_RABBITMQ_PASSWORD }}" + # this is what the glance service uses to connect to RabbitMQ + glance: + password: "{{ .GLANCE_RABBITMQ_PASSWORD }}" + # this is what the ironic service uses to connect to RabbitMQ + ironic: + password: "{{ .IRONIC_RABBITMQ_PASSWORD }}" + # this is what the neutron service uses to connect to RabbitMQ + neutron: + password: "{{ .NEUTRON_RABBITMQ_PASSWORD }}" + # this is what the nova service uses to connect to RabbitMQ + nova: + password: "{{ .NOVA_RABBITMQ_PASSWORD }}" + + # 'baremetal' is the ironic service + baremetal: + # set our public facing URL + host_fqdn_override: + public: + host: ironic.{{ .DNS_ZONE }} + + # 'image' is the glance service + image: + # set our public facing URL + host_fqdn_override: + public: + host: glance.{{ .DNS_ZONE }} + + # 'network' is the neutron service + network: + # set our public facing URL + host_fqdn_override: + public: + host: neutron.{{ .DNS_ZONE }} + + # 'compute' is the nova service + compute: + # set our public facing URL + host_fqdn_override: + public: + host: nova.{{ .DNS_ZONE }} + + # 'placement' is the nova service + placement: + # set our public facing URL + host_fqdn_override: + public: + host: placement.{{ .DNS_ZONE }} + + # 'dashboard' is the horizon service + dashboard: + # set our public facing URL + host_fqdn_override: + public: + host: horizon.{{ .DNS_ZONE }} + +# necessary cause the ingress definition in openstack-helm-infra helm-toolkit hardcodes this +secrets: + tls: + baremetal: + api: + public: ironic-tls-public + image: + api: + public: glance-tls-public + identity: + api: + public: keystone-tls-public + network: + server: + public: neutron-tls-public + compute: + osapi: + public: nova-tls-public + placement: + api: + public: placement-tls-public + dashboard: + dashboard: + public: horizon-tls-public +` diff --git a/go/deploy-cli/cmd/other/other.go b/go/deploy-cli/cmd/other/other.go new file mode 100644 index 000000000..048d995e3 --- /dev/null +++ b/go/deploy-cli/cmd/other/other.go @@ -0,0 +1,125 @@ +package other + +import ( + "os" + "path/filepath" + + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + "github.com/rackerlabs/understack/go/deploy-cli/helpers" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + "github.com/gookit/goutil/fsutil" + "github.com/gookit/goutil/strutil" + + "context" + "fmt" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + cmd.RootCmd.AddCommand(Other) +} + +var openStackSecrets map[string]any + +var Other = &cobra.Command{ + Use: "other-secrets", + Short: "Create secret for keystone, ironic, placement, neutron, nova, glance", + Long: "Create secret for keystone, ironic, placement, neutron, nova, glance", + Run: generateOtherSecrets, +} + +func generateOtherSecrets(_ *cobra.Command, _ []string) { + // create constant OpenStack memcache key to avoid cache invalidation on deploy + openStackSecrets = map[string]any{ + "DNS_ZONE": envutil.Getenv("DNS_ZONE"), + "MEMCACHE_SECRET_KEY": helpers.GenerateRandomString(32), + "HORIZON_DB_PASSWORD": helpers.GenerateRandomString(32), + "KEYSTONE_ADMIN_PASSWORD": loadOrGenSecret("admin-keystone-password", "openstack"), + } + + services := []string{"keystone", "ironic", "placement", "neutron", "nova", "glance", "horizon"} + dependencies := []string{"rabbitmq", "keystone", "db"} + + for _, service := range services { + log.Warnf("Running For %s Services", service) + + if err := generateServiceSecrets(service, dependencies); err != nil { + log.Errorf("Failed to generate secrets for %s: %v", service, err) + } + } + + if err := updateOpenStackSecretsFile(); err != nil { + log.Fatal("failed to update openstack file", "err", err) + } + + // Create Empty Dirs + emptyDirs := []string{"argo-events", "ovn", "metallb", "undersync", "cilium"} + for _, service := range emptyDirs { + fsutil.WriteFile(helpers.GetManifestPathToService(service)+"/.keep", "", os.ModePerm) + } +} + +func generateServiceSecrets(service string, dependencies []string) error { + manifestPath := helpers.GetManifestPathToService(service) + + for _, dep := range dependencies { + secretName := fmt.Sprintf("secret-%s-password", dep) + filePath := filepath.Join(manifestPath, secretName+".yaml") + secret := loadOrGenSecret(fmt.Sprintf("%s-%s-password", service, dep), "openstack") + + config := helpers.SecretConfig{ + Name: secretName, + Namespace: "openstack", + Data: map[string]string{ + "username": service, + "password": secret, + }, + } + + if err := helpers.CreateKubeSealSecretFile(config, filePath); err != nil { + return err + } + // add all the passwords to map, for openstack secrets.yaml file + openStackSecrets[strutil.Uppercase(fmt.Sprintf("%s_%s_PASSWORD", service, dep))] = secret + } + + helpers.UpdateKustomizeFile(manifestPath) + + return nil +} + +func updateOpenStackSecretsFile() error { + data, err := helpers.TemplateHelper(SecretOpenStackTemplate, openStackSecrets) + if err != nil { + return fmt.Errorf("failed to render template: %w", err) + } + + secretFilePath := filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "manifests", + "secret-openstack.yaml", + ) + log.Info("updating secret-openstack.yaml", "path", secretFilePath) + + if err := fsutil.WriteFile(secretFilePath, data, os.ModePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", secretFilePath, err) + } + return nil +} + +func loadOrGenSecret(serviceName, namespace string) string { + client, _ := helpers.KubeClientSet().CoreV1().Secrets(namespace).Get(context.Background(), serviceName, metav1.GetOptions{}) + encodedPassword, ok := client.Data["password"] + if ok { + log.Info("using password from cluster", "service", serviceName, "namespace", namespace) + return string(encodedPassword) + } + log.Warn("password not in cluster", "service", serviceName, "namespace", namespace) + log.Info("Creating Random password for", "service", serviceName, "namespace", namespace) + return helpers.GenerateRandomString(32) +} diff --git a/go/deploy-cli/cmd/root.go b/go/deploy-cli/cmd/root.go new file mode 100644 index 000000000..1503d8d36 --- /dev/null +++ b/go/deploy-cli/cmd/root.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + // Viper config location + cfgFile string +) + +var RootCmd = &cobra.Command{ + Use: "", + Short: "", + Long: ``, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + }, + PreRun: func(cmd *cobra.Command, args []string) { + }, + Run: func(cmd *cobra.Command, args []string) { + }, + PostRun: func(cmd *cobra.Command, args []string) { + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + }, +} + +// Execute will execute the root command +func Execute() error { + return RootCmd.Execute() +} + +func init() { +} diff --git a/go/deploy-cli/go.mod b/go/deploy-cli/go.mod new file mode 100644 index 000000000..37ec26984 --- /dev/null +++ b/go/deploy-cli/go.mod @@ -0,0 +1,76 @@ +module github.com/rackerlabs/understack/go/deploy-cli + +go 1.23.6 + +require ( + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/charmbracelet/log v0.4.1 + github.com/gookit/goutil v0.6.18 + github.com/spf13/cobra v1.9.1 + k8s.io/api v0.32.3 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v0.32.3 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect +) diff --git a/go/deploy-cli/go.sum b/go/deploy-cli/go.sum new file mode 100644 index 000000000..1c1d9df74 --- /dev/null +++ b/go/deploy-cli/go.sum @@ -0,0 +1,211 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= +github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw= +github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/go/deploy-cli/helpers/helpers.go b/go/deploy-cli/helpers/helpers.go new file mode 100644 index 000000000..7aca4e9b8 --- /dev/null +++ b/go/deploy-cli/helpers/helpers.go @@ -0,0 +1,50 @@ +package helpers + +import ( + "path/filepath" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/envutil" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +// SecretConfig holds configuration for creating secrets +type SecretConfig struct { + Name string + Namespace string + Data map[string]string +} + +func GetManifestPathToService(service string) string { + return filepath.Join( + envutil.Getenv("UC_DEPLOY"), + envutil.Getenv("DEPLOY_NAME"), + "manifests", + service, + ) +} + +func CreateKubeSealSecretFile(config SecretConfig, filePath string) error { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: config.Name, + Namespace: config.Namespace, + }, + Type: corev1.SecretTypeOpaque, + StringData: config.Data, + } + + secretYAML, err := yaml.Marshal(secret) + if err != nil { + return err + } + + log.Info("creating kubeseal secret", "path", filePath) + return KubeSeal(secretYAML, filePath) +} diff --git a/go/deploy-cli/helpers/kube.go b/go/deploy-cli/helpers/kube.go new file mode 100644 index 000000000..acf8eff63 --- /dev/null +++ b/go/deploy-cli/helpers/kube.go @@ -0,0 +1,24 @@ +package helpers + +import ( + "log" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +func KubeClientSet() *kubernetes.Clientset { + // Load kubeconfig from the default location (~/.kube/config) + config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) + if err != nil { + log.Fatalf("Failed to load kubeconfig: %v", err) + } + + // Create Kubernetes clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatalf("Failed to create Kubernetes client: %v", err) + } + + return clientset +} diff --git a/go/deploy-cli/helpers/kubeseal.go b/go/deploy-cli/helpers/kubeseal.go new file mode 100644 index 000000000..c07019d27 --- /dev/null +++ b/go/deploy-cli/helpers/kubeseal.go @@ -0,0 +1,55 @@ +package helpers + +import ( + "bytes" + "fmt" + "os" + "os/exec" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/fsutil" +) + +func KubeSeal(inputData []byte, outputPath string) error { + // Convert secret to YAML first + tmpFile, err := os.CreateTemp("", "secret-*.yaml") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte(inputData)); err != nil { + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + + cmd := exec.Command("kubeseal", + "--scope", "cluster-wide", + "--allow-empty-data", + "--format", "yaml", + ) + // Set up stdin + cmd.Stdin, err = os.Open(tmpFile.Name()) + if err != nil { + return err + } + + // Capture output + var out bytes.Buffer + cmd.Stdout = &out + + // Run command + err = cmd.Run() + if err != nil { + log.Info("err", "err", err) + return fmt.Errorf("kubeseal failed: %v", err) + } + + err = fsutil.WriteFile(outputPath, out.String(), os.ModePerm) + if err != nil { + log.Fatal("error in kustomization.yaml file", "err", err) + } + return nil +} diff --git a/go/deploy-cli/helpers/kustomization.go b/go/deploy-cli/helpers/kustomization.go new file mode 100644 index 000000000..a81ed7119 --- /dev/null +++ b/go/deploy-cli/helpers/kustomization.go @@ -0,0 +1,66 @@ +package helpers + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/charmbracelet/log" + "github.com/gookit/goutil/fsutil" +) + +const kustomizationFile = "kustomization.yaml" + +func UpdateKustomizeFile(dir string) { + files, err := scanYamlFiles(dir) + if err != nil { + log.Fatalf("Error scanning directory: %v", err) + } + + if len(files) == 0 { + log.Info("No YAML files found to include in kustomization.yaml") + return + } + + kustomization := buildKustomizationYAML(files) + + kustomPath := filepath.Join(dir, kustomizationFile) + err = os.WriteFile(kustomPath, []byte(kustomization), 0644) + if err != nil { + log.Fatalf("Error writing kustomization.yaml: %v", err) + } + + log.Printf("Successfully wrote %s with %d resources.\n", kustomPath, len(files)) +} + +func scanYamlFiles(dir string) ([]string, error) { + fileSet := make(map[string]bool) + + fsutil.FindInDir(dir, func(filePath string, de fs.DirEntry) error { + fileSet[de.Name()] = true + return nil + }, fsutil.IncludeSuffix(".yaml", ".yml"), + fsutil.ExcludeDotFile, + fsutil.ExcludeNames(kustomizationFile)) + + var uniqueFiles []string + for f := range fileSet { + uniqueFiles = append(uniqueFiles, f) + } + sort.Strings(uniqueFiles) + return uniqueFiles, nil +} + +func buildKustomizationYAML(resources []string) string { + var sb strings.Builder + sb.WriteString("apiVersion: kustomize.config.k8s.io/v1beta1\n") + sb.WriteString("kind: Kustomization\n") + sb.WriteString("resources:\n") + for _, res := range resources { + sb.WriteString(fmt.Sprintf("- %s\n", res)) + } + return sb.String() +} diff --git a/go/deploy-cli/helpers/random.go b/go/deploy-cli/helpers/random.go new file mode 100644 index 000000000..de6ef939f --- /dev/null +++ b/go/deploy-cli/helpers/random.go @@ -0,0 +1,30 @@ +package helpers + +import ( + "crypto/rand" + "log" + "math/big" + "os" +) + +func GenerateRandomString(length int) string { + // Define the dictionary: _A-Za-z0-9 + const dictionary = "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + dictLen := big.NewInt(int64(len(dictionary))) + + // Buffer to hold the result + result := make([]byte, length) + + // Generate random indices into the dictionary + for i := 0; i < length; i++ { + // Generate a secure random number between 0 and dictLen-1 + n, err := rand.Int(rand.Reader, dictLen) + if err != nil { + log.Fatal("failed to generate random password", "err", err) + os.Exit(1) + } + result[i] = dictionary[n.Int64()] + } + + return string(result) +} diff --git a/go/deploy-cli/helpers/template.go b/go/deploy-cli/helpers/template.go new file mode 100644 index 000000000..2b2554c22 --- /dev/null +++ b/go/deploy-cli/helpers/template.go @@ -0,0 +1,22 @@ +package helpers + +import ( + "bytes" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +// TemplateHelper renders a template string using the provided variables +func TemplateHelper(tmplStr string, vars map[string]any) (string, error) { + tmpl, err := template.New("tmpl"). + Funcs(sprig.TxtFuncMap()). + Option("missingkey=error"). + Parse(tmplStr) + if err != nil { + return "", err + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, vars) + return buf.String(), err +} diff --git a/go/deploy-cli/main.go b/go/deploy-cli/main.go new file mode 100644 index 000000000..785668b7f --- /dev/null +++ b/go/deploy-cli/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/rackerlabs/understack/go/deploy-cli/cmd" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/argocd" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/certManager" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/dex" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/helmConfig" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/init" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/node" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/openstack" + _ "github.com/rackerlabs/understack/go/deploy-cli/cmd/other" +) + +func main() { + cmd.Execute() +}