chore: init
This commit is contained in:
commit
813407cfbf
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal 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
4
README.md
Normal 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
9
go.mod
Normal 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
11
go.sum
Normal 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
243
main.go
Normal 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()
|
||||
}
|
||||
Loading…
Reference in a new issue