diff --git a/cmd/tk/main.go b/cmd/tk/main.go index b113fee9..6fbee802 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -19,7 +19,7 @@ var rootCmd = &cobra.Command{ SilenceUsage: true, SilenceErrors: true, Short: "Command line utility for assembling Kubernetes CD pipelines", - Long: `Command line utility for assembling Kubernetes CD pipelines.`, + Long: `Command line utility for assembling Kubernetes CD pipelines the GitOps way.`, Example: ` # Check prerequisites tk check --pre @@ -30,7 +30,7 @@ var rootCmd = &cobra.Command{ tk create source git webapp \ --url=https://github.com/stefanprodan/podinfo \ --branch=master \ - --interval=5m + --interval=3m # Create a kustomization for deploying a series of microservices tk create kustomization webapp \ @@ -43,6 +43,9 @@ var rootCmd = &cobra.Command{ --health-check="Deployment/backend.webapp" \ --health-check="Deployment/frontend.webapp" \ --health-check-timeout=2m + + # Trigger a git sync and apply changes if any + sync kustomization webapp --with-source `, } diff --git a/cmd/tk/sync.go b/cmd/tk/sync.go new file mode 100644 index 00000000..c51555aa --- /dev/null +++ b/cmd/tk/sync.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var syncCmd = &cobra.Command{ + Use: "sync", + Short: "Synchronize commands", +} + +func init() { + rootCmd.AddCommand(syncCmd) +} diff --git a/cmd/tk/sync_kustomization.go b/cmd/tk/sync_kustomization.go new file mode 100644 index 00000000..951ceb81 --- /dev/null +++ b/cmd/tk/sync_kustomization.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "fmt" + "time" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +var syncKsCmd = &cobra.Command{ + Use: "kustomization [name]", + Short: "Synchronize kustomization", + Long: ` +The sync kustomization command triggers a reconciliation of a Kustomization resource and waits for it to finish.`, + Example: ` # Trigger a kustomization apply outside of the reconciliation interval + sync kustomization podinfo + + # Trigger a git sync of the kustomization source and apply changes + sync kustomization podinfo --with-source +`, + RunE: syncKsCmdRun, +} + +var ( + syncKsWithSource bool +) + +func init() { + syncKsCmd.Flags().BoolVar(&syncKsWithSource, "with-source", false, "synchronize kustomization source") + + syncCmd.AddCommand(syncKsCmd) +} + +func syncKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("kustomization name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + var kustomization kustomizev1.Kustomization + err = kubeClient.Get(ctx, namespacedName, &kustomization) + if err != nil { + return err + } + + if syncKsWithSource { + err := syncSourceGitCmdRun(nil, []string{kustomization.Spec.SourceRef.Name}) + if err != nil { + return err + } + } else { + logAction("annotating kustomization %s in %s namespace", name, namespace) + if kustomization.Annotations == nil { + kustomization.Annotations = map[string]string{ + kustomizev1.SyncAtAnnotation: time.Now().String(), + } + } else { + kustomization.Annotations[kustomizev1.SyncAtAnnotation] = time.Now().String() + } + if err := kubeClient.Update(ctx, &kustomization); err != nil { + return err + } + logSuccess("kustomization annotated") + } + + logWaiting("waiting for kustomization sync") + if err := wait.PollImmediate(2*time.Second, timeout, + isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logSuccess("kustomization sync completed") + + err = kubeClient.Get(ctx, namespacedName, &kustomization) + if err != nil { + return err + } + + err = kubeClient.Get(ctx, namespacedName, &kustomization) + if err != nil { + return err + } + + if kustomization.Status.LastAppliedRevision != "" { + logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision) + } else { + return fmt.Errorf("kustomization sync failed") + } + return nil +} diff --git a/cmd/tk/sync_source.go b/cmd/tk/sync_source.go new file mode 100644 index 00000000..ce2ec70d --- /dev/null +++ b/cmd/tk/sync_source.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var syncSourceCmd = &cobra.Command{ + Use: "source", + Short: "Synchronize source commands", +} + +func init() { + syncCmd.AddCommand(syncSourceCmd) +} diff --git a/cmd/tk/sync_source_git.go b/cmd/tk/sync_source_git.go new file mode 100644 index 00000000..b714f2bb --- /dev/null +++ b/cmd/tk/sync_source_git.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "fmt" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "time" +) + +var syncSourceGitCmd = &cobra.Command{ + Use: "git [name]", + Short: "Synchronize git source", + Long: ` +The sync source command triggers a reconciliation of a GitRepository resource and waits for it to finish.`, + Example: ` # Trigger a git pull for an existing source + sync source git podinfo +`, + RunE: syncSourceGitCmdRun, +} + +func init() { + syncSourceCmd.AddCommand(syncSourceGitCmd) +} + +func syncSourceGitCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("source name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + logAction("annotating source %s in %s namespace", name, namespace) + var gitRepository sourcev1.GitRepository + err = kubeClient.Get(ctx, namespacedName, &gitRepository) + if err != nil { + return err + } + + if gitRepository.Annotations == nil { + gitRepository.Annotations = map[string]string{ + sourcev1.SyncAtAnnotation: time.Now().String(), + } + } else { + gitRepository.Annotations[sourcev1.SyncAtAnnotation] = time.Now().String() + } + if err := kubeClient.Update(ctx, &gitRepository); err != nil { + return err + } + logSuccess("source annotated") + + logWaiting("waiting for git sync") + if err := wait.PollImmediate(2*time.Second, timeout, + isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logSuccess("git sync completed") + + err = kubeClient.Get(ctx, namespacedName, &gitRepository) + if err != nil { + return err + } + + if gitRepository.Status.Artifact != nil { + logSuccess("fetched revision %s", gitRepository.Status.Artifact.Revision) + } else { + return fmt.Errorf("git sync failed, artifact not found") + } + return nil +}