From a89a6710f0ef100f36269cc10e810a82163d3e33 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Mon, 16 Mar 2026 07:18:07 +0700 Subject: [PATCH] feat(toolbox): bootstrapping GitOps --- Makefile | 9 +-- flake.nix | 1 + toolbox/cmd/gitops.go | 131 +++++++++++++++++++++++++++++++++++++++++ toolbox/cmd/root.go | 12 ++++ toolbox/cmd/secrets.go | 8 +-- 5 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 toolbox/cmd/gitops.go diff --git a/Makefile b/Makefile index 202f141..3d5e205 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,7 @@ compose: infra: cd infra/${env} && terragrunt apply --all -bootstrap: - # TODO maybe a single bootstrap command? +bootstrap: platform # TODO needs to wait for namespaces e.g. vault toolbox secrets \ --settings settings.yaml \ @@ -20,8 +19,10 @@ bootstrap: --host kube-1 platform: - # TODO don't hard code registry - cd platform/${env} && oras push --format=json docker.io/khuedoan/platform-manifests:${env} . + toolbox gitops \ + --path platform/${env} \ + --hosts-file infra/_modules/nixos/hosts.json \ + --host kube-1 apps: # TODO multiple env diff --git a/flake.nix b/flake.nix index 20a35a8..545483d 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ age ansible ansible-lint + fluxcd fzf gnumake go diff --git a/toolbox/cmd/gitops.go b/toolbox/cmd/gitops.go new file mode 100644 index 0000000..8df83ce --- /dev/null +++ b/toolbox/cmd/gitops.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "context" + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/charmbracelet/log" + "github.com/spf13/cobra" + + "github.com/khuedoan/cloudlab/toolbox/internal/cluster" +) + +const ( + registryNamespace = "registry" + registryService = "svc/registry" + registryPort = 5000 + gitopsRepository = "platform" + gitopsTag = "latest" + fluxNamespace = "flux-system" + fluxSource = "platform" + fluxKustomization = "platform" +) + +var ( + gitopsPath string +) + +func init() { + gitopsCmd.Flags().StringVar(&gitopsPath, "path", "", "Path to the manifest bundle to publish") + _ = gitopsCmd.MarkFlagRequired("path") +} + +var gitopsCmd = &cobra.Command{ + Use: "gitops", + Short: "Proxy the in-cluster registry and push the GitOps manifests artifact", + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := validateClusterFlags(); err != nil { + return err + } + if _, err := exec.LookPath("flux"); err != nil { + return fmt.Errorf("find flux CLI: %w", err) + } + return nil + }, + RunE: runGitopsPush, +} + +func runGitopsPush(cmd *cobra.Command, args []string) error { + manifestPath, err := filepath.Abs(gitopsPath) + if err != nil { + return fmt.Errorf("resolve path %q: %w", gitopsPath, err) + } + + connectCtx, cancel := context.WithTimeout(cmd.Context(), connectTimeout) + defer cancel() + + hostAddr, err := cluster.LoadHost(hostsFile, host) + if err != nil { + return fmt.Errorf("load host: %w", err) + } + + conn, err := cluster.Connect(cluster.SSHConfig{ + Host: hostAddr, + User: sshUser, + KeyPath: sshKey, + KnownHostsPath: sshKnownHosts, + Timeout: connectTimeout, + }) + if err != nil { + return fmt.Errorf("connect to cluster: %w", err) + } + defer conn.Close() + + tunnel, err := conn.Forward(connectCtx, cluster.ServiceConfig{ + Namespace: registryNamespace, + Name: registryService, + Port: registryPort, + }) + if err != nil { + return fmt.Errorf("forward registry: %w", err) + } + + artifactURL := fmt.Sprintf("oci://%s/%s:%s", tunnel.LocalAddr, gitopsRepository, gitopsTag) + args = []string{ + "push", + "artifact", + artifactURL, + "--path", manifestPath, + "--insecure-registry", + } + + log.Infof("pushing %s from %s", artifactURL, manifestPath) + + output, err := fluxOutput(cmd.Context(), args...) + if err != nil { + return fmt.Errorf("push artifact: %w\n%s", err, strings.TrimSpace(string(output))) + } + + if trimmed := strings.TrimSpace(string(output)); trimmed != "" { + log.Info(trimmed) + } + + requestedAt := time.Now().UTC().Format(time.RFC3339Nano) + log.Infof("triggering Flux sync for %s/%s", fluxNamespace, fluxKustomization) + + output, err = conn.RunCommandContext( + cmd.Context(), + fmt.Sprintf( + "kubectl annotate --overwrite -n %s ocirepository.source.toolkit.fluxcd.io/%s reconcile.fluxcd.io/requestedAt=%q && kubectl annotate --overwrite -n %s kustomization.kustomize.toolkit.fluxcd.io/%s reconcile.fluxcd.io/requestedAt=%q", + fluxNamespace, fluxSource, requestedAt, + fluxNamespace, fluxKustomization, requestedAt, + ), + ) + if err != nil { + return fmt.Errorf("trigger flux sync: %w", err) + } + + if trimmed := strings.TrimSpace(string(output)); trimmed != "" { + log.Info(trimmed) + } + + return nil +} +func fluxOutput(ctx context.Context, args ...string) ([]byte, error) { + fluxCmd := exec.CommandContext(ctx, "flux", args...) + return fluxCmd.CombinedOutput() +} diff --git a/toolbox/cmd/root.go b/toolbox/cmd/root.go index c284d61..1649101 100644 --- a/toolbox/cmd/root.go +++ b/toolbox/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "path/filepath" @@ -25,6 +26,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&sshKey, "ssh-key", defaultSSHKey(), "Path to SSH private key") rootCmd.PersistentFlags().StringVar(&sshKnownHosts, "ssh-known-hosts", defaultKnownHostsFile(), "Path to SSH known_hosts file") + rootCmd.AddCommand(gitopsCmd) rootCmd.AddCommand(secretsCmd) } @@ -57,3 +59,13 @@ func defaultKnownHostsFile() string { } return filepath.Join(home, ".ssh", "known_hosts") } + +func validateClusterFlags() error { + if hostsFile == "" { + return fmt.Errorf("--hosts-file is required") + } + if host == "" { + return fmt.Errorf("--host is required") + } + return nil +} diff --git a/toolbox/cmd/secrets.go b/toolbox/cmd/secrets.go index ce43527..0aee1e8 100644 --- a/toolbox/cmd/secrets.go +++ b/toolbox/cmd/secrets.go @@ -25,13 +25,7 @@ var secretsCmd = &cobra.Command{ Use: "secrets", Short: "Manage secrets in Vault", PreRunE: func(cmd *cobra.Command, args []string) error { - if hostsFile == "" { - return fmt.Errorf("--hosts-file is required") - } - if host == "" { - return fmt.Errorf("--host is required") - } - return nil + return validateClusterFlags() }, RunE: runSecrets, } -- 2.51.2