364 lines
10 KiB
Go
364 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var orgName, manifestsRepoName, fluxRepoName string
|
|
|
|
//go:embed templates/manifests.md.tmpl
|
|
var manifestsReadmeTmpl string
|
|
|
|
//go:embed templates/flux.md.tmpl
|
|
var fluxReadmeTmpl string
|
|
|
|
const fluxConfigTmpl = `---
|
|
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
|
kind: HelmRepository
|
|
metadata:
|
|
name: ghcr-charts
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 5m
|
|
type: oci
|
|
url: oci://ghcr.io/%s
|
|
secretRef:
|
|
name: ghcr-auth
|
|
---
|
|
apiVersion: source.toolkit.fluxcd.io/v1
|
|
kind: GitRepository
|
|
metadata:
|
|
name: cicd-deployment-manifests
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 1m0s
|
|
url: https://github.com/%s/%s
|
|
ref:
|
|
branch: main
|
|
secretRef:
|
|
name: github-auth
|
|
---
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: namespaces
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 10m
|
|
path: ./namespaces
|
|
prune: true
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: cicd-deployment-manifests
|
|
---
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: preview-apps
|
|
namespace: flux-system
|
|
spec:
|
|
dependsOn:
|
|
- name: namespaces
|
|
interval: 2m0s
|
|
path: ./apps/previews
|
|
prune: true
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: cicd-deployment-manifests
|
|
---
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: staging-apps
|
|
namespace: flux-system
|
|
spec:
|
|
dependsOn:
|
|
- name: namespaces
|
|
interval: 2m0s
|
|
path: ./apps/staging
|
|
prune: true
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: cicd-deployment-manifests
|
|
---
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: production-apps
|
|
namespace: flux-system
|
|
spec:
|
|
dependsOn:
|
|
- name: namespaces
|
|
interval: 2m0s
|
|
path: ./apps/production
|
|
prune: true
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: cicd-deployment-manifests
|
|
`
|
|
|
|
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 and flux repositories with the required structure.",
|
|
Run: runInitRepo,
|
|
}
|
|
|
|
// Define command flags
|
|
initCmd.Flags().StringVar(&orgName, "org", "", "The GitHub organization (e.g., Free-Maidn)")
|
|
initCmd.Flags().StringVar(&manifestsRepoName, "manifests-repo", "cicd-deployment-manifests", "The name of the manifests repository")
|
|
initCmd.Flags().StringVar(&fluxRepoName, "flux-repo", "", "The name of the flux repository")
|
|
initCmd.MarkFlagRequired("org")
|
|
initCmd.MarkFlagRequired("flux-repo")
|
|
|
|
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) {
|
|
// Check system dependencies
|
|
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)
|
|
}
|
|
|
|
// Verify GitHub authentication
|
|
fmt.Println("[INFO] Checking GitHub authentication...")
|
|
if err := runCommandQuiet("gh", "auth", "status"); err != nil {
|
|
fmt.Println("[ERROR] You are not logged into the GitHub CLI. Please run 'gh auth login'.")
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
|
|
|
|
// Initialize both repositories
|
|
initializeManifestsRepo()
|
|
initializeFluxRepo()
|
|
|
|
fmt.Println("\n🎉 Onboarding complete! 🎉")
|
|
fmt.Printf("✅ Manifests repository: https://github.com/%s/%s\n", orgName, manifestsRepoName)
|
|
fmt.Printf("✅ Flux repository: https://github.com/%s/%s\n", orgName, fluxRepoName)
|
|
}
|
|
|
|
func initializeManifestsRepo() {
|
|
fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName)
|
|
fmt.Printf("\n[INFO] Setting up manifests repository '%s'...\n", fullRepoName)
|
|
|
|
// Check if repository exists, create if not
|
|
if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil {
|
|
fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName)
|
|
if err := runCommand("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] Manifests repository created.")
|
|
} else {
|
|
fmt.Println("[SUCCESS] Manifests repository already exists.")
|
|
}
|
|
|
|
// Clone and setup repository structure
|
|
setupRepository(fullRepoName, createManifestsStructure)
|
|
}
|
|
|
|
func initializeFluxRepo() {
|
|
fullRepoName := fmt.Sprintf("%s/%s", orgName, fluxRepoName)
|
|
fmt.Printf("\n[INFO] Setting up flux repository '%s'...\n", fullRepoName)
|
|
|
|
// Check if repository exists, create if not
|
|
if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil {
|
|
fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName)
|
|
if err := runCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Flux CD cluster configurations"); err != nil {
|
|
fmt.Printf("[ERROR] Failed to create repository: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("[SUCCESS] Flux repository created.")
|
|
} else {
|
|
fmt.Println("[SUCCESS] Flux repository already exists.")
|
|
}
|
|
|
|
// Clone and setup repository structure
|
|
setupRepository(fullRepoName, createFluxStructure)
|
|
}
|
|
|
|
func setupRepository(fullRepoName string, structureFunc func(string)) {
|
|
// Create temporary directory for cloning
|
|
tempDir, err := os.MkdirTemp("", "repo-setup-*")
|
|
if err != nil {
|
|
fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Clone repository
|
|
fmt.Printf("[INFO] Cloning repository into temporary directory...\n")
|
|
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)
|
|
}
|
|
|
|
// Create repository structure
|
|
fmt.Println("[INFO] Creating repository structure...")
|
|
structureFunc(tempDir)
|
|
|
|
// Commit and push changes if any exist
|
|
fmt.Println("[INFO] Committing and pushing changes...")
|
|
commitAndPush(tempDir, fullRepoName)
|
|
}
|
|
|
|
func createManifestsStructure(baseDir string) {
|
|
// Create README for manifests repository
|
|
manifestsReadme := fmt.Sprintf(manifestsReadmeTmpl, manifestsRepoName)
|
|
writeFile(filepath.Join(baseDir, "README.md"), manifestsReadme)
|
|
|
|
// Create namespaces directory and files
|
|
namespacesDir := filepath.Join(baseDir, "namespaces")
|
|
if err := os.MkdirAll(namespacesDir, 0755); err != nil {
|
|
fmt.Printf("[ERROR] Failed to create namespaces directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Create staging namespace
|
|
stagingNamespace := `apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: staging
|
|
`
|
|
writeFile(filepath.Join(namespacesDir, "staging.yaml"), stagingNamespace)
|
|
|
|
// Create production namespace
|
|
productionNamespace := `apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: production
|
|
`
|
|
writeFile(filepath.Join(namespacesDir, "production.yaml"), productionNamespace)
|
|
|
|
// Create namespaces kustomization
|
|
namespacesKustomization := `apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
resources:
|
|
- staging.yaml
|
|
- production.yaml
|
|
`
|
|
writeFile(filepath.Join(namespacesDir, "kustomization.yaml"), namespacesKustomization)
|
|
|
|
// Create apps directory structure with .gitkeep files
|
|
environments := []string{"staging", "production", "previews"}
|
|
for _, env := range environments {
|
|
dirPath := filepath.Join(baseDir, "apps", env)
|
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
fmt.Printf("[ERROR] Failed to create directory %s: %v\n", dirPath, err)
|
|
os.Exit(1)
|
|
}
|
|
// Create .gitkeep to ensure empty directories are tracked
|
|
writeFile(filepath.Join(dirPath, ".gitkeep"), "")
|
|
}
|
|
}
|
|
|
|
func createFluxStructure(baseDir string) {
|
|
// Create README for flux repository
|
|
fluxReadme := fmt.Sprintf(fluxReadmeTmpl, fluxRepoName)
|
|
writeFile(filepath.Join(baseDir, "README.md"), fluxReadme)
|
|
|
|
// Create clusters/dev-cluster directory
|
|
clusterDir := filepath.Join(baseDir, "clusters", "dev-cluster")
|
|
if err := os.MkdirAll(clusterDir, 0755); err != nil {
|
|
fmt.Printf("[ERROR] Failed to create cluster directory: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Create the flux configuration file with dynamic organization name
|
|
fluxConfig := fmt.Sprintf(fluxConfigTmpl, strings.ToLower(orgName), orgName, manifestsRepoName)
|
|
writeFile(filepath.Join(clusterDir, "cicd-manifests-repo.yaml"), fluxConfig)
|
|
}
|
|
|
|
func commitAndPush(tempDir, repoName string) {
|
|
// Configure git user for the commit
|
|
runCommandInDir(tempDir, "git", "config", "user.name", "Maidn")
|
|
runCommandInDir(tempDir, "git", "config", "user.email", "maidn@free-maidn.com")
|
|
runCommandInDir(tempDir, "git", "add", ".")
|
|
|
|
// Check if there are changes to commit
|
|
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 {
|
|
// Commit and push changes
|
|
commitMsg := "feat: Initialize repository structure for CI/CD"
|
|
if err := runCommandInDir(tempDir, "git", "commit", "-m", commitMsg); err != nil {
|
|
fmt.Printf("[ERROR] Failed to commit changes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := runCommandInDir(tempDir, "git", "push", "origin", "main"); err != nil {
|
|
fmt.Printf("[ERROR] Failed to push changes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("[SUCCESS] Changes pushed to %s successfully.\n", repoName)
|
|
}
|
|
}
|
|
|
|
// Helper function to write content to a file
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Helper function to check if a command exists in PATH
|
|
func commandExists(cmd string) bool {
|
|
_, err := exec.LookPath(cmd)
|
|
return err == nil
|
|
}
|
|
|
|
// Helper function to run a command with output visible
|
|
func runCommand(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// Helper function to run a command quietly (no stdout)
|
|
func runCommandQuiet(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// Helper function to run a command in a specific directory
|
|
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()
|
|
}
|