feat: restructure & secret setup
This commit is contained in:
parent
b2b43a98fd
commit
7668d0d180
57
cmd/repo.go
Normal file
57
cmd/repo.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/github"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var orgName, manifestsRepoName, fluxRepoName string
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(repoCmd)
|
||||
repoCmd.AddCommand(initCmd)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func runInitRepo(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("[INFO] Checking dependencies (git and gh)...")
|
||||
if !utils.CommandExists("git") || !utils.CommandExists("gh") {
|
||||
fmt.Println("[ERROR] 'git' and 'gh' must be installed and in your PATH.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("[INFO] Checking GitHub authentication...")
|
||||
if err := utils.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.")
|
||||
|
||||
repoManager := github.NewRepoManager(orgName, manifestsRepoName, fluxRepoName)
|
||||
repoManager.InitializeManifestsRepo()
|
||||
repoManager.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)
|
||||
}
|
||||
21
cmd/root.go
Normal file
21
cmd/root.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cicd-tool",
|
||||
Short: "A CLI tool to manage CI/CD setup for applications.",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
137
cmd/vault.go
Normal file
137
cmd/vault.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/k8s"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/ui"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/vault"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var vaultCmd = &cobra.Command{
|
||||
Use: "vault",
|
||||
Short: "Manage Vault secrets for CI/CD.",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
vaultCmd.AddCommand(createDockerSecretCmd)
|
||||
vaultCmd.AddCommand(createGhcrSecretCmd)
|
||||
vaultCmd.AddCommand(createGithubSecretCmd)
|
||||
}
|
||||
|
||||
var createDockerSecretCmd = &cobra.Command{
|
||||
Use: "create-docker-secret",
|
||||
Short: "Creates a secret in Vault for Docker Hub and applies it to Kubernetes.",
|
||||
Run: runCreateDockerSecret,
|
||||
}
|
||||
|
||||
var createGhcrSecretCmd = &cobra.Command{
|
||||
Use: "create-ghcr-secret",
|
||||
Short: "Creates secrets in Vault for GHCR.",
|
||||
Run: runCreateGhcrSecret,
|
||||
}
|
||||
|
||||
var createGithubSecretCmd = &cobra.Command{
|
||||
Use: "create-github-secret",
|
||||
Short: "Creates a GitHub authentication secret in Vault.",
|
||||
Run: runCreateGithubSecret,
|
||||
}
|
||||
|
||||
func runCreateDockerSecret(cmd *cobra.Command, args []string) {
|
||||
if err := k8s.CheckExternalSecretCRD(); err != nil {
|
||||
fmt.Printf("[ERROR] Prerequisite check failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
vm, err := vault.NewVaultManager()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
username, password, err := ui.GetUserInputForDockerHub()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dockerConfigJSON, err := vault.CreateDockerConfig(vault.DockerHubRegistry, username, password)
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create Docker config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
secretData := map[string]string{vault.DockerConfigJSONKey: dockerConfigJSON}
|
||||
if err := vm.StoreSecret(vault.DockerHubSecretPath, secretData); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to store Docker Hub secret in Vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := k8s.CreateDockerHubExternalSecret(); err != nil {
|
||||
// Call our new universal handler for the specific cache error.
|
||||
utils.HandleKubectlCacheError(err)
|
||||
|
||||
// Provide a generic warning for all other potential errors.
|
||||
if !strings.Contains(err.Error(), "no matches for kind") {
|
||||
fmt.Printf("[WARNING] The secret was stored in Vault, but failed to create the ExternalSecret: %v\n", err)
|
||||
}
|
||||
os.Exit(1) // Exit after handling the error to prompt the user to re-run.
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateGhcrSecret(cmd *cobra.Command, args []string) {
|
||||
vm, err := vault.NewVaultManager()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
username, registryToken, orgToken, err := ui.GetUserInputForGhcr()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dockerConfigJSON, err := vault.CreateDockerConfig(vault.GhcrRegistry, username, registryToken)
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create Docker config for GHCR: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dockerSecretData := map[string]string{vault.DockerConfigJSONKey: dockerConfigJSON}
|
||||
if err := vm.StoreSecret(vault.GhcrSecretPath, dockerSecretData); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to store GHCR secret in Vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
orgSecretData := map[string]string{vault.OrgManagementPATKey: orgToken}
|
||||
if err := vm.StoreSecret(vault.OrgManagementSecretPath, orgSecretData); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to store organization management PAT in Vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateGithubSecret(cmd *cobra.Command, args []string) {
|
||||
vm, err := vault.NewVaultManager()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
username, token, err := ui.GetUserInputForGithubAuth()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
secretData := map[string]string{"username": username, "password": token}
|
||||
if err := vm.StoreSecret(vault.GithubAuthSecretPath, secretData); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to store GitHub auth secret in Vault: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
11
go.mod
11
go.mod
|
|
@ -1,14 +1,15 @@
|
|||
module github.com/Pingu-Studio/MaidnCLI
|
||||
|
||||
go 1.25.0
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
golang.org/x/term v0.34.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
golang.org/x/term v0.36.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
)
|
||||
|
|
|
|||
14
go.sum
14
go.sum
|
|
@ -1,15 +1,29 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
9
internal/assets/assets.go
Normal file
9
internal/assets/assets.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package assets
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed templates/manifests.md.tmpl
|
||||
var ManifestsReadmeTmpl string
|
||||
|
||||
//go:embed templates/flux.md.tmpl
|
||||
var FluxReadmeTmpl string
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -6,6 +6,9 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/assets"
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
|
||||
)
|
||||
|
||||
const fluxConfigTmpl = `---
|
||||
|
|
@ -93,14 +96,30 @@ spec:
|
|||
name: cicd-deployment-manifests
|
||||
`
|
||||
|
||||
func initializeManifestsRepo() {
|
||||
fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName)
|
||||
// RepoManager holds the state for repository operations
|
||||
type RepoManager struct {
|
||||
OrgName string
|
||||
ManifestsRepoName string
|
||||
FluxRepoName string
|
||||
}
|
||||
|
||||
// NewRepoManager creates a new instance of RepoManager
|
||||
func NewRepoManager(org, manifestsRepo, fluxRepo string) *RepoManager {
|
||||
return &RepoManager{
|
||||
OrgName: org,
|
||||
ManifestsRepoName: manifestsRepo,
|
||||
FluxRepoName: fluxRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeManifestsRepo sets up the manifests repository
|
||||
func (rm *RepoManager) InitializeManifestsRepo() {
|
||||
fullRepoName := fmt.Sprintf("%s/%s", rm.OrgName, rm.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 {
|
||||
if err := utils.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 {
|
||||
if err := utils.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)
|
||||
}
|
||||
|
|
@ -109,18 +128,17 @@ func initializeManifestsRepo() {
|
|||
fmt.Println("[SUCCESS] Manifests repository already exists.")
|
||||
}
|
||||
|
||||
// Clone and setup repository structure
|
||||
setupRepository(fullRepoName, createManifestsStructure)
|
||||
rm.setupRepository(fullRepoName, rm.createManifestsStructure)
|
||||
}
|
||||
|
||||
func initializeFluxRepo() {
|
||||
fullRepoName := fmt.Sprintf("%s/%s", orgName, fluxRepoName)
|
||||
// InitializeFluxRepo sets up the flux repository
|
||||
func (rm *RepoManager) InitializeFluxRepo() {
|
||||
fullRepoName := fmt.Sprintf("%s/%s", rm.OrgName, rm.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 {
|
||||
if err := utils.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 {
|
||||
if err := utils.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)
|
||||
}
|
||||
|
|
@ -129,12 +147,10 @@ func initializeFluxRepo() {
|
|||
fmt.Println("[SUCCESS] Flux repository already exists.")
|
||||
}
|
||||
|
||||
// Clone and setup repository structure
|
||||
setupRepository(fullRepoName, createFluxStructure)
|
||||
rm.setupRepository(fullRepoName, rm.createFluxStructure)
|
||||
}
|
||||
|
||||
func setupRepository(fullRepoName string, structureFunc func(string)) {
|
||||
// Create temporary directory for cloning
|
||||
func (rm *RepoManager) setupRepository(fullRepoName string, structureFunc func(string)) {
|
||||
tempDir, err := os.MkdirTemp("", "repo-setup-*")
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err)
|
||||
|
|
@ -142,97 +158,73 @@ func setupRepository(fullRepoName string, structureFunc func(string)) {
|
|||
}
|
||||
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 {
|
||||
if err := utils.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)
|
||||
func (rm *RepoManager) createManifestsStructure(baseDir string) {
|
||||
manifestsReadme := fmt.Sprintf(assets.ManifestsReadmeTmpl, rm.ManifestsRepoName)
|
||||
utils.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)
|
||||
}
|
||||
os.MkdirAll(namespacesDir, 0755)
|
||||
|
||||
// Create staging namespace
|
||||
stagingNamespace := `apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: staging
|
||||
`
|
||||
writeFile(filepath.Join(namespacesDir, "staging.yaml"), stagingNamespace)
|
||||
utils.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)
|
||||
utils.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)
|
||||
utils.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"), "")
|
||||
os.MkdirAll(dirPath, 0755)
|
||||
utils.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)
|
||||
func (rm *RepoManager) createFluxStructure(baseDir string) {
|
||||
fluxReadme := fmt.Sprintf(assets.FluxReadmeTmpl, rm.FluxRepoName)
|
||||
utils.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)
|
||||
}
|
||||
os.MkdirAll(clusterDir, 0755)
|
||||
|
||||
// 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)
|
||||
fluxConfig := fmt.Sprintf(fluxConfigTmpl, strings.ToLower(rm.OrgName), rm.OrgName, rm.ManifestsRepoName)
|
||||
utils.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", ".")
|
||||
utils.RunCommandInDir(tempDir, "git", "config", "user.name", "Maidn")
|
||||
utils.RunCommandInDir(tempDir, "git", "config", "user.email", "maidn@free-maidn.com")
|
||||
utils.RunCommandInDir(tempDir, "git", "add", ".")
|
||||
|
||||
// Check if there are changes to commit
|
||||
statusCmd := exec.Command("git", "status", "--porcelain")
|
||||
statusCmd.Dir = tempDir
|
||||
output, _ := statusCmd.Output()
|
||||
|
|
@ -240,13 +232,12 @@ func commitAndPush(tempDir, repoName string) {
|
|||
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 {
|
||||
if err := utils.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 {
|
||||
if err := utils.RunCommandInDir(tempDir, "git", "push", "origin", "main"); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to push changes: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
131
internal/k8s/external_secret.go
Normal file
131
internal/k8s/external_secret.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/vault"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ExternalSecret Structs to represent the YAML structure
|
||||
type ExternalSecret struct {
|
||||
ApiVersion string `yaml:"apiVersion"`
|
||||
Kind string `yaml:"kind"`
|
||||
Metadata Metadata `yaml:"metadata"`
|
||||
Spec ExternalSecretSpec `yaml:"spec"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Name string `yaml:"name"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
}
|
||||
|
||||
type ExternalSecretSpec struct {
|
||||
SecretStoreRef SecretStoreRef `yaml:"secretStoreRef"`
|
||||
Target ExternalSecretTarget `yaml:"target"`
|
||||
Data []ExternalSecretData `yaml:"data"`
|
||||
}
|
||||
|
||||
type SecretStoreRef struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
}
|
||||
|
||||
type ExternalSecretTarget struct {
|
||||
Name string `yaml:"name"`
|
||||
CreationPolicy string `yaml:"creationPolicy"`
|
||||
Template ExternalSecretTemplate `yaml:"template,omitempty"`
|
||||
}
|
||||
|
||||
type ExternalSecretTemplate struct {
|
||||
Type string `yaml:"type"`
|
||||
Data map[string]string `yaml:"data"`
|
||||
}
|
||||
|
||||
type ExternalSecretData struct {
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
RemoteRef ExternalSecretDataRemoteRef `yaml:"remoteRef"`
|
||||
}
|
||||
|
||||
type ExternalSecretDataRemoteRef struct {
|
||||
Key string `yaml:"key"`
|
||||
Property string `yaml:"property"`
|
||||
}
|
||||
|
||||
// CheckExternalSecretCRD checks if the ExternalSecret CRD is installed on the cluster.
|
||||
func CheckExternalSecretCRD() error {
|
||||
fmt.Println("[INFO] Verifying that the ExternalSecret CRD is installed...")
|
||||
// Use api-resources to check if the resource kind exists in the specified API group
|
||||
cmd := exec.Command("kubectl", "api-resources", "--api-group=external-secrets.io", "-o", "name")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
// This error occurs if kubectl fails or the API group doesn't exist at all.
|
||||
return fmt.Errorf("failed to query for ExternalSecret CRD. Is kubectl configured correctly? Error: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
// We expect the output to contain "externalsecrets.external-secrets.io"
|
||||
if !strings.Contains(string(output), "externalsecrets.external-secrets.io") {
|
||||
return fmt.Errorf("the 'ExternalSecret' CRD was not found on the cluster. Please install the External Secrets Operator first.\nSee: https://external-secrets.io/latest/getting-started/installation/")
|
||||
}
|
||||
|
||||
fmt.Println("[SUCCESS] ExternalSecret CRD found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDockerHubExternalSecret generates and applies the ExternalSecret for Docker Hub.
|
||||
func CreateDockerHubExternalSecret() error {
|
||||
fmt.Println("[INFO] Creating ExternalSecret for Docker Hub in 'flux-system' namespace...")
|
||||
|
||||
externalSecret := ExternalSecret{
|
||||
ApiVersion: "external-secrets.io/v1beta1",
|
||||
Kind: "ExternalSecret",
|
||||
Metadata: Metadata{
|
||||
Name: "dockerhub-auth",
|
||||
Namespace: "flux-system",
|
||||
},
|
||||
Spec: ExternalSecretSpec{
|
||||
SecretStoreRef: SecretStoreRef{
|
||||
Name: "vault-backend",
|
||||
Kind: "ClusterSecretStore",
|
||||
},
|
||||
Target: ExternalSecretTarget{
|
||||
Name: "dockerhub-auth",
|
||||
CreationPolicy: "Owner",
|
||||
Template: ExternalSecretTemplate{
|
||||
Type: "kubernetes.io/dockerconfigjson",
|
||||
Data: map[string]string{
|
||||
".dockerconfigjson": "{{ .dockerconfigjson }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
Data: []ExternalSecretData{
|
||||
{
|
||||
SecretKey: "dockerconfigjson",
|
||||
RemoteRef: ExternalSecretDataRemoteRef{
|
||||
Key: vault.DockerHubSecretPath,
|
||||
Property: vault.DockerConfigJSONKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
yamlData, err := yaml.Marshal(&externalSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal ExternalSecret to YAML: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("kubectl", "apply", "-f", "-")
|
||||
cmd.Stdin = bytes.NewReader(yamlData)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply ExternalSecret: %s\n%w", string(output), err)
|
||||
}
|
||||
|
||||
fmt.Printf("[SUCCESS] Applied ExternalSecret 'dockerhub-auth' in 'flux-system' namespace.\n")
|
||||
return nil
|
||||
}
|
||||
120
internal/ui/prompts.go
Normal file
120
internal/ui/prompts.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// GetUserInputForGhcr prompts the user for GitHub-related credentials for GHCR.
|
||||
func GetUserInputForGhcr() (username, registryToken, orgToken string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Enter your GitHub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
err = fmt.Errorf("username cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
registryToken, err = promptForToken(reader, "Enter your GitHub PAT for container registry")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
orgToken, err = promptForToken(reader, "Enter your GitHub PAT for organization management")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserInputForDockerHub prompts the user for Docker Hub credentials.
|
||||
func GetUserInputForDockerHub() (username, password string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Enter your Docker Hub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Print("Enter your Docker Hub password or access token: ")
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
password = string(passwordBytes)
|
||||
|
||||
if username == "" || password == "" {
|
||||
err = fmt.Errorf("username and password cannot be empty")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserInputForGithubAuth prompts the user for a general GitHub PAT.
|
||||
func GetUserInputForGithubAuth() (username, token string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Enter your GitHub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
err = fmt.Errorf("username cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
token, err = promptForToken(reader, "Enter your GitHub PAT for repository access")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
|
||||
fmt.Printf("%s: ", prompt)
|
||||
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := string(tokenBytes)
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
|
||||
validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
|
||||
hasValidPrefix := false
|
||||
for _, prefix := range validPrefixes {
|
||||
if strings.HasPrefix(token, prefix) {
|
||||
hasValidPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasValidPrefix {
|
||||
fmt.Printf("[WARNING] The token does not appear to be a valid GitHub PAT (should start with one of %v)\n", validPrefixes)
|
||||
fmt.Print("Do you want to continue anyway? (y/N): ")
|
||||
confirm, _ := reader.ReadString('\n')
|
||||
if strings.TrimSpace(strings.ToLower(confirm)) != "y" {
|
||||
return "", fmt.Errorf("operation cancelled by user")
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
99
internal/utils/helpers.go
Normal file
99
internal/utils/helpers.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteFile writes content to a file, exiting on error.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// CommandExists checks if a command exists in the system's PATH.
|
||||
func CommandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// RunCommand runs a command with its output visible to the user.
|
||||
func RunCommand(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// RunCommandQuiet runs a command without printing its stdout.
|
||||
func RunCommandQuiet(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// RunCommandInDir runs 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()
|
||||
}
|
||||
|
||||
// HandleKubectlCacheError detects a specific kubectl cache error and offers an interactive fix.
|
||||
func HandleKubectlCacheError(err error) {
|
||||
// Guard clause: Only proceed if the error is the one we're looking for.
|
||||
if err == nil || !strings.Contains(err.Error(), "no matches for kind") {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n[DIAGNOSIS] A common kubectl cache issue was detected.")
|
||||
fmt.Println("This happens when kubectl's local cache is out of sync with the cluster, often after installing new CRDs.")
|
||||
fmt.Print("Would you like to attempt to clear the cache now? (y/N): ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
|
||||
if strings.TrimSpace(strings.ToLower(response)) != "y" {
|
||||
fmt.Println("[INFO] Operation cancelled by user.")
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var shell string
|
||||
|
||||
// Determine the correct command based on the operating system.
|
||||
if runtime.GOOS == "windows" {
|
||||
shell = "PowerShell"
|
||||
// Using -NoProfile and -NonInteractive for cleaner execution
|
||||
cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", "Remove-Item -Path $env:USERPROFILE\\.kube\\cache -Recurse -Force")
|
||||
} else {
|
||||
shell = "shell"
|
||||
// The user's home directory can be fetched reliably.
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Printf("[ERROR] Could not determine home directory to clear cache: %v\n", err)
|
||||
return
|
||||
}
|
||||
cachePath := fmt.Sprintf("%s/.kube/cache", homeDir)
|
||||
cmd = exec.Command("rm", "-rf", cachePath)
|
||||
}
|
||||
|
||||
fmt.Printf("[INFO] Attempting to clear kubectl cache using %s...\n", shell)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to clear kubectl cache: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("[SUCCESS] Kubectl cache cleared successfully.")
|
||||
fmt.Println("[INFO] Please try running the previous command again.")
|
||||
}
|
||||
}
|
||||
167
internal/vault/client.go
Normal file
167
internal/vault/client.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
|
||||
)
|
||||
|
||||
// Constants for Vault and Kubernetes
|
||||
const (
|
||||
vaultNamespace = "vault"
|
||||
vaultPodLabelSelector = "app.kubernetes.io/name=vault"
|
||||
vaultRootTokenSecretName = "vault-unseal-keys"
|
||||
vaultRootTokenSecretKey = "vault-root"
|
||||
defaultVaultAddress = "https://127.0.0.1:8200"
|
||||
|
||||
// Exported constants
|
||||
GhcrSecretPath = "secret/ghcr-auth"
|
||||
DockerHubSecretPath = "secret/dockerhub-auth"
|
||||
GithubAuthSecretPath = "secret/github-auth"
|
||||
OrgManagementSecretPath = "secret/org-management-pat"
|
||||
DockerConfigJSONKey = ".dockerconfigjson"
|
||||
OrgManagementPATKey = "ORG_MANAGEMENT_PAT"
|
||||
GhcrRegistry = "ghcr.io"
|
||||
DockerHubRegistry = "docker.io"
|
||||
)
|
||||
|
||||
// DockerConfig represents the structure of a Docker config.json
|
||||
type DockerConfig struct {
|
||||
Auths map[string]DockerAuth `json:"auths"`
|
||||
}
|
||||
|
||||
// DockerAuth represents the authentication info for a Docker registry
|
||||
type DockerAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Auth string `json:"auth"`
|
||||
}
|
||||
|
||||
// VaultManager handles all interactions with Kubernetes and Vault.
|
||||
type VaultManager struct {
|
||||
podName string
|
||||
rootToken string
|
||||
}
|
||||
|
||||
// NewVaultManager creates and initializes a new VaultManager.
|
||||
func NewVaultManager() (*VaultManager, error) {
|
||||
vm := &VaultManager{}
|
||||
|
||||
fmt.Println("[INFO] Checking dependencies (kubectl)...")
|
||||
if !utils.CommandExists("kubectl") {
|
||||
return nil, fmt.Errorf("'kubectl' must be installed and in your PATH")
|
||||
}
|
||||
|
||||
fmt.Println("[INFO] Finding Vault pod...")
|
||||
podName, err := vm.getVaultPodName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vm.podName = podName
|
||||
fmt.Printf("[INFO] Using Vault pod: %s\n", vm.podName)
|
||||
|
||||
fmt.Println("[INFO] Retrieving Vault root token...")
|
||||
rootToken, err := vm.getVaultRootToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vm.rootToken = rootToken
|
||||
fmt.Println("[SUCCESS] Found Vault root token.")
|
||||
|
||||
fmt.Println("[INFO] Verifying Vault connection and token...")
|
||||
if err := vm.verifyVaultToken(); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate Vault token: %w", err)
|
||||
}
|
||||
fmt.Println("[SUCCESS] Vault token is valid.")
|
||||
|
||||
return vm, nil
|
||||
}
|
||||
|
||||
func (vm *VaultManager) getVaultPodName() (string, error) {
|
||||
commonNames := []string{"vault-0", "vault-1", "vault-2"}
|
||||
for _, name := range commonNames {
|
||||
if err := utils.RunCommandQuiet("kubectl", "get", "pod", name, "-n", vaultNamespace); err == nil {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-o", "jsonpath={.items[0].metadata.name}")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(output) > 0 {
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find any running Vault pods in the '%s' namespace", vaultNamespace)
|
||||
}
|
||||
|
||||
func (vm *VaultManager) getVaultRootToken() (string, error) {
|
||||
cmd := exec.Command("kubectl", "get", "secret", vaultRootTokenSecretName, "-n", vaultNamespace, "-o", fmt.Sprintf("jsonpath={.data.%s}", vaultRootTokenSecretKey))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get '%s' secret from '%s' namespace: %w", vaultRootTokenSecretName, vaultNamespace, err)
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return "", fmt.Errorf("key '%s' not found in secret '%s'", vaultRootTokenSecretKey, vaultRootTokenSecretName)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(string(output))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode base64 root token: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(decoded)), nil
|
||||
}
|
||||
|
||||
func (vm *VaultManager) runVaultCommandQuiet(command string) error {
|
||||
envSetup := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='%s' && export VAULT_TOKEN='%s'", defaultVaultAddress, vm.rootToken)
|
||||
fullCmd := fmt.Sprintf("%s && %s", envSetup, command)
|
||||
|
||||
return utils.RunCommandQuiet("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd)
|
||||
}
|
||||
|
||||
func (vm *VaultManager) verifyVaultToken() error {
|
||||
return vm.runVaultCommandQuiet("vault token lookup")
|
||||
}
|
||||
|
||||
// StoreSecret stores a key-value secret at the specified path in Vault quietly.
|
||||
func (vm *VaultManager) StoreSecret(path string, data map[string]string) error {
|
||||
fmt.Printf("[INFO] Storing secret in Vault at path: %s...\n", path)
|
||||
|
||||
var kvPairs []string
|
||||
for key, value := range data {
|
||||
escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'")
|
||||
kvPairs = append(kvPairs, fmt.Sprintf("%s='%s'", key, escapedValue))
|
||||
}
|
||||
|
||||
vaultCmd := fmt.Sprintf("vault kv put %s %s", path, strings.Join(kvPairs, " "))
|
||||
if err := vm.runVaultCommandQuiet(vaultCmd); err != nil {
|
||||
return fmt.Errorf("failed to store secret in Vault: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[SUCCESS] Secret at '%s' stored successfully in Vault!\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateDockerConfig creates a Docker config JSON string.
|
||||
func CreateDockerConfig(registry, username, token string) (string, error) {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
|
||||
config := DockerConfig{
|
||||
Auths: map[string]DockerAuth{
|
||||
registry: {
|
||||
Username: username,
|
||||
Password: token,
|
||||
Auth: auth,
|
||||
},
|
||||
},
|
||||
}
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal docker config: %w", err)
|
||||
}
|
||||
return string(configJSON), nil
|
||||
}
|
||||
120
main.go
120
main.go
|
|
@ -1,125 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/Pingu-Studio/MaidnCLI/cmd"
|
||||
)
|
||||
|
||||
var orgName, manifestsRepoName, fluxRepoName string
|
||||
|
||||
//go:embed templates/manifests.md.tmpl
|
||||
var manifestsReadmeTmpl string
|
||||
|
||||
//go:embed templates/flux.md.tmpl
|
||||
var fluxReadmeTmpl 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 vaultCmd = &cobra.Command{
|
||||
Use: "vault",
|
||||
Short: "Manage Vault secrets for CI/CD.",
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initializes the manifests and flux repositories with the required structure.",
|
||||
Run: runInitRepo,
|
||||
}
|
||||
|
||||
var createDockerSecretCmd = &cobra.Command{
|
||||
Use: "create-docker-secret",
|
||||
Short: "Creates a Docker registry secret in Vault for Docker Hub.",
|
||||
Run: runCreateDockerSecret,
|
||||
}
|
||||
|
||||
var createGhcrSecretCmd = &cobra.Command{
|
||||
Use: "create-ghcr-secret",
|
||||
Short: "Creates a Docker registry secret in Vault for GHCR.",
|
||||
Run: runCreateGhcrSecret,
|
||||
}
|
||||
|
||||
var createGithubSecretCmd = &cobra.Command{
|
||||
Use: "create-github-secret",
|
||||
Short: "Creates or updates a GitHub authentication secret in Vault.",
|
||||
Run: runCreateGithubSecret,
|
||||
}
|
||||
|
||||
// Define command flags for repo init
|
||||
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")
|
||||
|
||||
// Add commands to their parent commands
|
||||
repoCmd.AddCommand(initCmd)
|
||||
vaultCmd.AddCommand(createDockerSecretCmd)
|
||||
vaultCmd.AddCommand(createGhcrSecretCmd)
|
||||
vaultCmd.AddCommand(createGithubSecretCmd)
|
||||
rootCmd.AddCommand(repoCmd)
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
|
||||
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 runCreateDockerSecret(cmd *cobra.Command, args []string) {
|
||||
if err := createDockerHubSecretWorkflow(); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create Docker Hub secret: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateGhcrSecret(cmd *cobra.Command, args []string) {
|
||||
if err := createGhcrSecretWorkflow(); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create GHCR secret: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateGithubSecret(cmd *cobra.Command, args []string) {
|
||||
if err := createAndStoreGithubAuthSecret(); err != nil {
|
||||
fmt.Printf("[ERROR] Failed to create GitHub auth secret: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("[SUCCESS] GitHub auth secret created successfully in Vault!")
|
||||
cmd.Execute()
|
||||
}
|
||||
|
|
|
|||
46
utils.go
46
utils.go
|
|
@ -1,46 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
376
vault.go
376
vault.go
|
|
@ -1,376 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// --- Constants for Vault and Kubernetes ---
|
||||
const (
|
||||
vaultNamespace = "vault"
|
||||
vaultPodLabelSelector = "app.kubernetes.io/name=vault"
|
||||
vaultRootTokenSecretName = "vault-unseal-keys"
|
||||
vaultRootTokenSecretKey = "vault-root"
|
||||
ghcrSecretPath = "secret/ghcr-auth"
|
||||
dockerHubSecretPath = "secret/dockerhub-auth"
|
||||
githubAuthSecretPath = "secret/github-auth"
|
||||
orgManagementSecretPath = "secret/org-management-pat"
|
||||
dockerConfigJSONKey = ".dockerconfigjson"
|
||||
orgManagementPATKey = "ORG_MANAGEMENT_PAT"
|
||||
ghcrRegistry = "ghcr.io"
|
||||
dockerHubRegistry = "docker.io"
|
||||
defaultVaultAddress = "https://127.0.0.1:8200"
|
||||
)
|
||||
|
||||
// --- Structs for Docker Config ---
|
||||
|
||||
// DockerConfig represents the structure of a Docker config.json
|
||||
type DockerConfig struct {
|
||||
Auths map[string]DockerAuth `json:"auths"`
|
||||
}
|
||||
|
||||
// DockerAuth represents the authentication info for a Docker registry
|
||||
type DockerAuth struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Auth string `json:"auth"`
|
||||
}
|
||||
|
||||
// --- VaultManager to Encapsulate Logic ---
|
||||
|
||||
// VaultManager handles all interactions with Kubernetes and Vault.
|
||||
type VaultManager struct {
|
||||
podName string
|
||||
rootToken string
|
||||
}
|
||||
|
||||
// NewVaultManager creates and initializes a new VaultManager.
|
||||
func NewVaultManager() (*VaultManager, error) {
|
||||
vm := &VaultManager{}
|
||||
|
||||
fmt.Println("[INFO] Checking dependencies (kubectl)...")
|
||||
if !commandExists("kubectl") {
|
||||
return nil, fmt.Errorf("'kubectl' must be installed and in your PATH")
|
||||
}
|
||||
|
||||
fmt.Println("[INFO] Finding Vault pod...")
|
||||
podName, err := vm.getVaultPodName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vm.podName = podName
|
||||
fmt.Printf("[INFO] Using Vault pod: %s\n", vm.podName)
|
||||
|
||||
fmt.Println("[INFO] Retrieving Vault root token...")
|
||||
rootToken, err := vm.getVaultRootToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vm.rootToken = rootToken
|
||||
fmt.Println("[SUCCESS] Found Vault root token.")
|
||||
|
||||
fmt.Println("[INFO] Verifying Vault connection and token...")
|
||||
if err := vm.verifyVaultToken(); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate Vault token: %w", err)
|
||||
}
|
||||
fmt.Println("[SUCCESS] Vault token is valid.")
|
||||
|
||||
return vm, nil
|
||||
}
|
||||
|
||||
// getVaultPodName finds a running Vault pod in the cluster.
|
||||
func (vm *VaultManager) getVaultPodName() (string, error) {
|
||||
// Use `runCommandQuiet` to check for the existence of common pod names.
|
||||
commonNames := []string{"vault-0", "vault-1", "vault-2"}
|
||||
for _, name := range commonNames {
|
||||
if err := runCommandQuiet("kubectl", "get", "pod", name, "-n", vaultNamespace); err == nil {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
// For getting the name via label, we MUST capture the command's output,
|
||||
// so we use exec.Command().Output() directly.
|
||||
cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-o", "jsonpath={.items[0].metadata.name}")
|
||||
output, err := cmd.Output()
|
||||
if err == nil && len(output) > 0 {
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find any running Vault pods in the '%s' namespace", vaultNamespace)
|
||||
}
|
||||
|
||||
// getVaultRootToken retrieves the Vault root token from the Kubernetes secret.
|
||||
func (vm *VaultManager) getVaultRootToken() (string, error) {
|
||||
// We need to capture the secret data from stdout, so we must use exec.Command().Output().
|
||||
// The utility functions in utils.go do not support this.
|
||||
cmd := exec.Command("kubectl", "get", "secret", vaultRootTokenSecretName, "-n", vaultNamespace, "-o", fmt.Sprintf("jsonpath={.data.%s}", vaultRootTokenSecretKey))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get '%s' secret from '%s' namespace: %w", vaultRootTokenSecretName, vaultNamespace, err)
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return "", fmt.Errorf("key '%s' not found in secret '%s'", vaultRootTokenSecretKey, vaultRootTokenSecretName)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(string(output))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode base64 root token: %w", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(decoded)), nil
|
||||
}
|
||||
|
||||
// runVaultCommandQuiet executes a command inside the Vault pod without printing its output.
|
||||
func (vm *VaultManager) runVaultCommandQuiet(command string) error {
|
||||
envSetup := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='%s' && export VAULT_TOKEN='%s'", defaultVaultAddress, vm.rootToken)
|
||||
fullCmd := fmt.Sprintf("%s && %s", envSetup, command)
|
||||
|
||||
// Use the runCommandQuiet utility to suppress command stdout.
|
||||
return runCommandQuiet("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd)
|
||||
}
|
||||
|
||||
// verifyVaultToken checks if the root token is valid by running a quiet command.
|
||||
func (vm *VaultManager) verifyVaultToken() error {
|
||||
return vm.runVaultCommandQuiet("vault token lookup")
|
||||
}
|
||||
|
||||
// StoreSecret stores a key-value secret at the specified path in Vault quietly.
|
||||
func (vm *VaultManager) StoreSecret(path string, data map[string]string) error {
|
||||
fmt.Printf("[INFO] Storing secret in Vault at path: %s...\n", path)
|
||||
|
||||
var kvPairs []string
|
||||
for key, value := range data {
|
||||
// Escape single quotes for shell command robustness.
|
||||
escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'")
|
||||
kvPairs = append(kvPairs, fmt.Sprintf("%s='%s'", key, escapedValue))
|
||||
}
|
||||
|
||||
vaultCmd := fmt.Sprintf("vault kv put %s %s", path, strings.Join(kvPairs, " "))
|
||||
if err := vm.runVaultCommandQuiet(vaultCmd); err != nil {
|
||||
return fmt.Errorf("failed to store secret in Vault: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("[SUCCESS] Secret at '%s' stored successfully in Vault!\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Main Workflow Functions ---
|
||||
func createDockerHubSecretWorkflow() error {
|
||||
vm, err := NewVaultManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Vault manager: %w", err)
|
||||
}
|
||||
|
||||
return createDockerHubSecret(vm)
|
||||
}
|
||||
|
||||
func createGhcrSecretWorkflow() error {
|
||||
vm, err := NewVaultManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Vault manager: %w", err)
|
||||
}
|
||||
|
||||
return createGhcrSecrets(vm)
|
||||
}
|
||||
|
||||
func createGhcrSecrets(vm *VaultManager) error {
|
||||
fmt.Println("\n[INFO] Configuring secret for GitHub Container Registry (GHCR)...")
|
||||
username, registryToken, orgToken, err := getGHCRUserInput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerConfigJSON, err := createDockerConfig(ghcrRegistry, username, registryToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Docker config: %w", err)
|
||||
}
|
||||
dockerSecretData := map[string]string{dockerConfigJSONKey: dockerConfigJSON}
|
||||
if err := vm.StoreSecret(ghcrSecretPath, dockerSecretData); err != nil {
|
||||
return fmt.Errorf("failed to store Docker registry secret: %w", err)
|
||||
}
|
||||
|
||||
orgSecretData := map[string]string{orgManagementPATKey: orgToken}
|
||||
if err := vm.StoreSecret(orgManagementSecretPath, orgSecretData); err != nil {
|
||||
return fmt.Errorf("failed to store organization management PAT: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDockerHubSecret(vm *VaultManager) error {
|
||||
fmt.Println("\n[INFO] Configuring secret for Docker Hub...")
|
||||
username, password, err := getUserInputForDockerHub()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerConfigJSON, err := createDockerConfig(dockerHubRegistry, username, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Docker config: %w", err)
|
||||
}
|
||||
dockerSecretData := map[string]string{dockerConfigJSONKey: dockerConfigJSON}
|
||||
|
||||
if err := vm.StoreSecret(dockerHubSecretPath, dockerSecretData); err != nil {
|
||||
return fmt.Errorf("failed to store Docker Hub secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAndStoreGithubAuthSecret() error {
|
||||
username, token, err := getUserInputForGithubAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vm, err := NewVaultManager()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Vault manager: %w", err)
|
||||
}
|
||||
|
||||
secretData := map[string]string{
|
||||
"username": username,
|
||||
"password": token,
|
||||
}
|
||||
if err := vm.StoreSecret(githubAuthSecretPath, secretData); err != nil {
|
||||
return fmt.Errorf("failed to store github-auth secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- User Input and Helper Functions ---
|
||||
func getGHCRUserInput() (username, registryToken, orgToken string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Enter your GitHub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
err = fmt.Errorf("username cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
registryToken, err = promptForToken(reader, "Enter your GitHub personal access token for container registry")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
orgToken, err = promptForToken(reader, "Enter your GitHub personal access token for organization management")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getUserInputForDockerHub() (username, password string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Enter your Docker Hub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Print("Enter your Docker Hub password or access token: ")
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
password = string(passwordBytes)
|
||||
|
||||
if username == "" || password == "" {
|
||||
err = fmt.Errorf("username and password cannot be empty")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getUserInputForGithubAuth() (username, token string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Enter your GitHub username: ")
|
||||
username, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
err = fmt.Errorf("username cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
token, err = promptForToken(reader, "Enter your GitHub personal access token for repository access")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// promptForToken provides a generic way to ask for a token with validation.
|
||||
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
|
||||
fmt.Printf("%s: ", prompt)
|
||||
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := string(tokenBytes)
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
|
||||
validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
|
||||
hasValidPrefix := false
|
||||
for _, prefix := range validPrefixes {
|
||||
if strings.HasPrefix(token, prefix) {
|
||||
hasValidPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasValidPrefix {
|
||||
fmt.Printf("[WARNING] The token does not appear to be a valid GitHub personal access token (should start with one of %v)\n", validPrefixes)
|
||||
fmt.Print("Do you want to continue anyway? (y/N): ")
|
||||
confirm, _ := reader.ReadString('\n')
|
||||
if strings.TrimSpace(strings.ToLower(confirm)) != "y" {
|
||||
return "", fmt.Errorf("operation cancelled by user")
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// createDockerConfig creates a Docker config JSON string.
|
||||
func createDockerConfig(registry, username, token string) (string, error) {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
|
||||
config := DockerConfig{
|
||||
Auths: map[string]DockerAuth{
|
||||
registry: {
|
||||
Username: username,
|
||||
Password: token,
|
||||
Auth: auth,
|
||||
},
|
||||
},
|
||||
}
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal docker config: %w", err)
|
||||
}
|
||||
return string(configJSON), nil
|
||||
}
|
||||
Loading…
Reference in a new issue