diff --git a/cmd/gitops/kubernetes.go b/cmd/gitops/kubernetes.go index 1864165..b189f42 100644 --- a/cmd/gitops/kubernetes.go +++ b/cmd/gitops/kubernetes.go @@ -28,15 +28,15 @@ func applyKubernetes(c *cli.Context) error { println(color.InGreen("No changes to apply.")) exitApplication(c, true) return nil - } + } + + prettyPrintPlan(p, c.Bool("show-unchanged")) - prettyPrintPlan(p) - if !c.Bool("auto-approve") { println("GitOps CLI will apply these changes to your Kubernetes cluster.") println("Only 'yes' will be accepted to approve.") promtAnswer := util.StringPrompt("Apply changes above: ") - + if promtAnswer != "yes" { println("Aborting") return nil @@ -70,7 +70,7 @@ func planKubernetes(c *cli.Context) error { if p.NothingToDo() { println(color.InGreen("No changes to apply.")) } else { - prettyPrintPlan(p) + prettyPrintPlan(p, c.Bool("show-unchanged")) dirLimitString := "" if c.String("dir") != "" { dirLimitString = " --dir " + c.String("dir") @@ -92,7 +92,7 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { println("Limiting to cluster " + color.InBlue(clusterLimit)) limitPrintln = true } - + dirLimit := getDirLimit(c) if dirLimit != "" { println("Limiting to directory " + color.InPurple(dirLimit)) @@ -115,10 +115,10 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { return nil, err } log.Trace("Loaded ", len(localSecrets), " local secrets with target ", secret.SecretTargetTypeKubernetes) - + p := &plan.Plan{ TargetType: secret.SecretTargetTypeKubernetes, - Items: []plan.PlanItem{}, + Items: []plan.PlanItem{}, } bar := progressbar.NewOptions(len(localSecrets), @@ -130,7 +130,7 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { progressbar.OptionSetPredictTime(false), progressbar.OptionSetDescription("[green][Syncing local state with cluster][reset]"), ) - + for _, localSecret := range localSecrets { bar.Add(1) // check for local secret in state @@ -173,7 +173,7 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { stateSecretFound := false for _, localSecret := range localSecrets { if stateSecret.Path == localSecret.Path { - stateSecretFound = true + stateSecretFound = true break } } @@ -188,9 +188,9 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { // therefore we are checking if the cluster secret actually exists log.Trace("Secret ", stateSecret.CombinedName(), " does not exist locally") remoteSecret, err := k8s.GetSecret(&secret.Secret{ - Name: stateSecret.Name, + Name: stateSecret.Name, Namespace: stateSecret.Namespace, - Type: stateSecret.Type, + Type: stateSecret.Type, }, stateSecret.Target) if err != nil { // only throw error if err is not "not found" @@ -211,7 +211,7 @@ func createKubernetesPlan(c *cli.Context) (*plan.Plan, error) { // at this state, the local secret does not exist anymore, but the secret is still in the state // also, the cluster still holds the secret which needs to be deleted planItem := plan.PlanItem{ - LocalSecret: nil, + LocalSecret: nil, RemoteSecret: remoteSecret, } planItem.ComputeDiff() @@ -243,12 +243,12 @@ func getDirLimit(c *cli.Context) string { return dirLimit } -func prettyPrintPlan(p *plan.Plan) { +func prettyPrintPlan(p *plan.Plan, showUnchanged bool) { println("") println("GitOps CLI computed the following changes for your cluster:") println("-------------------------------------------------------") println("") - p.Print() + p.Print(showUnchanged) println("") println("-------------------------------------------------------") println("") diff --git a/cmd/gitops/main.go b/cmd/gitops/main.go index a3cdda3..7db7625 100644 --- a/cmd/gitops/main.go +++ b/cmd/gitops/main.go @@ -17,68 +17,73 @@ func main() { Usage: "GitOps CLI", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "root-dir", - Value: "", - Usage: "root directory of the git repository", + Name: "root-dir", + Value: "", + Usage: "root directory of the git repository", EnvVars: []string{"GITOPS_ROOT_DIR"}, }, &cli.StringFlag{ - Name: "kubeconfig", + Name: "kubeconfig", Aliases: []string{"k"}, - Value: "", - Usage: "kubeconfig file to use for connecting to the Kubernetes cluster", + Value: "", + Usage: "kubeconfig file to use for connecting to the Kubernetes cluster", EnvVars: []string{"KUBECONFIG", "GITOPS_KUBECONFIG"}, }, &cli.BoolFlag{ - Name: "verbose", + Name: "verbose", Aliases: []string{"v"}, - Usage: "debug output", + Usage: "debug output", EnvVars: []string{"GITOPS_VERBOSE"}, }, &cli.BoolFlag{ - Name: "very-verbose", + Name: "very-verbose", Aliases: []string{"vv"}, - Usage: "trace output", + Usage: "trace output", EnvVars: []string{"GITOPS_VERY_VERBOSE"}, }, &cli.BoolFlag{ - Name: "cleartext", - Usage: "print secrets in cleartext to the console", + Name: "cleartext", + Usage: "print secrets in cleartext to the console", EnvVars: []string{"GITOPS_CLEARTEXT"}, }, &cli.BoolFlag{ - Name: "print", - Usage: "print secrets to the console", + Name: "print", + Usage: "print secrets to the console", EnvVars: []string{"GITOPS_PRINT"}, }, + &cli.BoolFlag{ + Name: "show-unchanged", + Usage: "display unchanged secrets in the plan overview", + EnvVars: []string{"GITOPS_SHOW_UNCHANGED"}, + }, }, Commands: []*cli.Command{ { - Name: "secrets", + Name: "secrets", Aliases: []string{"s"}, - Usage: "GitOps managed secrets", + Usage: "GitOps managed secrets", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "dir", + Name: "dir", Aliases: []string{"d"}, - Value: "", - Usage: "directory to limit secret discovery to", + Value: "", + Usage: "directory to limit secret discovery to", EnvVars: []string{"GITOPS_SECRETS_DIR"}, }, }, Subcommands: []*cli.Command{ { - Name: "apply", + Name: "apply", Aliases: []string{"a"}, - Usage: "Push secrets into your infrastructure", + Usage: "Push secrets into your infrastructure", Subcommands: []*cli.Command{ { - Name: "kubernetes", + Name: "kubernetes", Aliases: []string{"k8s"}, - Usage: "Push secrets into a Kubernetes cluster", + Usage: "Push secrets into a Kubernetes cluster", Flags: []cli.Flag{ &cli.BoolFlag{ - Name: "auto-approve", + Name: "auto-approve", Usage: "apply the changes without prompting for approval", }, }, @@ -88,7 +93,7 @@ func main() { }, }, { - Name: "vault", + Name: "vault", Usage: "Push secrets into vault", Action: func(c *cli.Context) error { log.Fatal("Not implemented yet") @@ -98,14 +103,14 @@ func main() { }, }, { - Name: "plan", + Name: "plan", Aliases: []string{"p"}, - Usage: "Plan the application of secrets into your infrastructure", + Usage: "Plan the application of secrets into your infrastructure", Subcommands: []*cli.Command{ { - Name: "kubernetes", + Name: "kubernetes", Aliases: []string{"k8s"}, - Usage: "Plan the application of secrets into a Kubernetes cluster", + Usage: "Plan the application of secrets into a Kubernetes cluster", Action: func(c *cli.Context) error { initApplication(c) @@ -115,7 +120,7 @@ func main() { }, }, { - Name: "template", + Name: "template", Usage: "Test the templating of secrets", Action: func(c *cli.Context) error { initApplication(c) @@ -125,11 +130,11 @@ func main() { }, }, { - Name: "clusters", + Name: "clusters", Usage: "Managing target clusters", Subcommands: []*cli.Command{ { - Name: "list", + Name: "list", Usage: "List all target clusters", Action: func(c *cli.Context) error { initApplication(c) @@ -145,7 +150,7 @@ func main() { }, }, { - Name: "add", + Name: "add", Usage: "Add a target cluster. ", Action: func(c *cli.Context) error { initApplication(c) @@ -159,7 +164,7 @@ func main() { log.Fatal("Usage: gitops clusters add ") } err := state.GetState().AddCluster(&state.ClusterState{ - Name: c.Args().Get(0), + Name: c.Args().Get(0), ConfigFile: kubeconfig, }) if err != nil { @@ -169,7 +174,7 @@ func main() { }, }, { - Name: "remove", + Name: "remove", Usage: "Remove a target cluster", Action: func(c *cli.Context) error { initApplication(c) @@ -184,7 +189,7 @@ func main() { }, }, { - Name: "test", + Name: "test", Usage: "Test a target cluster connection", Action: func(c *cli.Context) error { initApplication(c) diff --git a/internal/plan/plan.go b/internal/plan/plan.go index 66d7b3f..6d75d07 100644 --- a/internal/plan/plan.go +++ b/internal/plan/plan.go @@ -40,8 +40,11 @@ func (i *PlanItem) ComputeDiff() { i.Diff = secret.CompareSecrets(i.RemoteSecret, i.LocalSecret) } -func (p *Plan) Print() { +func (p *Plan) Print(showUnchanged bool) { for i, item := range p.Items { + if !showUnchanged && item.Diff.Equal { + continue + } item.Diff.Print() if i < len(p.Items)-1 { println("---") @@ -90,4 +93,4 @@ func executeKubernetesPlan(p *Plan) error { } } return nil -} \ No newline at end of file +} diff --git a/internal/secret/loader.go b/internal/secret/loader.go index f53f7fb..01a0b90 100644 --- a/internal/secret/loader.go +++ b/internal/secret/loader.go @@ -1,6 +1,7 @@ package secret import ( + "errors" "strings" "github.com/mxcd/gitops-cli/internal/util" @@ -9,9 +10,9 @@ import ( ) /* - Loads all the secrets from the local file system - Applies the specified target filter - Use SecretTargetAll to load all secrets +Loads all the secrets from the local file system +Applies the specified target filter +Use SecretTargetAll to load all secrets */ func LoadLocalSecrets(targetTypeFilter SecretTargetType) ([]*Secret, error) { return LoadLocalSecretsLimited(targetTypeFilter, "", "") @@ -23,7 +24,7 @@ func LoadLocalSecretsLimited(targetTypeFilter SecretTargetType, directoryLimit s if err != nil { return nil, err } - + // Filter by directory limit filteredFileNames := []string{} for _, secretFileName := range secretFileNames { @@ -31,14 +32,13 @@ func LoadLocalSecretsLimited(targetTypeFilter SecretTargetType, directoryLimit s log.Trace("Skipping file due to directory filter: ", secretFileName) continue } - if strings.HasSuffix(secretFileName, "values.gitops.secret.enc.yml") || strings.HasSuffix(secretFileName, "values.gitops.secret.enc.yaml") { + if strings.HasSuffix(secretFileName, "values.gitops.secret.enc.yml") || strings.HasSuffix(secretFileName, "values.gitops.secret.enc.yaml") { log.Trace("Skipping values file: ", secretFileName) continue } filteredFileNames = append(filteredFileNames, secretFileName) } secretFileNames = filteredFileNames - secrets := []*Secret{} bar := progressbar.NewOptions(len(secretFileNames), @@ -54,6 +54,7 @@ func LoadLocalSecretsLimited(targetTypeFilter SecretTargetType, directoryLimit s bar.Add(1) secret, err := FromPath(secretFileName) if err != nil { + bar.Finish() return nil, err } if secret.TargetType != targetTypeFilter && targetTypeFilter != SecretTargetTypeAll { @@ -64,10 +65,18 @@ func LoadLocalSecretsLimited(targetTypeFilter SecretTargetType, directoryLimit s log.Trace("Skipping file due to target filter: ", secretFileName) continue } + for _, s := range secrets { + if s.Name == secret.Name && s.Target == secret.Target { + bar.Finish() + println("") + log.Error("Unable to load secret '", secret.Name, "' from '", secret.Path, "' because a secret with the same name and target already exists: '", s.Path, "'") + return nil, errors.New("error loading secrets: duplicate secret name and target") + } + } secrets = append(secrets, secret) } bar.Finish() println("") println("") return secrets, nil -} \ No newline at end of file +}