feat: added readme template generation & automated flux manifest generation

This commit is contained in:
eding 2025-08-29 21:44:03 +02:00
parent b75c316ea7
commit 552a4fbbc4
4 changed files with 357 additions and 83 deletions

4
.vscode/launch.json vendored
View file

@ -13,7 +13,9 @@
"--org",
"Pingu-Studio",
"--manifests-repo",
"cicd-deployment-manifests"
"cicd-deployment-manifests-test",
"--flux-repo",
"maidn-cicd-test"
],
"cwd": "${workspaceFolder}"
}

321
main.go
View file

@ -1,15 +1,108 @@
package main
import (
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
var orgName, manifestsRepoName string
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{
@ -24,16 +117,20 @@ func main() {
var initCmd = &cobra.Command{
Use: "init",
Short: "Initializes the manifests repository with namespaces and environment directories.",
Short: "Initializes the manifests and flux repositories with the required structure.",
Run: runInitRepo,
}
initCmd.Flags().StringVar(&orgName, "org", "", "The GitHub organization (e.g., Pingu-Studio)")
// 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)
@ -41,12 +138,14 @@ func main() {
}
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'.")
@ -54,49 +153,164 @@ func runInitRepo(cmd *cobra.Command, args []string) {
}
fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName)
// Initialize both repositories
initializeManifestsRepo()
initializeFluxRepo()
fmt.Printf("[INFO] Checking for repository '%s'...\n", fullRepoName)
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("[WARNING] Repository '%s' not found. Creating it now...\n", fullRepoName)
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] Repository created.")
fmt.Println("[SUCCESS] Manifests repository created.")
} else {
fmt.Println("[SUCCESS] Repository already exists.")
fmt.Println("[SUCCESS] Manifests repository already exists.")
}
tempDir, err := os.MkdirTemp("", "manifests-repo-*")
// 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)
fmt.Printf("[INFO] Cloning repository into %s...\n", 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)
}
fmt.Println("[INFO] Ensuring standard directory structure and manifests exist...")
createStructureAndManifests(tempDir)
// Create repository structure
fmt.Println("[INFO] Creating repository structure...")
structureFunc(tempDir)
fmt.Println("[INFO] Committing and pushing changes if any...")
runCommandInDir(tempDir, "git", "config", "user.name", "CICD Tool Initializer")
runCommandInDir(tempDir, "git", "config", "user.email", "actions@github.com")
// 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 initialized correctly.")
fmt.Println("[INFO] No changes to commit. Repository is already up to date.")
} else {
commitMsg := "feat: Initialize repository structure with namespaces and apps"
// 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)
@ -105,74 +319,11 @@ func runInitRepo(cmd *cobra.Command, args []string) {
fmt.Printf("[ERROR] Failed to push changes: %v\n", err)
os.Exit(1)
}
fmt.Println("[SUCCESS] Changes pushed successfully.")
}
fmt.Println("\n🎉 Onboarding complete! 🎉")
}
func createStructureAndManifests(baseDir string) {
environments := []string{"previews", "staging", "production"}
placeholderContent := `apiVersion: v1
kind: ConfigMap
metadata:
name: placeholder
data:
info: "This file ensures Kustomize globs do not fail in an empty environment."
`
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)
}
writeFile(filepath.Join(dirPath, "placeholder.yaml"), placeholderContent)
}
appsKustomizationContent := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- previews/*.yaml
- staging/*.yaml
- production/*.yaml
`
writeFile(filepath.Join(baseDir, "apps", "kustomization.yaml"), appsKustomizationContent)
namespacesDir := filepath.Join(baseDir, "namespaces")
if err := os.MkdirAll(namespacesDir, 0755); err != nil {
fmt.Printf("[ERROR] Failed to create directory %s: %v\n", namespacesDir, err)
os.Exit(1)
}
for _, ns := range environments {
namespaceContent := fmt.Sprintf(`apiVersion: v1
kind: Namespace
metadata:
name: %s
`, ns)
writeFile(filepath.Join(namespacesDir, ns+".yaml"), namespaceContent)
}
kustomizeNamespaces := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- previews.yaml
- staging.yaml
- production.yaml
`
writeFile(filepath.Join(namespacesDir, "kustomization.yaml"), kustomizeNamespaces)
kustomizeRoot := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./namespaces
- ./apps
`
writeFile(filepath.Join(baseDir, "kustomization.yaml"), kustomizeRoot)
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)
@ -180,11 +331,13 @@ func writeFile(path, content string) {
}
}
// 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
@ -192,6 +345,7 @@ func runCommand(name string, args ...string) error {
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
@ -199,6 +353,7 @@ func runCommandQuiet(name string, args ...string) error {
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

72
templates/flux.md.tmpl Normal file
View file

@ -0,0 +1,72 @@
# %s
## Overview
This repository contains the core configuration for Flux CD, which manages our Kubernetes clusters using a GitOps approach. It defines the sources of truth for our applications and the automation rules for deploying them.
## Repository Structure
The structure is designed to be minimal, with all cluster-specific configurations located in a dedicated directory.
```
.
├── clusters/
│ └── dev-cluster/
│ └── cicd-manifests-repo.yaml # Flux resource definitions
└── README.md
```
## Configuration Details
The **/clusters/dev-cluster** directory contains the primary Flux CD custom resources that orchestrate the deployment pipeline:
* **`HelmRepository`**: Configures access to the OCI-based Helm chart registry where application charts are stored.
* **`GitRepository`**: Defines the source Git repository (`cicd-deployment-manifests`) that holds the application deployment manifests.
* **`Kustomization`**: Specifies how the manifests from the source repository should be applied to the cluster. Multiple `Kustomization` resources are used to manage different environments and enforce deployment dependencies (e.g., ensuring namespaces exist before applications are deployed).
## Operational Workflow
1. **Source Monitoring**: Flux continuously monitors the `cicd-deployment-manifests` repository for any new commits to the `main` branch.
2. **Automated Reconciliation**: Upon detecting changes, Flux automatically fetches the latest manifests and applies them to the cluster.
3. **Dependency Management**: The `dependsOn` attribute in the `Kustomization` resources ensures that deployments occur in the correct order.
4. **Pruning**: Flux is configured to prune resources. If a manifest is deleted from the Git repository, Flux will automatically delete the corresponding resource from the Kubernetes cluster.
## Prerequisites
For Flux to operate correctly, the Kubernetes cluster must be bootstrapped with the following secrets in the `flux-system` namespace.
### Required Secrets
**1. GitHub Authentication (`github-auth`)**
A secret containing a GitHub Personal Access Token (PAT) with repository access.
```yaml
apiVersion: v1
kind: Secret
metadata:
name: github-auth
namespace: flux-system
type: Opaque
data:
username: <BASE64_ENCODED_GITHUB_USERNAME>
password: <BASE64_ENCODED_GITHUB_PAT>
```
**2. Container Registry Authentication (`ghcr-auth`)**
A Docker registry secret for pulling images from the GitHub Container Registry (GHCR).
```yaml
apiVersion: v1
kind: Secret
metadata:
name: ghcr-auth
namespace: flux-system
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <BASE64_ENCODED_DOCKER_CONFIG_JSON>
```
---
*This repository is automatically generated and managed by internal CI/CD tooling.*

View file

@ -0,0 +1,45 @@
# %s
## Overview
This repository serves as the centralized source for all Kubernetes deployment manifests. It is managed by Flux CD, adhering to GitOps principles to ensure that the state of the Kubernetes clusters reflects the configuration defined here.
## Repository Structure
The repository is organized to support multiple environments and to provide a clear separation of concerns.
```
.
├── apps/
│ ├── previews/ # Manifests for dynamic preview environments
│ ├── staging/ # Manifests for the staging environment
│ └── production/ # Manifests for the production environment
├── namespaces/ # Core Kubernetes namespace definitions
│ ├── staging.yaml
│ ├── production.yaml
│ └── kustomization.yaml
└── README.md
```
### Key Directories
* **/namespaces**: Contains the YAML definitions for Kubernetes namespaces used across all environments. This ensures that foundational resources are managed consistently.
* **/apps**: Holds the application-specific deployment manifests, categorized by environment.
## Environment Management
- **Production**: The live environment serving end-users. Changes to this directory directly impact production services.
- **Staging**: A pre-production environment that mirrors production. It is used for final testing and validation before deploying changes to production.
- **Previews**: A dynamic environment for deploying feature branches or pull requests. These are short-lived and used for testing and review purposes.
## Deployment Workflow
Changes are deployed automatically by Flux CD, which continuously monitors this repository. The standard workflow is as follows:
1. **Commit Manifests**: A developer or an automated process commits new or updated Kubernetes manifests to the appropriate directory (e.g., `apps/staging`).
2. **Push to `main`**: The changes are pushed to the `main` branch.
3. **Flux Synchronization**: Flux detects the changes in the repository.
4. **Cluster Reconciliation**: Flux applies the manifest changes to the corresponding Kubernetes cluster, ensuring the cluster's state matches the repository's configuration.
---
*This repository is automatically generated and managed by internal CI/CD tooling.*