diff --git a/cmd/tfctl/main.go b/cmd/tfctl/main.go index ac3dc1c20..8bf4d506e 100644 --- a/cmd/tfctl/main.go +++ b/cmd/tfctl/main.go @@ -20,8 +20,10 @@ var ( BuildVersion string ) -var defaultNamespace = "flux-system" -var kubeconfigArgs = genericclioptions.NewConfigFlags(false) +var ( + defaultNamespace = "flux-system" + kubeconfigArgs = genericclioptions.NewConfigFlags(false) +) func main() { cmd := newRootCommand() @@ -127,20 +129,47 @@ func buildUninstallCmd(app *tfctl.CLI) *cobra.Command { } var reconcileExamples = ` - # Reconcile a Terraform resource + # Reconcile a specific Terraform resource tfctl reconcile --namespace=default my-resource + + # Reconcile all Terraform resources in the namespace + tfctl reconcile --all ` func buildReconcileCmd(app *tfctl.CLI) *cobra.Command { - return &cobra.Command{ + reconcile := &cobra.Command{ Use: "reconcile NAME", - Short: "Trigger a reconcile of the provided resource", + Short: "Trigger a reconcile of the provided resource or all resources", Example: strings.Trim(reconcileExamples, "\n"), - Args: cobra.ExactArgs(1), + Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { - return app.Reconcile(os.Stdout, args[0]) + all, err := cmd.Flags().GetBool("all") + if err != nil { + return err + } + + resource := "" + + if all { + if len(args) > 0 { + return errors.New("cannot use --all and provide resource name at the same time") + } + } else { + if len(args) == 0 { + return errors.New("resource name required unless --all is specified") + } + resource = args[0] + if len(args) > 1 { + return errors.New("only one resource name accepted") + } + } + return app.Reconcile(os.Stdout, resource) }, } + + reconcile.Flags().Bool("all", false, "Trigger reconcile for all Terraform resources in the namespace") + + return reconcile } var suspendExamples = ` diff --git a/tfctl/reconcile.go b/tfctl/reconcile.go index 940844c1c..21c1dd714 100644 --- a/tfctl/reconcile.go +++ b/tfctl/reconcile.go @@ -2,6 +2,7 @@ package tfctl import ( "context" + "errors" "fmt" "io" "time" @@ -13,25 +14,47 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// Reconcile annotates the given object +// Reconcile triggers a reconciliation of Terraform resources. +// If resource == "", it reconciles all resources in the namespace. func (c *CLI) Reconcile(out io.Writer, resource string) error { + if resource == "" { + return reconcileAllResources(context.TODO(), out, c.client, c.namespace) + } key := types.NamespacedName{ Name: resource, Namespace: c.namespace, } - err := requestReconciliation(context.TODO(), c.client, key) if err != nil { return err } - fmt.Fprintf(out, " Reconcile requested for %s/%s\n", c.namespace, resource) - return nil } +func reconcileAllResources(ctx context.Context, out io.Writer, kubeClient client.Client, namespace string) error { + terraformList := &infrav1.TerraformList{} + if err := kubeClient.List(ctx, terraformList, client.InNamespace(namespace)); err != nil { + return err + } + + var errs []error + for _, terraform := range terraformList.Items { + key := types.NamespacedName{ + Name: terraform.Name, + Namespace: terraform.Namespace, + } + if err := requestReconciliation(ctx, kubeClient, key); err != nil { + errs = append(errs, fmt.Errorf("failed to reconcile %s/%s: %w", terraform.Namespace, terraform.Name, err)) + } else { + fmt.Fprintf(out, " Reconcile requested for %s/%s\n", terraform.Namespace, terraform.Name) + } + } + return errors.Join(errs...) +} + func requestReconciliation(ctx context.Context, kubeClient client.Client, namespacedName types.NamespacedName) error { - return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { terraform := &infrav1.Terraform{} if err := kubeClient.Get(ctx, namespacedName, terraform); err != nil { return err