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() }