From 813407cfbf6a4b22d99799d27ecb2bcd054f9004 Mon Sep 17 00:00:00 2001 From: eding Date: Thu, 21 Aug 2025 23:19:27 +0200 Subject: [PATCH] chore: init --- .vscode/launch.json | 21 ++++ README.md | 4 + go.mod | 9 ++ go.sum | 11 ++ main.go | 243 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 288 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..57425b7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run and Debug CICD Tool", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": [ + "repo", + "init", + "--org", + "Pingu-Studio", + "--manifests-repo", + "cicd-deployment-manifests" + ], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9ffeb6 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +## Commands +go mod init github.com/Pingu-Studio/MaidnCLI +go get -u github.com/spf13/cobra@latest + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1b2f74e --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/Pingu-Studio/MaidnCLI + +go 1.25.0 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4aae07f --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..09e7143 --- /dev/null +++ b/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +// Global variables for flags +var orgName, manifestsRepoName string + +func main() { + var rootCmd = &cobra.Command{ + Use: "cicd-tool", + Short: "A CLI tool to manage CI/CD setup for applications.", + } + + var repoCmd = &cobra.Command{ + Use: "repo", + Short: "Manage CI/CD repositories.", + } + + var initCmd = &cobra.Command{ + Use: "init", + Short: "Initializes the manifests repository with namespaces and environment directories.", + Run: runInitRepo, + } + + // Add flags to the 'init' command + initCmd.Flags().StringVar(&orgName, "org", "", "The GitHub organization (e.g., Pingu-Studio)") + initCmd.Flags().StringVar(&manifestsRepoName, "manifests-repo", "cicd-deployment-manifests", "The name of the manifests repository") + + repoCmd.AddCommand(initCmd) + rootCmd.AddCommand(repoCmd) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func runInitRepo(cmd *cobra.Command, args []string) { + fmt.Println("šŸš€ Starting CI/CD Manifests Repository Initialization...") + + // 1. Dependency Checks + fmt.Println("[INFO] Checking dependencies (git and gh)...") + if !commandExists("git") || !commandExists("gh") { + fmt.Println("[ERROR] 'git' and 'gh' must be installed and in your PATH.") + os.Exit(1) + } + fmt.Println("[SUCCESS] Dependencies are present.") + + // 2. Interactive Authentication Check + fmt.Println("[INFO] Checking GitHub authentication...") + if err := runCommand("gh", "auth", "status"); err != nil { + fmt.Println("[WARNING] You are not logged into the GitHub CLI. Let's fix that.") + if err := runInteractiveCommand("gh", "auth", "login"); err != nil { + fmt.Println("[ERROR] Authentication failed. Please run 'gh auth login' manually and try again.") + os.Exit(1) + } + } + fmt.Println("[SUCCESS] Authentication check passed.") + + // 3. Interactive Input for Missing Flags + if orgName == "" { + orgName = promptForInput("Enter the GitHub organization name:") + } + if manifestsRepoName == "" { + manifestsRepoName = promptForInput("Enter the manifests repository name:") + } + + // 4. Check & Create GitHub Repository + fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName) + fmt.Printf("[INFO] Checking for repository '%s'...\n", fullRepoName) + if err := runCommand("gh", "repo", "view", fullRepoName); err != nil { + fmt.Printf("[INFO] Repository '%s' not found. Creating it now...\n", fullRepoName) + if err := runInteractiveCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Centralized deployment manifests for Flux CD"); err != nil { + fmt.Printf("[ERROR] Failed to create repository: %v\n", err) + os.Exit(1) + } + fmt.Println("[SUCCESS] Repository created.") + } else { + fmt.Println("[SUCCESS] Repository already exists.") + } + + // 5. Clone and Setup Directory Structure + tempDir, err := os.MkdirTemp("", "manifests-repo-*") + if err != nil { + fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + + fmt.Printf("[INFO] Cloning repository into %s...\n", tempDir) + repoURL := fmt.Sprintf("https://github.com/%s.git", fullRepoName) + if err := runCommand("git", "clone", repoURL, tempDir); err != nil { + fmt.Printf("[ERROR] Failed to clone repository: %v\n", err) + os.Exit(1) + } + + // 6. Generate Directories and Manifests + fmt.Println("[INFO] Creating repository structure and Kubernetes manifests...") + createStructureAndManifests(tempDir) + fmt.Println("[SUCCESS] Repository structure and manifests generated.") + + // 7. Commit and Push + fmt.Println("[INFO] Committing and pushing changes...") + runCommandInDir(tempDir, "git", "config", "user.name", "MaidnCLI") + runCommandInDir(tempDir, "git", "config", "user.email", "actions@github.com") + runCommandInDir(tempDir, "git", "add", "-A") + + statusCmd := exec.Command("git", "status", "--porcelain") + statusCmd.Dir = tempDir + output, _ := statusCmd.Output() + + if len(output) == 0 { + fmt.Println("[INFO] No changes to commit. Repository is already up to date.") + } else { + commitMsg := "feat: Initialize repo with namespaces and environment structure" + if err := runCommandInDir(tempDir, "git", "commit", "-m", commitMsg); err != nil { + // Ignore error if commit fails due to no changes, otherwise fail + if !strings.Contains(err.Error(), "nothing to commit") { + fmt.Printf("[ERROR] Failed to commit changes: %v\n", err) + os.Exit(1) + } + } + if err := runCommandInDir(tempDir, "git", "push"); err != nil { + fmt.Printf("[ERROR] Failed to push changes: %v\n", err) + os.Exit(1) + } + fmt.Println("[SUCCESS] Changes pushed successfully.") + } + + fmt.Println("\nšŸŽ‰ Onboarding complete! šŸŽ‰") + fmt.Println("\n[ACTION REQUIRED] Please update your Flux Kustomization resource in your cluster:") + fmt.Println(" - Find the Kustomization that points to this repository.") + fmt.Println(" - Change `spec.path` from `./apps` to `./`.") + fmt.Println(" - This allows Flux to see the new `namespaces` directory.") + fmt.Println("\nExample:") + fmt.Println(" kubectl edit kustomization all-apps -n flux-system") +} + +func createStructureAndManifests(baseDir string) { + // Directories to create + dirs := []string{ + filepath.Join(baseDir, "apps", "previews"), + filepath.Join(baseDir, "apps", "staging"), + filepath.Join(baseDir, "apps", "production"), + filepath.Join(baseDir, "namespaces"), + } + + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("[ERROR] Failed to create directory %s: %v\n", dir, err) + os.Exit(1) + } + // Create a .gitkeep file in the leaf directories under 'apps' + if strings.HasPrefix(filepath.Base(dir), "preview") || strings.HasPrefix(filepath.Base(dir), "staging") || strings.HasPrefix(filepath.Base(dir), "production") { + writeFile(filepath.Join(dir, ".gitkeep"), "") + } + } + + // Create namespace manifests + for _, ns := range []string{"previews", "staging", "production"} { + namespaceContent := fmt.Sprintf(`apiVersion: v1 +kind: Namespace +metadata: + name: %s +`, ns) + writeFile(filepath.Join(baseDir, "namespaces", ns+".yaml"), namespaceContent) + } + + // Create kustomization for namespaces + kustomizeNamespaces := `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - previews.yaml + - staging.yaml + - production.yaml +` + writeFile(filepath.Join(baseDir, "namespaces", "kustomization.yaml"), kustomizeNamespaces) + + // Create the ROOT kustomization file + kustomizeRoot := `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./namespaces + - ./apps +` + writeFile(filepath.Join(baseDir, "kustomization.yaml"), kustomizeRoot) +} + +// --- Helper Functions --- + +func writeFile(path, content string) { + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + fmt.Printf("[ERROR] Failed to write file %s: %v\n", path, err) + os.Exit(1) + } +} + +func promptForInput(prompt string) string { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("[PROMPT] %s ", prompt) + input, _ := reader.ReadString('\n') + return strings.TrimSpace(input) +} + +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +func runCommand(name string, args ...string) error { + cmd := exec.Command(name, args...) + // Suppress output for non-interactive checks + if name == "gh" && args[0] == "auth" && args[1] == "status" || + name == "gh" && args[0] == "repo" && args[1] == "view" { + cmd.Stdout = nil + cmd.Stderr = nil + } + return cmd.Run() +} + +func runInteractiveCommand(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func runCommandInDir(dir string, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} \ No newline at end of file