chore: init

This commit is contained in:
eding 2025-08-21 23:19:27 +02:00
commit 813407cfbf
5 changed files with 288 additions and 0 deletions

21
.vscode/launch.json vendored Normal file
View file

@ -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}"
}
]
}

4
README.md Normal file
View file

@ -0,0 +1,4 @@
## Commands
go mod init github.com/Pingu-Studio/MaidnCLI
go get -u github.com/spf13/cobra@latest

9
go.mod Normal file
View file

@ -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
)

11
go.sum Normal file
View file

@ -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=

243
main.go Normal file
View file

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