this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(toolbox): bootstrapping GitOps

+150 -11
+5 -4
Makefile
··· 11 11 infra: 12 12 cd infra/${env} && terragrunt apply --all 13 13 14 - bootstrap: 15 - # TODO maybe a single bootstrap command? 14 + bootstrap: platform 16 15 # TODO needs to wait for namespaces e.g. vault 17 16 toolbox secrets \ 18 17 --settings settings.yaml \ ··· 20 19 --host kube-1 21 20 22 21 platform: 23 - # TODO don't hard code registry 24 - cd platform/${env} && oras push --format=json docker.io/khuedoan/platform-manifests:${env} . 22 + toolbox gitops \ 23 + --path platform/${env} \ 24 + --hosts-file infra/_modules/nixos/hosts.json \ 25 + --host kube-1 25 26 26 27 apps: 27 28 # TODO multiple env
+1
flake.nix
··· 23 23 age 24 24 ansible 25 25 ansible-lint 26 + fluxcd 26 27 fzf 27 28 gnumake 28 29 go
+131
toolbox/cmd/gitops.go
··· 1 + package cmd 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "os/exec" 7 + "path/filepath" 8 + "strings" 9 + "time" 10 + 11 + "github.com/charmbracelet/log" 12 + "github.com/spf13/cobra" 13 + 14 + "github.com/khuedoan/cloudlab/toolbox/internal/cluster" 15 + ) 16 + 17 + const ( 18 + registryNamespace = "registry" 19 + registryService = "svc/registry" 20 + registryPort = 5000 21 + gitopsRepository = "platform" 22 + gitopsTag = "latest" 23 + fluxNamespace = "flux-system" 24 + fluxSource = "platform" 25 + fluxKustomization = "platform" 26 + ) 27 + 28 + var ( 29 + gitopsPath string 30 + ) 31 + 32 + func init() { 33 + gitopsCmd.Flags().StringVar(&gitopsPath, "path", "", "Path to the manifest bundle to publish") 34 + _ = gitopsCmd.MarkFlagRequired("path") 35 + } 36 + 37 + var gitopsCmd = &cobra.Command{ 38 + Use: "gitops", 39 + Short: "Proxy the in-cluster registry and push the GitOps manifests artifact", 40 + PreRunE: func(cmd *cobra.Command, args []string) error { 41 + if err := validateClusterFlags(); err != nil { 42 + return err 43 + } 44 + if _, err := exec.LookPath("flux"); err != nil { 45 + return fmt.Errorf("find flux CLI: %w", err) 46 + } 47 + return nil 48 + }, 49 + RunE: runGitopsPush, 50 + } 51 + 52 + func runGitopsPush(cmd *cobra.Command, args []string) error { 53 + manifestPath, err := filepath.Abs(gitopsPath) 54 + if err != nil { 55 + return fmt.Errorf("resolve path %q: %w", gitopsPath, err) 56 + } 57 + 58 + connectCtx, cancel := context.WithTimeout(cmd.Context(), connectTimeout) 59 + defer cancel() 60 + 61 + hostAddr, err := cluster.LoadHost(hostsFile, host) 62 + if err != nil { 63 + return fmt.Errorf("load host: %w", err) 64 + } 65 + 66 + conn, err := cluster.Connect(cluster.SSHConfig{ 67 + Host: hostAddr, 68 + User: sshUser, 69 + KeyPath: sshKey, 70 + KnownHostsPath: sshKnownHosts, 71 + Timeout: connectTimeout, 72 + }) 73 + if err != nil { 74 + return fmt.Errorf("connect to cluster: %w", err) 75 + } 76 + defer conn.Close() 77 + 78 + tunnel, err := conn.Forward(connectCtx, cluster.ServiceConfig{ 79 + Namespace: registryNamespace, 80 + Name: registryService, 81 + Port: registryPort, 82 + }) 83 + if err != nil { 84 + return fmt.Errorf("forward registry: %w", err) 85 + } 86 + 87 + artifactURL := fmt.Sprintf("oci://%s/%s:%s", tunnel.LocalAddr, gitopsRepository, gitopsTag) 88 + args = []string{ 89 + "push", 90 + "artifact", 91 + artifactURL, 92 + "--path", manifestPath, 93 + "--insecure-registry", 94 + } 95 + 96 + log.Infof("pushing %s from %s", artifactURL, manifestPath) 97 + 98 + output, err := fluxOutput(cmd.Context(), args...) 99 + if err != nil { 100 + return fmt.Errorf("push artifact: %w\n%s", err, strings.TrimSpace(string(output))) 101 + } 102 + 103 + if trimmed := strings.TrimSpace(string(output)); trimmed != "" { 104 + log.Info(trimmed) 105 + } 106 + 107 + requestedAt := time.Now().UTC().Format(time.RFC3339Nano) 108 + log.Infof("triggering Flux sync for %s/%s", fluxNamespace, fluxKustomization) 109 + 110 + output, err = conn.RunCommandContext( 111 + cmd.Context(), 112 + fmt.Sprintf( 113 + "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", 114 + fluxNamespace, fluxSource, requestedAt, 115 + fluxNamespace, fluxKustomization, requestedAt, 116 + ), 117 + ) 118 + if err != nil { 119 + return fmt.Errorf("trigger flux sync: %w", err) 120 + } 121 + 122 + if trimmed := strings.TrimSpace(string(output)); trimmed != "" { 123 + log.Info(trimmed) 124 + } 125 + 126 + return nil 127 + } 128 + func fluxOutput(ctx context.Context, args ...string) ([]byte, error) { 129 + fluxCmd := exec.CommandContext(ctx, "flux", args...) 130 + return fluxCmd.CombinedOutput() 131 + }
+12
toolbox/cmd/root.go
··· 1 1 package cmd 2 2 3 3 import ( 4 + "fmt" 4 5 "os" 5 6 "path/filepath" 6 7 ··· 25 26 rootCmd.PersistentFlags().StringVar(&sshKey, "ssh-key", defaultSSHKey(), "Path to SSH private key") 26 27 rootCmd.PersistentFlags().StringVar(&sshKnownHosts, "ssh-known-hosts", defaultKnownHostsFile(), "Path to SSH known_hosts file") 27 28 29 + rootCmd.AddCommand(gitopsCmd) 28 30 rootCmd.AddCommand(secretsCmd) 29 31 } 30 32 ··· 57 59 } 58 60 return filepath.Join(home, ".ssh", "known_hosts") 59 61 } 62 + 63 + func validateClusterFlags() error { 64 + if hostsFile == "" { 65 + return fmt.Errorf("--hosts-file is required") 66 + } 67 + if host == "" { 68 + return fmt.Errorf("--host is required") 69 + } 70 + return nil 71 + }
+1 -7
toolbox/cmd/secrets.go
··· 25 25 Use: "secrets", 26 26 Short: "Manage secrets in Vault", 27 27 PreRunE: func(cmd *cobra.Command, args []string) error { 28 - if hostsFile == "" { 29 - return fmt.Errorf("--hosts-file is required") 30 - } 31 - if host == "" { 32 - return fmt.Errorf("--host is required") 33 - } 34 - return nil 28 + return validateClusterFlags() 35 29 }, 36 30 RunE: runSecrets, 37 31 }