From 11b1f1b637e44c323a4f1a299132ebfce975f604 Mon Sep 17 00:00:00 2001 From: jon brookes Date: Sat, 16 Aug 2025 18:00:28 +0100 Subject: [PATCH] update: Added Longhorn installation process and updated memory allocation for VMs update: Added 'git' and 'vagrant' to required tools in pre-flight checks fix: configured k3s install to use internal nic for flanel network update: Added Longhorn installation process and updated memory allocation for VMs update: Added 'git' and 'vagrant' to required tools in pre-flight checks fix: configured k3s install to use internal nic for flanel network fix: corrected JSON formatting for config json update: reduce VM memory allocation to 2GB, add Longhorn installation scripts and prerequisites, and implement checks for existing pods fix: merge issues fix: merge issues update: Added Longhorn installation process and updated memory allocation for VMs update: Added 'git' and 'vagrant' to required tools in pre-flight checks fix: configured k3s install to use internal nic for flanel network update: Added Longhorn installation process and updated memory allocation for VMs update: Added 'git' and 'vagrant' to required tools in pre-flight checks fix: configured k3s install to use internal nic for flanel network fix: corrected JSON formatting for config json update: reduce VM memory allocation to 2GB, add Longhorn installation scripts and prerequisites, and implement checks for existing pods update: improve error logging in RunJsonDeployment and RunCommand functions update: add jq installation to provision script update: add version flag bump version fix: improve error messages for config file reading feat: add Windows gitbash installation support and improve binary download process clean up tmp code fix: increase timeout for some slower windows clients feat: add Ingress and Service configurations for nginx deployment, and implement MetalLB and Traeik installation scripts refactor: remove obsolete Traefik installation script feat: add environment checks and configurations for Vagrant setup, including dnsmasq MetalLB and ingress feat: add deployment and installation scripts for infmon-cli, including Kubernetes configurations feat: refactor customer project creation and add success/failure job scripts refactor: rename customer references to project in configuration and application logic feat: enhance JSON deployment handling with retry logic and command execution improvements feat: enhance RunJsonDeployment with error handling and retry logic; add tests for configuration reading feat: add automatic creation of base and config JSON files from examples if they do not exist refactor: remove database package and related functionality; update app state initialization and error handling refactor: update deployment handling to use ProjectConfig; improve error messages and logging feat: enhance RunJsonDeployment retry logic with configurable delay; improve logging for retries feat: implement LoadConfigs function for improved configuration loading; add logger setup refactor: remove unused fields from BaseConfig and ProjectConfig structs for cleaner configuration management refactor: clean up tests by removing obsolete functions and simplifying test cases chore: update version to v0.0.5 in install script feat: implement default configuration creation for BaseConfig and ProjectConfig; enhance validation logic fix: enhance configuration parsing and loading; streamline flag handling and error reporting refactor: remove obsolete configuration download logic from installation script --- .envrc.example | 7 +- .gitignore | 2 + README.md | 12 +- app/app.go | 218 +++++++++--------- app/app_test.go | 138 ++--------- app/k8s.go | 91 ++++---- base.json.example | 17 -- build.sh | 6 + config.json.example | 19 -- config/base.go | 120 +++++++--- config/base_test.go | 50 ++++ config/customer.go | 29 --- config/project.go | 108 +++++++++ config/project_test.go | 46 ++++ database/database.go | 74 ------ go.mod | 15 -- go.sum | 47 ---- install.sh | 25 +- logger/logger.go | 119 ++++++++++ logger/logger_test.go | 42 ++++ main.go | 106 +++------ main_test.go | 10 + pipelines/dev/carry_on.json | 20 ++ pipelines/dev/do_not_carry_on.json | 20 ++ pipelines/dev/failing.json | 29 +++ pipelines/dev/multiple_pipelines.json | 38 +++ pipelines/dev/retry.json | 11 + pipelines/dev/succeeding.json | 11 + pipelines/dev/vagrant-k3s.json | 15 +- scripts/configure_vagrant_k3s.sh | 2 - scripts/dev-pre-flight-checks.sh | 2 +- scripts/envrc_checks.sh | 22 ++ scripts/failue.sh | 21 ++ scripts/install_traefik.sh | 135 ----------- scripts/install_vagrant_workstation.sh | 2 + scripts/success.sh | 21 ++ vagrant/dev/ubuntu/.envrc.example | 1 - vagrant/dev/ubuntu/Vagrantfile | 10 +- .../dev/ubuntu/ansible/install_dnsmasq.yaml | 78 +++++++ .../dev/ubuntu/ansible/install_k3s_3node.yaml | 4 +- .../ansible/install_longhorn_prereqs.yaml | 47 ++++ .../ubuntu/ansible/provision_workstation.sh | 53 ++++- vagrant/dev/ubuntu/ansible/vars.yaml | 2 + .../dev/ubuntu/k8s/nginx-test/deployment.yaml | 35 +++ .../k8s/nginx-test/deployment_infmon.yaml | 34 +++ .../dev/ubuntu/k8s/nginx-test/ingress.yaml | 27 +++ vagrant/dev/ubuntu/k8s/nginx-test/install.sh | 7 + vagrant/dev/ubuntu/k8s/nginx-test/pvc.yaml | 12 + .../dev/ubuntu/k8s/nginx-test/service.yaml | 12 + vagrant/dev/ubuntu/k8s/traefik-tlsstore.yaml | 8 + .../dev/ubuntu/pipelines/vagrant-ingress.json | 20 ++ .../ubuntu/pipelines/vagrant-longhorn.json | 29 +++ .../dev/ubuntu/pipelines/vagrant-metallb.json | 11 + .../ubuntu/scripts/check_install_infctl.sh | 21 ++ .../dev/ubuntu/scripts/helm_check_install.sh | 15 ++ .../dev/ubuntu/scripts/install_longhorn.sh | 22 ++ vagrant/dev/ubuntu/scripts/install_metallb.sh | 65 ++++++ vagrant/dev/ubuntu/scripts/install_traefik.sh | 68 ++++++ .../scripts/install_vagrant_longhorn.sh | 54 +++++ .../dev/ubuntu/scripts/longhorn_prereqs.sh | 32 +++ .../dev/ubuntu/scripts/wait_for_longhorn.sh | 17 ++ 61 files changed, 1573 insertions(+), 761 deletions(-) delete mode 100644 base.json.example delete mode 100644 config.json.example create mode 100644 config/base_test.go delete mode 100644 config/customer.go create mode 100644 config/project.go create mode 100644 config/project_test.go delete mode 100644 database/database.go create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go create mode 100644 main_test.go create mode 100644 pipelines/dev/carry_on.json create mode 100644 pipelines/dev/do_not_carry_on.json create mode 100644 pipelines/dev/failing.json create mode 100644 pipelines/dev/multiple_pipelines.json create mode 100644 pipelines/dev/retry.json create mode 100644 pipelines/dev/succeeding.json create mode 100755 scripts/envrc_checks.sh create mode 100755 scripts/failue.sh delete mode 100755 scripts/install_traefik.sh create mode 100755 scripts/success.sh delete mode 100644 vagrant/dev/ubuntu/.envrc.example create mode 100644 vagrant/dev/ubuntu/ansible/install_dnsmasq.yaml create mode 100644 vagrant/dev/ubuntu/ansible/install_longhorn_prereqs.yaml create mode 100644 vagrant/dev/ubuntu/k8s/nginx-test/deployment.yaml create mode 100644 vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml create mode 100644 vagrant/dev/ubuntu/k8s/nginx-test/ingress.yaml create mode 100755 vagrant/dev/ubuntu/k8s/nginx-test/install.sh create mode 100644 vagrant/dev/ubuntu/k8s/nginx-test/pvc.yaml create mode 100644 vagrant/dev/ubuntu/k8s/nginx-test/service.yaml create mode 100644 vagrant/dev/ubuntu/k8s/traefik-tlsstore.yaml create mode 100644 vagrant/dev/ubuntu/pipelines/vagrant-ingress.json create mode 100644 vagrant/dev/ubuntu/pipelines/vagrant-longhorn.json create mode 100644 vagrant/dev/ubuntu/pipelines/vagrant-metallb.json create mode 100755 vagrant/dev/ubuntu/scripts/check_install_infctl.sh create mode 100755 vagrant/dev/ubuntu/scripts/helm_check_install.sh create mode 100755 vagrant/dev/ubuntu/scripts/install_longhorn.sh create mode 100755 vagrant/dev/ubuntu/scripts/install_metallb.sh create mode 100755 vagrant/dev/ubuntu/scripts/install_traefik.sh create mode 100755 vagrant/dev/ubuntu/scripts/install_vagrant_longhorn.sh create mode 100755 vagrant/dev/ubuntu/scripts/longhorn_prereqs.sh create mode 100755 vagrant/dev/ubuntu/scripts/wait_for_longhorn.sh diff --git a/.envrc.example b/.envrc.example index d9b1196..b10066d 100644 --- a/.envrc.example +++ b/.envrc.example @@ -1,10 +1,9 @@ -export VAGRANT_BRIDGE='Intel(R) Ethernet Connection (16) I219-V' - -# Network configuration for Vagrant/Ansible - +# export VAGRANT_BRIDGE='Intel(R) Ethernet Connection (16) I219-V' export WORKSTATION_IP="192.168.56.10" export VM1_IP="192.168.56.80" export VM2_IP="192.168.56.81" export VM3_IP="192.168.56.82" export VAGRANT_NETWORK_PREFIX="192.168.56" export K3S_URL_IP="192.168.56.250" +export METALLB_IP_RANGE="192.168.56.230-192.168.56.240" + diff --git a/.gitignore b/.gitignore index 8a9e3d1..644c688 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ scripts/ansible_inventory.ini scripts/ansible_inventory.ini vagrant/dev/ubuntu/ansible/ansible_inventory.ini *.cast +vagrant/dev/ubuntu/certs/ +vagrant/dev/ubuntu/config-dev diff --git a/README.md b/README.md index 1b0743c..0a450ac 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,9 @@ Key configuration options: - `env`: Environment file path - `preview_path`: Path for preview functionality -### Customer Configuration (`config.json`) +### Project Configuration (`config.json`) -Copy and customize the customer-specific configuration: +Copy and customize the project-specific configuration: ```bash cp config.json.example config.json @@ -155,7 +155,7 @@ cp config.json.example config.json Key configuration options: - `project`: Project name/identifier (used as Kubernetes namespace) -- `customer_directory`: Customer-specific directory +- `project_directory`: Project-specific directory - `ui_url`: UI service URL - `static_url`: Static content URL - `port`: Service port @@ -186,7 +186,7 @@ Run the CLI by providing a path to your pipeline JSON file: The tool will automatically: -1. Load base and customer configurations +1. Load base and project configurations 2. Initialize SQLite database for state management 3. Execute the deployment pipeline defined in your JSON file 4. Run scripts from the `scripts/` directory @@ -292,13 +292,13 @@ infctl-cli/ ├── main.go # Application entry point ├── go.mod # Go module definition ├── base.json.example # Base configuration template -├── config.json.example # Customer configuration template +├── config.json.example # Project configuration template ├── app/ # Core application logic │ ├── app.go # Pipeline orchestration and state management │ └── k8s.go # Kubernetes operations (kubectl, kustomize) ├── config/ # Configuration management │ ├── base.go # Base configuration handling -│ └── customer.go # Customer configuration handling +│ └── project.go # Project configuration handling ├── database/ # SQLite database operations ├── scripts/ # Shell scripts executed by the CLI │ ├── install_*.sh # Infrastructure installation scripts diff --git a/app/app.go b/app/app.go index 1d51443..e1c6bab 100644 --- a/app/app.go +++ b/app/app.go @@ -1,22 +1,21 @@ package app import ( - "database/sql" + "bufio" + "bytes" "encoding/json" "fmt" - "log" "log/slog" "os" + "os/exec" "time" "headshed/infctl-cli/config" - "headshed/infctl-cli/database" ) type AppState struct { - Config config.BaseConfig - Customer config.CustomerConfig - DB *sql.DB + Config config.BaseConfig + Project config.ProjectConfig } type PipelineStep struct { @@ -62,27 +61,31 @@ func (app *AppState) ToDoDeployment() []PipelineStep { return []PipelineStep{} } -func (app *AppState) RunJsonDeployment() []PipelineStep { +// func (app *AppState) RunJsonDeployment() []PipelineStep { +func (app *AppState) RunJsonDeployment() error { - jsonFile := app.Config.DeploymentFile + jsonFile := app.Project.DeploymentFile if jsonFile == "" { - log.Fatal("no config specified with --deployment-file=") + return fmt.Errorf("no config specified with [-f|-deployment-file]= => for all options see help with -h") } file, err := os.Open(jsonFile) if err != nil { slog.Error(fmt.Sprintf("Failed to open JSON file: %s", err)) + return fmt.Errorf("failed to open JSON file: %w", err) } - defer file.Close() + // fmt.Printf("jsonFile is : %s\n", jsonFile) + slog.Info(fmt.Sprintf("Using jsonFile: %s", jsonFile)) steps, err := parseStepsFromJSON(jsonFile) if err != nil { slog.Error(fmt.Sprintf("Failed to parse JSON file: %s", err)) + return fmt.Errorf("failed to parse JSON file: %w", err) } for _, step := range steps { - slog.Info(fmt.Sprintf("🔄 Running step: %s", step.Name)) + slog.Info(fmt.Sprintf("run json deployment => 🔄 %s", step.Name)) function, exists := functionMap[step.Function] if !exists { slog.Error(fmt.Sprintf("Unknown function: %s", step.Function)) @@ -90,136 +93,121 @@ func (app *AppState) RunJsonDeployment() []PipelineStep { } err := function(step.Params) + if err != nil { - slog.Error(fmt.Sprintf("❌ Step failed: %s, error: %v", step.Name, err)) + var innerErr error + if step.RetryCount > 0 { + for i := 0; i < step.RetryCount; i++ { + + sleep := app.Config.RetryDelaySenconds + + slog.Info(fmt.Sprintf("Retrying step: %s (attempt %d/%d) after waiting for %d seconds...", step.Name, i+1, step.RetryCount, sleep)) + time.Sleep(time.Duration(sleep) * time.Second) + + if innerErr = function(step.Params); innerErr == nil { + slog.Info(fmt.Sprintf("✅ Step completed: %s\n", step.Name)) + err = nil + break + } + } + if innerErr != nil { + if !step.ShouldAbort { + slog.Info(fmt.Sprintf("Not going to abort, step: %s", step.Name)) + } else { + return fmt.Errorf("critical failure at step: %s", step.Name) + } + } + } if step.ShouldAbort { - log.Fatalf("🚨Critical failure at step: %s", step.Name) + return fmt.Errorf("critical failure at step: %s", step.Name) } } else { slog.Info(fmt.Sprintf("✅ Step completed: %s", step.Name)) } } - return steps + return nil } -func (app *AppState) getPipeline() []PipelineStep { - - switch app.Config.DeploymentType { - +func (app *AppState) getPipeline() error { + switch app.Project.DeploymentMode { case "api": - return app.ToDoDeployment() - + return fmt.Errorf("api mode is not yet implemented") case "json": return app.RunJsonDeployment() - default: - return app.RunJsonDeployment() + // return app.RunJsonDeployment() + return fmt.Errorf("unknown mode: %s", app.Project.DeploymentMode) } } -func NewAppState(cust config.CustomerConfig, config config.BaseConfig, dbPath string) (*AppState, error) { - db, err := database.NewDatabase(dbPath) - if err != nil { - return nil, err - } - +func NewAppState(cust config.ProjectConfig, config config.BaseConfig) (*AppState, error) { return &AppState{ - Config: config, - Customer: cust, - DB: db, + Config: config, + Project: cust, }, nil } -func (app *AppState) runPipeline(steps []PipelineStep) error { - for _, step := range steps { - slog.Info(fmt.Sprintf("🔄 Running step: %s\n", step.Name)) +func RunCommand(command string) error { + slog.Debug(fmt.Sprintf("🐞 Running command: %s", command)) + cmd := exec.Command("sh", "-c", command) - // Look up the function in the functionMap - function, exists := functionMap[step.Function] - if !exists { - slog.Error(fmt.Sprintf("❌ Unknown function: %s", step.Function)) - if step.ShouldAbort { - return fmt.Errorf("🚨critical failure: unknown function %s", step.Function) - } - continue - } + var stdout, stderr bytes.Buffer - // Execute the function with the provided parameters - err := function(step.Params) - if err != nil { - slog.Error(fmt.Sprintf("❌ Step failed: %s, error: %v", step.Name, err)) - - // Retry logic - if step.RetryCount > 0 { - for i := 0; i < step.RetryCount; i++ { - slog.Info("Waiting for 20 seconds before retrying...") - time.Sleep(20 * time.Second) - if innerErr := function(step.Params); innerErr == nil { - slog.Info(fmt.Sprintf("✅ Step completed: %s\n", step.Name)) - err = nil - break - } else { - err = innerErr - } - } - } - - // Handle failure after retries - if err != nil { - if step.ShouldAbort { - return fmt.Errorf("🚨critical failure at step: %s", step.Name) - } - continue - } - } - - slog.Info(fmt.Sprintf("✅ Step completed: %s\n", step.Name)) - } - return nil -} - -func (app *AppState) SetUpNewCustomer() error { - - /* - | -------------------------- - | main pipeline - | -------------------------- - */ - - steps := app.getPipeline() - app.runPipeline(steps) - slog.Info(fmt.Sprintln("🎉 Pipeline setup complete!")) - - return nil -} - -func (app *AppState) CreatePipeline() error { - isNew, err := database.CheckProjectName(app.DB, app.Customer.Project) + // Get pipes for real-time reading + stdoutPipe, err := cmd.StdoutPipe() if err != nil { - return fmt.Errorf("failed to check project name: %w", err) + return fmt.Errorf("failed to create stdout pipe: %w", err) } - - if isNew { - - port, err := database.GetNextPortNumber(app.DB) - if err != nil { - return fmt.Errorf("failed to get next port number: %w", err) - } - err = database.AddProjectName(app.DB, app.Customer.Project, port) - if err != nil { - return fmt.Errorf("failed to add project name: %w", err) - } - slog.Info(fmt.Sprintln("Project name added:", app.Customer.Project)) - fmt.Printf("Port number assigned: %d\n", port) - app.Config.Port = port - } else { - slog.Info(fmt.Sprintln("Project name already exists:", app.Customer.Project)) - } - - err = app.SetUpNewCustomer() + stderrPipe, err := cmd.StderrPipe() if err != nil { - return fmt.Errorf("failed to set up new customer: %w", err) + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Start the command + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start command: %w", err) + } + + // Read stdout line by line and log through slog + go func() { + scanner := bufio.NewScanner(stdoutPipe) + for scanner.Scan() { + line := scanner.Text() + stdout.WriteString(line + "\n") + slog.Info(line) + } + }() + + // Read stderr line by line and log through slog + go func() { + scanner := bufio.NewScanner(stderrPipe) + for scanner.Scan() { + line := scanner.Text() + stderr.WriteString(line + "\n") + slog.Info(line) + } + }() + + // Wait for command to complete + err = cmd.Wait() + if err != nil { + slog.Error(fmt.Sprintf("❌ Command failed with error: %v", err)) + slog.Debug(fmt.Sprintf("🐞 Stdout: %s\n", stdout.String())) + slog.Debug(fmt.Sprintf("🐞 Stderr: %s\n", stderr.String())) + return fmt.Errorf("failed to run script command: %w", err) } return nil } + +func (app *AppState) SetUpNewProject() error { + return app.getPipeline() +} + +func (app *AppState) CreateProjectAndRunPipeline() error { + err := app.SetUpNewProject() + if err != nil { + return fmt.Errorf("Pipeline error: %w", err) + } + return nil +} diff --git a/app/app_test.go b/app/app_test.go index f6aa5f5..cd3db8f 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -2,129 +2,31 @@ package app import ( "encoding/json" - "math/rand" + "headshed/infctl-cli/config" "os" - "os/exec" "path/filepath" "testing" - - "headshed/infctl-cli/config" ) -func TestMain(m *testing.M) { - // Setup: Set TEST_ENV=true for all tests - err := os.Setenv("TEST_ENV", "true") - if err != nil { - panic("Failed to set TEST_ENV") - } - - // Run all tests - code := m.Run() - - // Teardown: Unset TEST_ENV after all tests - os.Unsetenv("TEST_ENV") - - // Exit with the test result code - os.Exit(code) -} +// Test only pipeline execution and shell command running func TestRunPipeline(t *testing.T) { - // Create a temporary directory for test assets tempDir, err := os.MkdirTemp("", "smoke-test") if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } - defer os.RemoveAll(tempDir) // Cleanup after test - - // Create test scripts - scripts := map[string]string{ - "good.sh": "#!/bin/bash\necho 'Good script executed'\nexit 0", - "warning.sh": "#!/bin/bash\necho 'Warning script executed'\nexit 0", - "error.sh": "#!/bin/bash\necho 'Error script executed'\nexit 1", - } - - for name, content := range scripts { - scriptPath := filepath.Join(tempDir, name) - if err := os.WriteFile(scriptPath, []byte(content), 0755); err != nil { - t.Fatalf("Failed to create script %s: %v", name, err) - } - } - - // Create a test JSON pipeline file - pipeline := []PipelineStep{ - {Name: "Good Step", Function: "RunCommand", Params: []string{filepath.Join(tempDir, "good.sh")}, RetryCount: 0, ShouldAbort: false}, - {Name: "Warning Step", Function: "RunCommand", Params: []string{filepath.Join(tempDir, "warning.sh")}, RetryCount: 0, ShouldAbort: false}, - {Name: "Error Step", Function: "RunCommand", Params: []string{filepath.Join(tempDir, "error.sh")}, RetryCount: 0, ShouldAbort: true}, - } - - pipelineFile := filepath.Join(tempDir, "pipeline.json") - pipelineData, err := json.Marshal(pipeline) - if err != nil { - t.Fatalf("Failed to marshal pipeline: %v", err) - } - if err := os.WriteFile(pipelineFile, pipelineData, 0644); err != nil { - t.Fatalf("Failed to write pipeline file: %v", err) - } - - // Set up AppState - app := &AppState{ - Config: config.BaseConfig{ - DeploymentFile: pipelineFile, - }, - } - - // Run the pipeline - err = app.runPipeline(pipeline) - if err == nil { - t.Errorf("Expected error due to 'Error Step', but got none") - } -} - -func randomString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) -} - -func TestK3DNamespaceCreation(t *testing.T) { - // Check if k3d is installed - _, err := exec.LookPath("k3d") - if err != nil { - t.Fatal("k3d is not installed. Please install k3d to run this test.") - } - - // Create a test cluster - clusterName := "test-" + randomString(6) - - cmd := exec.Command("k3d", "cluster", "create", clusterName, "--servers", "1") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - t.Fatalf("Failed to create k3d cluster: %v", err) - } - defer func() { - // Clean up the test cluster - cmd := exec.Command("k3d", "cluster", "delete", clusterName) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - t.Errorf("Failed to delete k3d cluster: %v", err) - } - }() - - // Create a temporary directory for the pipeline config - tempDir, err := os.MkdirTemp("", "k3d-test") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } defer os.RemoveAll(tempDir) + // Create a test script + scriptPath := filepath.Join(tempDir, "good.sh") + scriptContent := "#!/bin/bash\necho 'Good script executed'\nexit 0" + if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { + t.Fatalf("Failed to create script: %v", err) + } + // Create a test JSON pipeline file pipeline := []PipelineStep{ - {Name: "Ensure Namespace Exists", Function: "k8sNamespaceExists", Params: []string{"test-namespace"}, RetryCount: 0, ShouldAbort: true}, + {Name: "Good Step", Function: "RunCommand", Params: []string{scriptPath}, RetryCount: 0, ShouldAbort: false}, } pipelineFile := filepath.Join(tempDir, "pipeline.json") pipelineData, err := json.Marshal(pipeline) @@ -137,22 +39,20 @@ func TestK3DNamespaceCreation(t *testing.T) { // Set up AppState app := &AppState{ - Config: config.BaseConfig{ + Config: config.BaseConfig{}, + Project: config.ProjectConfig{ DeploymentFile: pipelineFile, }, } // Run the pipeline - err = app.runPipeline(pipeline) + err = app.RunJsonDeployment() if err != nil { - t.Fatalf("Pipeline execution failed: %v", err) - } - - // Verify the namespace exists - cmd = exec.Command("kubectl", "get", "ns", "test-namespace") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - t.Fatalf("Namespace 'test-namespace' was not created: %v", err) + t.Errorf("Expected no error, got: %v", err) } } + +// Removed randomString: not needed for current tests + +// Removed TestK3DNamespaceCreation: k3d and k8s namespace logic is no longer part of the app +// Removed TestSetUpNewProject: advanced project setup logic is no longer part of the app diff --git a/app/k8s.go b/app/k8s.go index 86f7a70..f183eeb 100644 --- a/app/k8s.go +++ b/app/k8s.go @@ -1,7 +1,6 @@ package app import ( - "bufio" "bytes" "fmt" "log/slog" @@ -65,55 +64,55 @@ func k8sCreateNamespace(project string) error { return nil } -func RunCommand(command string) error { - slog.Debug(fmt.Sprintf("🐞 Running command: %s", command)) - cmd := exec.Command("sh", "-c", command) +// func RunCommand(command string) error { +// slog.Debug(fmt.Sprintf("🐞 Running command: %s", command)) +// cmd := exec.Command("sh", "-c", command) - var stdout, stderr bytes.Buffer +// var stdout, stderr bytes.Buffer - // Get pipes for real-time reading - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("failed to create stdout pipe: %w", err) - } - stderrPipe, err := cmd.StderrPipe() - if err != nil { - return fmt.Errorf("failed to create stderr pipe: %w", err) - } +// // Get pipes for real-time reading +// stdoutPipe, err := cmd.StdoutPipe() +// if err != nil { +// return fmt.Errorf("failed to create stdout pipe: %w", err) +// } +// stderrPipe, err := cmd.StderrPipe() +// if err != nil { +// return fmt.Errorf("failed to create stderr pipe: %w", err) +// } - // Start the command - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start command: %w", err) - } +// // Start the command +// if err := cmd.Start(); err != nil { +// return fmt.Errorf("failed to start command: %w", err) +// } - // Read stdout line by line and log through slog - go func() { - scanner := bufio.NewScanner(stdoutPipe) - for scanner.Scan() { - line := scanner.Text() - stdout.WriteString(line + "\n") - slog.Info(line) - } - }() +// // Read stdout line by line and log through slog +// go func() { +// scanner := bufio.NewScanner(stdoutPipe) +// for scanner.Scan() { +// line := scanner.Text() +// stdout.WriteString(line + "\n") +// slog.Info(line) +// } +// }() - // Read stderr line by line and log through slog - go func() { - scanner := bufio.NewScanner(stderrPipe) - for scanner.Scan() { - line := scanner.Text() - stderr.WriteString(line + "\n") - slog.Error(line) - } - }() +// // Read stderr line by line and log through slog +// go func() { +// scanner := bufio.NewScanner(stderrPipe) +// for scanner.Scan() { +// line := scanner.Text() +// stderr.WriteString(line + "\n") +// slog.Info(line) +// } +// }() - // Wait for command to complete - err = cmd.Wait() - if err != nil { - slog.Error(fmt.Sprintf("❌ Command failed with error: %v\n", err)) - slog.Debug(fmt.Sprintf("🐞 Stdout: %s\n", stdout.String())) - slog.Debug(fmt.Sprintf("🐞 Stderr: %s\n", stderr.String())) - return fmt.Errorf("failed to run script command: %w", err) - } +// // Wait for command to complete +// err = cmd.Wait() +// if err != nil { +// slog.Error(fmt.Sprintf("❌ Command failed with error: %v\n", err)) +// slog.Debug(fmt.Sprintf("🐞 Stdout: %s\n", stdout.String())) +// slog.Debug(fmt.Sprintf("🐞 Stderr: %s\n", stderr.String())) +// return fmt.Errorf("failed to run script command: %w", err) +// } - return nil -} +// return nil +// } diff --git a/base.json.example b/base.json.example deleted file mode 100644 index 1d21d66..0000000 --- a/base.json.example +++ /dev/null @@ -1,17 +0,0 @@ -{ - "projects_directory": "/home/user/docker/", - "app_image": "headshead/some-app:0.0.1", - "webserver_image": "headsheddev/some-webserver_image:0.0.1", - "db": "database/database.sqlite", - "env": ".env", - "preview_path": "prv", - "data_www": "data_www", - "static_images": "data_images/", - "public_images": "images", - "php_conf": "php/local.ini", - "exports": "exports", - "logs": "logs/laravel.log", - "admin_url": ".headshed.dev", - "preview_url": "-prv.headshed.dev", - "nginx_conf": "nginx/conf.d" -} \ No newline at end of file diff --git a/build.sh b/build.sh index 412e8f0..683271c 100755 --- a/build.sh +++ b/build.sh @@ -5,6 +5,12 @@ mkdir -p bin echo "Building for Linux..." GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/infctl-linux-amd64 +echo "Building for Raspberry Pi (Linux ARM)..." +GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -o bin/infctl-linux-armv7 + +echo "Building for Raspberry Pi (Linux ARM64)..." +GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bin/infctl-linux-arm64 + echo "Building for Windows..." GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/infctl-windows-amd64.exe diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 30f9cc6..0000000 --- a/config.json.example +++ /dev/null @@ -1,19 +0,0 @@ -{ - "project": "hdshd", - "project_data": "path_to/project_data", - "app_image": "headsheddev/some-app:0.0.1", - "env": "path_to/.env", - "images": "path_to_images", - "php_conf": "path_to/local.ini", - "exports": "path_to/exports", - "db": "path_to/db", - "logs": "path_to/logs", - "preview_path": "path_to_preview", - "webserver_image": "headsheddev/my-nginx:0.0.1", - "public_images": "path_to/images", - "data_www": "path_to/www", - "nginx_conf": "path_to/conf.d", - "admin_url": "admin_url.headshed.dev", - "preview_url": "app-prv.headshed.dev", - "ui_url": "ww2.headshed.dev", -} \ No newline at end of file diff --git a/config/base.go b/config/base.go index acbd4d2..2a2977c 100644 --- a/config/base.go +++ b/config/base.go @@ -7,37 +7,30 @@ import ( "os" ) +const Version = "v0.0.5" + +// Package-level variables for flags +var ( + baseConfigFile string + projectConfigFile string + pipelineFile string +) + type BaseConfig struct { - ProjectsDirectory string `json:"projects_directory"` - Env string `json:"env"` - StaticImages string `json:"static_images"` - PublicImages string `json:"public_images"` - PhpConf string `json:"php_conf"` - Exports string `json:"exports"` - Logs string `json:"logs"` - PreviewPath string `json:"preview_path"` - DataWww string `json:"data_www"` - NginxConf string `json:"nginx_conf"` - AdminURL string `json:"admin_url"` - PreviewURL string `json:"preview_url"` - AppImage string `json:"app_image"` - WebserverImage string `json:"webserver_image"` - EmptyDB string `json:"empty_db"` - DB string `json:"db"` - EmptyImages string `json:"empty_imaages"` - DeploymentType string `json:"deployment_type"` - DeploymentFile string `json:"deployment_file"` - Port int `json:"port"` + RetryDelaySenconds int `json:"retry_delay_seconds"` } -func ReadBaseConfig(path string) (BaseConfig, error) { - - deploymentType := os.Getenv("DEPLOYMENT_TYPE") - - deploymentFile := flag.String("deployment-file", "", "path to config file") - deploymentFileShorthand := flag.String("f", "", "shorthand for -deployment-file") - +// ParseFlags parses all command-line flags and handles help/version flags +func ParseFlags() { helpFlag := flag.Bool("help", false, "show help") + versionFlag := flag.Bool("version", false, "show version") + vFlag := flag.Bool("v", false, "show version (shorthand)") + + flag.StringVar(&baseConfigFile, "base-config", "", "Path to base config file (optional)") + flag.StringVar(&projectConfigFile, "project-config", "", "Path to project config file (optional)") + flag.StringVar(&pipelineFile, "f", "", "Path to pipeline file") + flag.StringVar(&pipelineFile, "deployment-file", "", "Path to pipeline file (long format)") + flag.Parse() if *helpFlag { @@ -46,22 +39,79 @@ func ReadBaseConfig(path string) (BaseConfig, error) { os.Exit(0) } - var config BaseConfig - if *deploymentFileShorthand != "" { - config.DeploymentFile = *deploymentFileShorthand - } else if *deploymentFile != "" { - config.DeploymentFile = *deploymentFile + if *versionFlag || *vFlag { + fmt.Println("infctl-cli version:", Version) + os.Exit(0) + } +} + +func ReadBaseConfig(path string) (BaseConfig, error) { + config := BaseConfig{} + + // If base.json does not exist, create it with default value + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := CreateDefaultBaseConfig(path); err != nil { + return BaseConfig{}, fmt.Errorf("failed to create default base config: %w", err) + } } data, err := os.ReadFile(path) if err != nil { - return BaseConfig{}, fmt.Errorf("failed to read file: %w", err) + return BaseConfig{}, fmt.Errorf("failed to read the config file: %w", err) } if err := json.Unmarshal(data, &config); err != nil { - return BaseConfig{}, fmt.Errorf("failed to unmarshal JSON: %w", err) + return config, fmt.Errorf("failed to unmarshal JSON: %w", err) } - config.DeploymentType = deploymentType return config, nil } + +// CreateDefaultBaseConfig creates a default base.json with retry_delay_seconds: 3 +func CreateDefaultBaseConfig(path string) error { + defaultConfig := BaseConfig{ + RetryDelaySenconds: 3, + } + data, err := json.MarshalIndent(defaultConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal default base config: %w", err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("failed to write default base config to file: %w", err) + } + return nil +} + +// LoadConfigs resolves config paths and loads both configs +func LoadConfigs() (BaseConfig, ProjectConfig, error) { + wd, err := os.Getwd() + if err != nil { + return BaseConfig{}, ProjectConfig{}, fmt.Errorf("failed to get current directory: %w", err) + } + + var baseConfigPath string + if baseConfigFile == "" { + baseConfigPath = wd + string(os.PathSeparator) + "base.json" + } else { + baseConfigPath = baseConfigFile + } + + var projectConfigPath string + if projectConfigFile == "" { + projectConfigPath = wd + string(os.PathSeparator) + "config.json" + } else { + projectConfigPath = projectConfigFile + } + + baseConfig, err := ReadBaseConfig(baseConfigPath) + if err != nil { + return BaseConfig{}, ProjectConfig{}, fmt.Errorf("error reading base config file: %w", err) + } + + projectConfig, err := ReadProjectConfig(projectConfigPath, &pipelineFile) + if err != nil { + return BaseConfig{}, ProjectConfig{}, fmt.Errorf("error reading project config file: %w", err) + } + + return baseConfig, projectConfig, nil +} diff --git a/config/base_test.go b/config/base_test.go new file mode 100644 index 0000000..87a6412 --- /dev/null +++ b/config/base_test.go @@ -0,0 +1,50 @@ +package config + +import ( + "os" + "testing" +) + +func TestReadBaseConfig_Basic(t *testing.T) { + // Create a temporary config file + file, err := os.CreateTemp("", "baseconfig_*.json") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(file.Name()) + + jsonContent := `{ + "projects_directory": "/projects", + "env": "dev", + "static_images": "/static", + "public_images": "/public", + "php_conf": "/php.ini", + "exports": "/exports", + "logs": "/logs", + "preview_path": "/preview", + "data_www": "/data", + "nginx_conf": "/nginx.conf", + "admin_url": "http://admin", + "preview_url": "http://preview", + "app_image": "app:v1", + "webserver_image": "web:v1", + "empty_db": "empty.db", + "db": "app.db", + "empty_imaages": "empty.img", + "deployment_type": "json", + "deployment_file": "base.json", + "port": 8080 + }` + file.WriteString(jsonContent) + file.Close() + + os.Setenv("DEPLOYMENT_TYPE", "json") + config, err := ReadBaseConfig(file.Name()) + if err != nil { + t.Fatalf("ReadBaseConfig failed: %v", err) + } + // Only check RetryDelaySenconds as that's the only field in BaseConfig now + if config.RetryDelaySenconds != 0 { + t.Errorf("expected RetryDelaySenconds 0, got %d", config.RetryDelaySenconds) + } +} diff --git a/config/customer.go b/config/customer.go deleted file mode 100644 index 560fff2..0000000 --- a/config/customer.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" -) - -type CustomerConfig struct { - Project string `json:"project"` - CustomerDirectory string `json:"customer_directory"` - UIURL string `json:"ui_url"` - StaticURL string `json:"static_url"` - Port int `json:"port"` -} - -func ReadCustomerConfig(path string) (CustomerConfig, error) { - data, err := os.ReadFile(path) - if err != nil { - return CustomerConfig{}, fmt.Errorf("failed to read file: %w", err) - } - - var cust CustomerConfig - if err := json.Unmarshal(data, &cust); err != nil { - return CustomerConfig{}, fmt.Errorf("failed to unmarshal JSON: %w", err) - } - - return cust, nil -} diff --git a/config/project.go b/config/project.go new file mode 100644 index 0000000..3ed3cd1 --- /dev/null +++ b/config/project.go @@ -0,0 +1,108 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" +) + +type ProjectConfig struct { + LogFormat string `json:"log_format"` + DeploymentFile string `json:"deployment_file"` + DeploymentType string `json:"deployment_type"` + DeploymentMode string `json:"deployment_mode"` +} + +func ValidateProjectConfig(config ProjectConfig) error { + + if config.LogFormat != "full" && config.LogFormat != "basic" && config.LogFormat != "none" { + return fmt.Errorf("invalid LogFormat: %s (must be 'full', 'basic', or 'none')", config.LogFormat) + } + + if _, err := os.Stat(config.DeploymentFile); os.IsNotExist(err) { + return fmt.Errorf("deployment file does not exist: %s", config.DeploymentFile) + } else if err != nil { + return fmt.Errorf("error checking deployment file: %w", err) + } + + if config.DeploymentMode != "json" && config.DeploymentMode != "api" { + return fmt.Errorf("invalid DeploymentMode: %s (must be 'json' or 'api')", config.DeploymentMode) + } + + fmt.Printf("DeploymentType: %s\n", config.DeploymentType) + if config.DeploymentType != "development" && config.DeploymentType != "pre-production" && config.DeploymentType != "production" { + return fmt.Errorf("invalid DeploymentType: %s (must be 'development', 'pre-production', or 'production')", config.DeploymentType) + } + return nil +} + +func CreateDefaultJsonConfig(path string, depploymentFile string) error { + + defaultConfig := ProjectConfig{ + LogFormat: "full", + DeploymentType: "development", + DeploymentFile: depploymentFile, + DeploymentMode: "json", + } + + data, err := json.MarshalIndent(defaultConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal default config: %w", err) + } + + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("failed to write default config to file: %w", err) + } + + return nil +} + +func ReadProjectConfig(path string, pipelineFile *string) (ProjectConfig, error) { + + var config ProjectConfig + + if pipelineFile == nil || *pipelineFile == "" { + return ProjectConfig{}, fmt.Errorf("no deployment file specified, please use -f or --deployment-file flag") + } + + config.DeploymentFile = *pipelineFile + + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := CreateDefaultJsonConfig(path, config.DeploymentFile); err != nil { + return ProjectConfig{}, fmt.Errorf("failed to create default config: %w", err) + } + } + + data, err := os.ReadFile(path) + if err != nil { + return ProjectConfig{}, fmt.Errorf("failed to read project configfile: %w", err) + } + + var proj ProjectConfig + if err := json.Unmarshal(data, &proj); err != nil { + return ProjectConfig{}, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + if proj.DeploymentMode != "" { + config.DeploymentMode = proj.DeploymentMode + } else { + config.DeploymentMode = "json" + } + + deploymentModeEnv := os.Getenv("DEPLOYMENT_MODE") + if deploymentModeEnv != "" { + config.DeploymentMode = deploymentModeEnv + } + + config.LogFormat = proj.LogFormat + if config.LogFormat == "" { + config.LogFormat = "full" + } + config.DeploymentType = proj.DeploymentType + + if err := ValidateProjectConfig(config); err != nil { + return ProjectConfig{}, err + } + + return config, nil +} diff --git a/config/project_test.go b/config/project_test.go new file mode 100644 index 0000000..6b486da --- /dev/null +++ b/config/project_test.go @@ -0,0 +1,46 @@ +package config + +import ( + "os" + "testing" +) + +func TestReadProjectConfig_Basic(t *testing.T) { + file, err := os.CreateTemp("", "projectconfig_*.json") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(file.Name()) + + jsonContent := `{ + "log_format": "full", + "deployment_file": "pipeline.json", + "deployment_mode": "json", + "deployment_type": "development" + }` + file.WriteString(jsonContent) + file.Close() + + pipelineFile, err := os.CreateTemp("", "pipeline_*.json") + if err != nil { + t.Fatalf("failed to create temp pipeline file: %v", err) + } + defer os.Remove(pipelineFile.Name()) + pipelineFile.WriteString(`{}`) // minimal valid JSON + pipelineFile.Close() + + pipelineFilePath := pipelineFile.Name() + config, err := ReadProjectConfig(file.Name(), &pipelineFilePath) + if err != nil { + t.Fatalf("ReadProjectConfig failed: %v", err) + } + if config.LogFormat != "full" { + t.Errorf("expected LogFormat 'full', got '%s'", config.LogFormat) + } + if config.DeploymentFile != pipelineFilePath { + t.Errorf("expected DeploymentFile '%s', got '%s'", pipelineFilePath, config.DeploymentFile) + } + if config.DeploymentMode != "json" { + t.Errorf("expected DeploymentMode 'json', got '%s'", config.DeploymentMode) + } +} diff --git a/database/database.go b/database/database.go deleted file mode 100644 index 88769e3..0000000 --- a/database/database.go +++ /dev/null @@ -1,74 +0,0 @@ -package database - -import ( - "database/sql" - "log" - "log/slog" - "os" - - _ "modernc.org/sqlite" -) - -func NewDatabase(dbPath string) (*sql.DB, error) { - - // Check if the application is running in a test environment - if os.Getenv("TEST_ENV") == "true" { - dbPath = ":memory:" // Use in-memory database for tests - slog.Info("🧪 Running in test environment, using in-memory database") - log.Fatal("🧪 Running in test environment, using in-memory database ") - } - - db, err := sql.Open("sqlite", dbPath) - if err != nil { - return nil, err - } - - createTableSQL := ` - CREATE TABLE IF NOT EXISTS project_name ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - project_name TEXT NOT NULL, - port INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - - );` - _, err = db.Exec(createTableSQL) - if err != nil { - return nil, err - } - - return db, nil -} - -func CheckProjectName(db *sql.DB, projectName string) (bool, error) { - var exists bool - query := `SELECT EXISTS(SELECT 1 FROM project_name WHERE project_name = ? LIMIT 1);` - err := db.QueryRow(query, projectName).Scan(&exists) - if err != nil && err != sql.ErrNoRows { - return false, err - } - return !exists, nil -} - -func AddProjectName(db *sql.DB, projectName string, port int) error { - query := `INSERT INTO project_name (project_name, port) VALUES (?, ?);` - _, err := db.Exec(query, projectName, port) - if err != nil { - return err - } - return nil -} - -func GetNextPortNumber(db *sql.DB) (int, error) { - var maxPortNumber sql.NullInt64 - query := `SELECT MAX(port) FROM project_name;` - err := db.QueryRow(query).Scan(&maxPortNumber) - if err != nil && err != sql.ErrNoRows { - return 0, err - } - - if !maxPortNumber.Valid { - // No rows in the table, return a default port number - return 10000, nil - } - return int(maxPortNumber.Int64 + 1), nil -} diff --git a/go.mod b/go.mod index bb27c9c..f91821a 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,3 @@ module headshed/infctl-cli go 1.23.3 - -require modernc.org/sqlite v1.38.0 - -require ( - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/sys v0.33.0 // indirect - modernc.org/libc v1.65.10 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect -) diff --git a/go.sum b/go.sum index 3dbd354..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +0,0 @@ -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= -modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA= -modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= -modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= -modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/install.sh b/install.sh index 561bfb6..239a360 100755 --- a/install.sh +++ b/install.sh @@ -20,18 +20,33 @@ fi case "$OS" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; + MSYS_NT-10.0-26100) OS="windows" ;; # gitbash *) echo "Unsupported OS: $OS"; exit 1 ;; esac # Construct the download URL -VERSION="v0.0.3" +VERSION="v0.0.5" BINARY_URL="https://codeberg.org/headshed/infctl-cli/releases/download/$VERSION/infctl-$OS-$ARCH" # Download the binary echo "Downloading infctl binary for $OS-$ARCH..." -sudo curl -s -L "$BINARY_URL" -o /usr/local/bin/infctl -# Make it executable -sudo chmod +x /usr/local/bin/infctl +if [ "$OS" == "windows" ]; then + # Create ~/bin if it doesn't exist + mkdir -p "$HOME/bin" + curl -s -L "$BINARY_URL.exe" -o "$HOME/bin/infctl.exe" + if [ $? -ne 0 ]; then + echo "Failed to download infctl for Windows" + exit 1 + fi + # Ensure ~/bin is in PATH for future sessions + if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bash_profile" 2>/dev/null; then + echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bash_profile" + echo "Added ~/bin to PATH in ~/.bash_profile. Please restart your shell or run: source ~/.bash_profile" + fi +else + sudo curl -s -L "$BINARY_URL" -o /usr/local/bin/infctl + sudo chmod +x /usr/local/bin/infctl +fi -echo "infctl installed successfully!" +echo "infctl install done." diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..8421c0d --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,119 @@ +package logger + +import ( + "context" + "fmt" + "log/slog" + "os" +) + +// multiHandler writes to multiple slog.Handlers +type multiHandler struct { + handlers []slog.Handler +} + +func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool { + for _, h := range m.handlers { + if h.Enabled(ctx, level) { + return true + } + } + return false +} + +func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error { + var err error + for _, h := range m.handlers { + if e := h.Handle(ctx, r); e != nil && err == nil { + err = e + } + } + return err +} + +func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandlers := make([]slog.Handler, len(m.handlers)) + for i, h := range m.handlers { + newHandlers[i] = h.WithAttrs(attrs) + } + return &multiHandler{handlers: newHandlers} +} + +func (m *multiHandler) WithGroup(name string) slog.Handler { + newHandlers := make([]slog.Handler, len(m.handlers)) + for i, h := range m.handlers { + newHandlers[i] = h.WithGroup(name) + } + return &multiHandler{handlers: newHandlers} +} + +type customMessageOnlyHandler struct { + output *os.File +} + +func (h *customMessageOnlyHandler) Enabled(_ context.Context, _ slog.Level) bool { + return true +} + +func (h *customMessageOnlyHandler) Handle(_ context.Context, r slog.Record) error { + msg := r.Message + if msg != "" { + _, err := fmt.Fprintln(h.output, msg) + return err + } + return nil +} + +func (h *customMessageOnlyHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h *customMessageOnlyHandler) WithGroup(_ string) slog.Handler { + return h +} + +// SetupLogger configures slog based on format and file +func SetupLogger(logFormat string, logFilePath string, level slog.Level) *slog.Logger { + var handlers []slog.Handler + var logFile *os.File + var levelVar slog.LevelVar + levelVar.Set(level) + + if logFilePath != "" { + f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err == nil { + logFile = f + // Do not close here; let OS handle it at process exit + } + } + + switch logFormat { + case "basic": + handlers = append(handlers, slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + if logFile != nil { + handlers = append(handlers, slog.NewTextHandler(logFile, &slog.HandlerOptions{})) + } + case "none": + handlers = append(handlers, &customMessageOnlyHandler{output: os.Stdout}) + if logFile != nil { + handlers = append(handlers, &customMessageOnlyHandler{output: logFile}) + } + default: + handlers = append(handlers, slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &levelVar})) + if logFile != nil { + handlers = append(handlers, slog.NewJSONHandler(logFile, &slog.HandlerOptions{Level: &levelVar})) + } + } + + if len(handlers) == 1 { + return slog.New(handlers[0]) + } + return slog.New(&multiHandler{handlers: handlers}) +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..7b29fb2 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,42 @@ +package logger + +import ( + "log/slog" + "os" + "testing" +) + +func TestMultiHandlerBasic(t *testing.T) { + // Create two custom handlers that write to files + f1, err := os.CreateTemp("", "log1_*.txt") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(f1.Name()) + f2, err := os.CreateTemp("", "log2_*.txt") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(f2.Name()) + + h1 := &customMessageOnlyHandler{output: f1} + h2 := &customMessageOnlyHandler{output: f2} + mh := &multiHandler{handlers: []slog.Handler{h1, h2}} + logger := slog.New(mh) + + logger.Info("test message") + + f1.Seek(0, 0) + buf1 := make([]byte, 100) + n1, _ := f1.Read(buf1) + if string(buf1[:n1]) != "test message\n" { + t.Errorf("expected message in log1, got: %q", string(buf1[:n1])) + } + + f2.Seek(0, 0) + buf2 := make([]byte, 100) + n2, _ := f2.Read(buf2) + if string(buf2[:n2]) != "test message\n" { + t.Errorf("expected message in log2, got: %q", string(buf2[:n2])) + } +} diff --git a/main.go b/main.go index f8602b6..eaa9f18 100644 --- a/main.go +++ b/main.go @@ -14,101 +14,61 @@ package main import ( - "context" + "encoding/json" + "fmt" - "log" "log/slog" "os" "headshed/infctl-cli/app" "headshed/infctl-cli/config" + "headshed/infctl-cli/logger" ) -type customMessageOnlyHandler struct { - output *os.File -} - -func (h *customMessageOnlyHandler) Enabled(_ context.Context, _ slog.Level) bool { - return true -} - -func (h *customMessageOnlyHandler) Handle(_ context.Context, r slog.Record) error { - // Directly retrieve the message from the record - msg := r.Message - if msg != "" { - _, err := fmt.Fprintln(h.output, msg) - return err - } - return nil -} - -func (h *customMessageOnlyHandler) WithAttrs(_ []slog.Attr) slog.Handler { - return h -} - -func (h *customMessageOnlyHandler) WithGroup(_ string) slog.Handler { - return h -} - func main() { - var levelVar slog.LevelVar - levelVar.Set(slog.LevelDebug) - - var logger *slog.Logger - if os.Getenv("LOG_FORMAT") == "basic" { - logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey { - return slog.Attr{} - } - return a - }, - })) - - } else if os.Getenv("LOG_FORMAT") == "none" { - logger = slog.New(&customMessageOnlyHandler{output: os.Stdout}) - - } else { - logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &levelVar})) + config.ParseFlags() + baseConfig, projectConfig, err := config.LoadConfigs() + if err != nil { + fmt.Fprintf(os.Stderr, "Config error: %v\n", err) + os.Exit(1) } + logFormat := projectConfig.LogFormat + if os.Getenv("LOG_FORMAT") != "" { + logFormat = os.Getenv("LOG_FORMAT") + } + logFilePath := os.Getenv("LOG_FILE") + + logger := logger.SetupLogger(logFormat, logFilePath, slog.LevelDebug) slog.SetDefault(logger) - if err := run(); err != nil { - log.Fatalf("Application error: %v", err) + if err := run(projectConfig, baseConfig); err != nil { + slog.Error("❌ 💥 Pipeline error: " + err.Error()) + os.Exit(1) + } else { + slog.Info("✅ 🚀 Pipeline completed successfully") } } -func run() error { +func run(projectConfig config.ProjectConfig, baseConfig config.BaseConfig) error { - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) - } - baseConfigPath := wd + string(os.PathSeparator) + "base.json" - configPath := wd + string(os.PathSeparator) + "config.json" - - baseConfig, err := config.ReadBaseConfig(baseConfigPath) - if err != nil { - return fmt.Errorf("error reading base config file: %w", err) - } - customerConfig, err := config.ReadCustomerConfig(configPath) - if err != nil { - return fmt.Errorf("error reading customer config file: %w", err) - } - appState, err := app.NewAppState(customerConfig, baseConfig, "app.db") + appState, err := app.NewAppState(projectConfig, baseConfig) if err != nil { return fmt.Errorf("failed to initialize app state: %w", err) } - defer func() { - if err := appState.DB.Close(); err != nil { - log.Printf("Error closing database: %v", err) - } - }() - if err := appState.CreatePipeline(); err != nil { - return fmt.Errorf("failed to create customer project: %w", err) + // Pretty-print appState as JSON + if os.Getenv("DEBUG") == "1" { + if jsonBytes, err := json.MarshalIndent(appState, "", " "); err == nil { + fmt.Printf(">>DEBUG>> appState:\n%s\n", string(jsonBytes)) + } else { + fmt.Printf(">>DEBUG>> appState: %+v\n", appState) + } + } + + if err := appState.CreateProjectAndRunPipeline(); err != nil { + return fmt.Errorf("pipeline error: %w", err) } return nil diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c4f274d --- /dev/null +++ b/main_test.go @@ -0,0 +1,10 @@ +package main + +import "testing" + +// Example test for main package +func TestMainDummy(t *testing.T) { + // This is a placeholder test. + // Add real tests for functions in main.go if possible. + t.Log("main package test ran") +} diff --git a/pipelines/dev/carry_on.json b/pipelines/dev/carry_on.json new file mode 100644 index 0000000..8547723 --- /dev/null +++ b/pipelines/dev/carry_on.json @@ -0,0 +1,20 @@ +[ + { + "name": "run a failing job", + "function": "RunCommand", + "params": [ + "./scripts/failue.sh" + ], + "retryCount": 2, + "shouldAbort": false + }, + { + "name": "run a successful job", + "function": "RunCommand", + "params": [ + "./scripts/success.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/pipelines/dev/do_not_carry_on.json b/pipelines/dev/do_not_carry_on.json new file mode 100644 index 0000000..0f9696e --- /dev/null +++ b/pipelines/dev/do_not_carry_on.json @@ -0,0 +1,20 @@ +[ + { + "name": "run a failing job", + "function": "RunCommand", + "params": [ + "./scripts/failue.sh" + ], + "retryCount": 2, + "shouldAbort": true + }, + { + "name": "run a successful job", + "function": "RunCommand", + "params": [ + "./scripts/success.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] diff --git a/pipelines/dev/failing.json b/pipelines/dev/failing.json new file mode 100644 index 0000000..4839fb1 --- /dev/null +++ b/pipelines/dev/failing.json @@ -0,0 +1,29 @@ +[ + { + "name": "run a failing job", + "function": "RunCommand", + "params": [ + "./scripts/failue.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "Configure Vagrant K3s", + "function": "RunCommand", + "params": [ + "./scripts/configure_vagrant_k3s.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "Create Vagrant workstation", + "function": "RunCommand", + "params": [ + "./scripts/install_vagrant_workstation.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/pipelines/dev/multiple_pipelines.json b/pipelines/dev/multiple_pipelines.json new file mode 100644 index 0000000..9a1ce3b --- /dev/null +++ b/pipelines/dev/multiple_pipelines.json @@ -0,0 +1,38 @@ +[ + { + "name": "run succeeding pipeline", + "function": "RunCommand", + "params": [ + "LOG_FORMAT=none infctl -f pipelines/dev/succeeding.json" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "run failing pipeline", + "function": "RunCommand", + "params": [ + "LOG_FORMAT=none infctl -f pipelines/dev/failing.json" + ], + "retryCount": 0, + "shouldAbort": false + }, + { + "name": "run retry pipeline", + "function": "RunCommand", + "params": [ + "LOG_FORMAT=none infctl -f pipelines/dev/retry.json" + ], + "retryCount": 0, + "shouldAbort": false + }, + { + "name": "run carry on pipeline", + "function": "RunCommand", + "params": [ + "LOG_FORMAT=none infctl -f pipelines/dev/carry_on.json" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/pipelines/dev/retry.json b/pipelines/dev/retry.json new file mode 100644 index 0000000..c158c52 --- /dev/null +++ b/pipelines/dev/retry.json @@ -0,0 +1,11 @@ +[ + { + "name": "run a failing job", + "function": "RunCommand", + "params": [ + "./scripts/failue.sh" + ], + "retryCount": 1, + "shouldAbort": true + } +] diff --git a/pipelines/dev/succeeding.json b/pipelines/dev/succeeding.json new file mode 100644 index 0000000..87a1bd4 --- /dev/null +++ b/pipelines/dev/succeeding.json @@ -0,0 +1,11 @@ +[ + { + "name": "run a successful job", + "function": "RunCommand", + "params": [ + "./scripts/success.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/pipelines/dev/vagrant-k3s.json b/pipelines/dev/vagrant-k3s.json index 99a86b1..3881c11 100644 --- a/pipelines/dev/vagrant-k3s.json +++ b/pipelines/dev/vagrant-k3s.json @@ -1,5 +1,13 @@ [ - + { + "name": "Checks for .envrc", + "function": "RunCommand", + "params": [ + "./scripts/envrc_checks.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, { "name": "Create Vagrant nodes", "function": "RunCommand", @@ -9,7 +17,6 @@ "retryCount": 0, "shouldAbort": true }, - { "name": "Configure Vagrant K3s", "function": "RunCommand", @@ -19,8 +26,6 @@ "retryCount": 0, "shouldAbort": true }, - - { "name": "Create Vagrant workstation", "function": "RunCommand", @@ -30,4 +35,4 @@ "retryCount": 0, "shouldAbort": true } -] +] \ No newline at end of file diff --git a/scripts/configure_vagrant_k3s.sh b/scripts/configure_vagrant_k3s.sh index 29aabdf..e7ab40a 100755 --- a/scripts/configure_vagrant_k3s.sh +++ b/scripts/configure_vagrant_k3s.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -# set -euo pipefail - # This script checks for Vagrant and VirtualBox prerequisites, # ensures Vagrant VMs are running, and gathers network and # system information from the VMs. diff --git a/scripts/dev-pre-flight-checks.sh b/scripts/dev-pre-flight-checks.sh index 4523443..2ffdde6 100755 --- a/scripts/dev-pre-flight-checks.sh +++ b/scripts/dev-pre-flight-checks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -required_tools=("infctl" "pwgen" "kubectl" "k3d" "helm" "jq" "docker") +required_tools=("infctl" "pwgen" "kubectl" "k3d" "helm" "jq" "git" "docker" "vagrant") MISSING=false check_required_tools() { diff --git a/scripts/envrc_checks.sh b/scripts/envrc_checks.sh new file mode 100755 index 0000000..f0029d3 --- /dev/null +++ b/scripts/envrc_checks.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# check if an .envrc file exists +if [ ! -f .envrc ]; then + echo ".envrc file not found" + cp .envrc.example .envrc + if [ $? -eq 0 ]; then + echo ".envrc file created from .envrc.example" + else + echo "Failed to create .envrc file" + exit 1 + fi +else + echo ".envrc file found" + cp .envrc vagrant/dev/ubuntu/.envrc + if [ $? -eq 0 ]; then + echo ".envrc file synced to vagrant/dev/ubuntu/.envrc" + else + echo "Failed to sync .envrc file" + exit 1 + fi +fi diff --git a/scripts/failue.sh b/scripts/failue.sh new file mode 100755 index 0000000..238532a --- /dev/null +++ b/scripts/failue.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# sleep 5 + +echo "crash" + +# sleep 1 + +echo "bang" + +# sleep 2 + +echo "wallop" + +# sleep 1 + +echo "Houston, we have a problem" + +sleep 1 + +exit 1 diff --git a/scripts/install_traefik.sh b/scripts/install_traefik.sh deleted file mode 100755 index d021b66..0000000 --- a/scripts/install_traefik.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash - -if kubectl -n kube-system get pods --no-headers 2>/dev/null | grep -q 'traefik'; then - echo "Traefik is already running in the 'kube-system' namespace. Upgrading instead." - - # Create a temporary values file for more complex configuration - cat > /tmp/traefik-values.yaml < /tmp/traefik-values.yaml < \ No newline at end of file diff --git a/vagrant/dev/ubuntu/Vagrantfile b/vagrant/dev/ubuntu/Vagrantfile index 98fc79d..eee4a3e 100644 --- a/vagrant/dev/ubuntu/Vagrantfile +++ b/vagrant/dev/ubuntu/Vagrantfile @@ -21,6 +21,7 @@ Vagrant.configure("2") do |config| # VM 1 Configuration config.vm.define "vm1" do |vm1| vm1.vm.box = "ubuntu/jammy64" + vm1.vm.boot_timeout = 600 vm1.vm.hostname = "vm1" # Fixed private network IP @@ -34,7 +35,7 @@ Vagrant.configure("2") do |config| end vm1.vm.provider "virtualbox" do |vb| - vb.memory = "2048" # 2GB memory + vb.memory = "2048" # 4GB memory vb.cpus = 2 end @@ -48,6 +49,7 @@ Vagrant.configure("2") do |config| # VM 2 Configuration config.vm.define "vm2" do |vm2| vm2.vm.box = "ubuntu/jammy64" + vm2.vm.boot_timeout = 600 vm2.vm.hostname = "vm2" # Fixed private network IP @@ -61,7 +63,7 @@ Vagrant.configure("2") do |config| end vm2.vm.provider "virtualbox" do |vb| - vb.memory = "2048" # 2GB memory + vb.memory = "2048" # 4GB memory vb.cpus = 2 end @@ -75,6 +77,7 @@ Vagrant.configure("2") do |config| # VM 3 Configuration config.vm.define "vm3" do |vm3| vm3.vm.box = "ubuntu/jammy64" + vm3.vm.boot_timeout = 600 vm3.vm.hostname = "vm3" # Fixed private network IP @@ -88,7 +91,7 @@ Vagrant.configure("2") do |config| end vm3.vm.provider "virtualbox" do |vb| - vb.memory = "2048" # 2GB memory + vb.memory = "2048" # 4GB memory vb.cpus = 2 end @@ -102,6 +105,7 @@ Vagrant.configure("2") do |config| # Ansible Controller/Workstation Configuration config.vm.define "workstation" do |ws| ws.vm.box = "ubuntu/jammy64" + ws.vm.boot_timeout = 600 ws.vm.hostname = "ansible-workstation" ws.vm.synced_folder ".", "/vagrant" diff --git a/vagrant/dev/ubuntu/ansible/install_dnsmasq.yaml b/vagrant/dev/ubuntu/ansible/install_dnsmasq.yaml new file mode 100644 index 0000000..d74d049 --- /dev/null +++ b/vagrant/dev/ubuntu/ansible/install_dnsmasq.yaml @@ -0,0 +1,78 @@ +--- +- name: Install Dnsmasq on workstation + hosts: localhost + become: true + become_user: root + serial: 1 # Ensure tasks are executed one host at a time + vars_files: + - vars.yaml + + tasks: + + - name: Install dnsmasq + ansible.builtin.apt: + name: dnsmasq + state: present + + + - name: Stop systemd-resolved + ansible.builtin.systemd: + name: systemd-resolved + state: stopped + + - name: Disable systemd-resolved + ansible.builtin.systemd: + name: systemd-resolved + enabled: false + + - name: check to see if /etc/resolv.conf is a symlink + ansible.builtin.stat: + path: /etc/resolv.conf + register: resolv_conf + + - name: Remove /etc/resolv.conf if it is a symlink + ansible.builtin.file: + path: /etc/resolv.conf + state: absent + when: resolv_conf.stat.islnk + + - name: Ensure /etc/resolv.conf is a regular file + ansible.builtin.file: + path: /etc/resolv.conf + state: touch + + - name: Ensure /etc/resolv.conf uses 127.0.0.1 for server + ansible.builtin.lineinfile: + path: /etc/resolv.conf + regexp: '^nameserver' + line: 'nameserver 127.0.0.1' + state: present + + - name: Configure dnsmasq + ansible.builtin.copy: + dest: /etc/dnsmasq.d/k3s-cluster.conf + content: | + address=/{{ dnsmasq_k3s_domain }} + server=1.1.1.1 + server=8.8.8.8 + owner: root + group: root + mode: "0644" + notify: Restart dnsmasq + + - name: Ensure conf-dir is uncommented in /etc/dnsmasq.conf + ansible.builtin.lineinfile: + path: /etc/dnsmasq.conf + regexp: '^#?conf-dir=/etc/dnsmasq.d' + line: 'conf-dir=/etc/dnsmasq.d' + state: present + owner: root + group: root + mode: '0644' + + handlers: + - name: Restart dnsmasq + ansible.builtin.systemd: + name: dnsmasq + state: restarted + diff --git a/vagrant/dev/ubuntu/ansible/install_k3s_3node.yaml b/vagrant/dev/ubuntu/ansible/install_k3s_3node.yaml index bd96230..a8ed7b3 100644 --- a/vagrant/dev/ubuntu/ansible/install_k3s_3node.yaml +++ b/vagrant/dev/ubuntu/ansible/install_k3s_3node.yaml @@ -55,7 +55,7 @@ - name: Install k3s on first node ansible.builtin.shell: | set -o pipefail - K3S_TOKEN=$(cat /opt/k3s-token) /bin/bash /tmp/k3s_install.sh server --cluster-init --disable traefik --disable servicelb --tls-san {{ k3s_url_ip }} --node-name vm1 --node-ip {{ vm1_ip }} + K3S_TOKEN=$(cat /opt/k3s-token) /bin/bash /tmp/k3s_install.sh server --cluster-init --disable traefik --disable servicelb --tls-san {{ k3s_url_ip }} --node-name vm1 --node-ip {{ vm1_ip }} --flannel-iface=enp0s8 if [ $? -eq 0 ]; then mkdir -p /home/vagrant/.kube && cp /etc/rancher/k3s/k3s.yaml /home/vagrant/.kube/config && chown vagrant:vagrant /home/vagrant/.kube/config fi @@ -91,7 +91,7 @@ {% endif %} K3S_URL=https://{{ k3s_url_ip }}:6443 \ K3S_TOKEN={{ k3s_token_content.stdout }} \ - INSTALL_K3S_EXEC="server --server https://{{ k3s_url_ip }}:6443 --disable traefik --disable servicelb --node-name={{ inventory_hostname }} --node-ip ${NODE_IP}" \ + INSTALL_K3S_EXEC="server --server https://{{ k3s_url_ip }}:6443 --disable traefik --disable servicelb --node-name={{ inventory_hostname }} --node-ip ${NODE_IP} --flannel-iface=enp0s8" \ /bin/bash /tmp/k3s_install.sh 2>&1 exit_code=$? if [ $exit_code -ne 0 ]; then diff --git a/vagrant/dev/ubuntu/ansible/install_longhorn_prereqs.yaml b/vagrant/dev/ubuntu/ansible/install_longhorn_prereqs.yaml new file mode 100644 index 0000000..c22a182 --- /dev/null +++ b/vagrant/dev/ubuntu/ansible/install_longhorn_prereqs.yaml @@ -0,0 +1,47 @@ +--- +- name: Install k3s on 3-node cluster + hosts: vm1,vm2,vm3 + become: true + become_user: root + serial: 1 # Ensure tasks are executed one host at a time + vars_files: + - vars.yaml + + tasks: + - name: Install open-iscsi on all nodes + ansible.builtin.package: + name: open-iscsi + state: present + + - name: Install nfs-common on all nodes + ansible.builtin.package: + name: nfs-common + state: present + + - name: Install cryptsetup and dmsetup packages + ansible.builtin.package: + name: + - cryptsetup + - dmsetup + state: present + + - name: Load dm_crypt kernel module + community.general.modprobe: + name: dm_crypt + state: present + + - name: Make dm_crypt module load on boot + ansible.builtin.lineinfile: + path: /etc/modules + line: dm_crypt + create: yes + + - name: Check if dm_crypt module is loaded + ansible.builtin.shell: lsmod | grep dm_crypt + register: dm_crypt_check + failed_when: false + changed_when: false + + - name: Show dm_crypt status + ansible.builtin.debug: + msg: "dm_crypt module is {{ 'loaded' if dm_crypt_check.rc == 0 else 'not loaded' }}" \ No newline at end of file diff --git a/vagrant/dev/ubuntu/ansible/provision_workstation.sh b/vagrant/dev/ubuntu/ansible/provision_workstation.sh index 45605b7..1077193 100644 --- a/vagrant/dev/ubuntu/ansible/provision_workstation.sh +++ b/vagrant/dev/ubuntu/ansible/provision_workstation.sh @@ -1,18 +1,27 @@ #!/usr/bin/env bash -sudo apt-get update -sudo apt-get install -y software-properties-common git vim python3.10-venv +sudo apt-get update +sudo apt-get install -y software-properties-common git vim python3.10-venv jq figlet + +source /vagrant/.envrc # Set up ansible environment for vagrant user sudo -u vagrant mkdir -p /home/vagrant/.ansible sudo -u vagrant touch /home/vagrant/.ansible/ansible.cfg # Create workspace and SSH directories -sudo -u vagrant mkdir -p /home/vagrant/ansible sudo -u vagrant mkdir -p /home/vagrant/.ssh sudo chmod 700 /home/vagrant/.ssh +# create directories and copy files to /home/vagrant +mkdir -p /home/vagrant/{ansible,scripts,pipelines,k8s} +sudo cp -r /vagrant/ansible/* /home/vagrant/ansible/ +sudo cp -r /vagrant/scripts/* /home/vagrant/scripts/ +sudo cp -r /vagrant/pipelines/* /home/vagrant/pipelines +sudo cp -r /vagrant/k8s/* /home/vagrant/k8s +sudo chmod +x /home/vagrant/pipelines/*.sh + # Copy the Vagrant private keys (these will be synced by Vagrant) for i in {1..3}; do sudo -u vagrant cp /vagrant/.vagrant/machines/vm$i/virtualbox/private_key /home/vagrant/.ssh/vm${i}_key @@ -82,7 +91,6 @@ if [ $? -ne 0 ]; then exit 1 fi -cp -r /vagrant/ansible/* /home/vagrant/ansible/ eval `ssh-agent -s` ssh-add # ~/machines/*/virtualbox/private_key @@ -98,12 +106,26 @@ if ! grep -qF "$BLOCK_START" "$BASHRC"; then eval `ssh-agent -s` ssh-add ~/machines/*/virtualbox/private_key ssh-add -L +source /vagrant/.envrc EOF else echo "Provisioning block already present in $BASHRC" fi -ANSIBLE_HOST_KEY_CHECKING=False ansible --inventory-file /home/vagrant/ansible/ansible_inventory.ini -m ping vm1,vm2,vm3 +echo +echo ------------------------- +echo + +su - vagrant +id + +echo +echo ------------------------- +echo + +ssh-add ~/.ssh/vm*_key + +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible --inventory-file /home/vagrant/ansible/ansible_inventory.ini -m ping vm1,vm2,vm3 if [ $? -ne 0 ]; then echo "Ansible ping failed. Please check your Vagrant VMs and network configuration." @@ -111,7 +133,7 @@ if [ $? -ne 0 ]; then fi # install_keepalived.yaml -ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook install_keepalived.yaml --inventory-file ansible_inventory.ini +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook install_keepalived.yaml --inventory-file ansible_inventory.ini if [ $? -ne 0 ]; then echo "Ansible playbook failed. Please check your Vagrant VMs and network configuration." exit 1 @@ -119,17 +141,32 @@ fi echo "Keepalived installation completed." # install_k3s_3node.yaml -ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook install_k3s_3node.yaml --inventory-file ansible_inventory.ini +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook install_k3s_3node.yaml --inventory-file ansible_inventory.ini if [ $? -ne 0 ]; then echo "Ansible playbook failed. Please check your Vagrant VMs and network configuration." exit 1 fi # copy_k8s_config.yaml -ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook copy_k8s_config.yaml --inventory-file ansible_inventory.ini +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook copy_k8s_config.yaml --inventory-file ansible_inventory.ini if [ $? -ne 0 ]; then echo "Ansible playbook failed. Please check your Vagrant VMs and network configuration." exit 1 fi +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook install_dnsmasq.yaml --inventory-file ansible_inventory.ini +if [ $? -ne 0 ]; then + echo "Ansible playbook failed. Please check your Vagrant VMs and network configuration." + exit 1 +fi + +# check infctl +cd /home/vagrant +bash /home/vagrant/scripts/check_install_infctl.sh +if [ $? -ne 0 ]; then + echo "infctl check failed. Please check your installation." + exit 1 +fi + + diff --git a/vagrant/dev/ubuntu/ansible/vars.yaml b/vagrant/dev/ubuntu/ansible/vars.yaml index 9a02a8e..5b1482c 100644 --- a/vagrant/dev/ubuntu/ansible/vars.yaml +++ b/vagrant/dev/ubuntu/ansible/vars.yaml @@ -7,6 +7,8 @@ k3s_url_ip: "{{ lookup('env', 'K3S_URL_IP') | default('192.168.56.250', true) }} workstation_ip: "{{ lookup('env', 'WORKSTATION_IP') | default('192.168.56.10', true) }}" network_prefix: "{{ lookup('env', 'VAGRANT_NETWORK_PREFIX') | default('192.168.56', true) }}" +dnsmasq_k3s_domain: "{{ lookup('env', 'DNSMASQ_K3S_DOMAIN') | default('headshed.it/192.168.56.230', true) }}" + # K3s configuration k3s_cluster_name: "dev-cluster" k3s_token_file: "/opt/k3s-token" diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/deployment.yaml b/vagrant/dev/ubuntu/k8s/nginx-test/deployment.yaml new file mode 100644 index 0000000..251c952 --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-with-storage + namespace: default +spec: + selector: + matchLabels: + app: nginx-storage + replicas: 1 + template: + metadata: + labels: + app: nginx-storage + spec: + + initContainers: + - name: init-nginx-content + image: busybox + command: ["sh", "-c", "echo '

Welcome to nginx!

using MVK

https://mvk.headshed.dev/

' > /usr/share/nginx/html/index.html"] + volumeMounts: + - name: nginx-data + mountPath: /usr/share/nginx/html + containers: + - name: nginx + image: nginx:stable + ports: + - containerPort: 80 + volumeMounts: + - name: nginx-data + mountPath: /usr/share/nginx/html + volumes: + - name: nginx-data + persistentVolumeClaim: + claimName: nginx-data-pvc diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml b/vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml new file mode 100644 index 0000000..faa7a6d --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infmon-cli + namespace: default +spec: + selector: + matchLabels: + app: infmon-cli + replicas: 1 + template: + metadata: + labels: + app: infmon-cli + spec: + containers: + - name: infmon-cli + image: 192.168.2.190:5000/infmon-cli:0.0.1 + command: ["sleep", "infinity"] + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + volumeMounts: + - name: kubeconfig + mountPath: /root/.kube/config + subPath: config + volumes: + - name: kubeconfig + secret: + secretName: infmon-kubeconfig diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/ingress.yaml b/vagrant/dev/ubuntu/k8s/nginx-test/ingress.yaml new file mode 100644 index 0000000..e0a7d35 --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/ingress.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik-ingress + namespace: default + # This annotation is good practice to ensure it uses the right entrypoint + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + # This block is the key. It tells Ingress controllers like Traefik + # to use the specified secret for TLS termination for the listed hosts. + tls: + - hosts: + - "*.headshed.it" # Or a specific subdomain like test.headshed.it + secretName: wildcard-headshed-it-tls # <-- The name of the secret you created + + rules: + - host: nginx.headshed.it # The actual domain you will use to access the service + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx-storage # The name of the k8s service for your app + port: + number: 80 # The port your service is listening on diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/install.sh b/vagrant/dev/ubuntu/k8s/nginx-test/install.sh new file mode 100755 index 0000000..c55873b --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/install.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + + +kubectl apply -f pvc.yaml +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml +kubectl apply -f ingress.yaml diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/pvc.yaml b/vagrant/dev/ubuntu/k8s/nginx-test/pvc.yaml new file mode 100644 index 0000000..9816354 --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nginx-data-pvc + namespace: default +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 1Gi diff --git a/vagrant/dev/ubuntu/k8s/nginx-test/service.yaml b/vagrant/dev/ubuntu/k8s/nginx-test/service.yaml new file mode 100644 index 0000000..6ae6ac8 --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/nginx-test/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx-storage + namespace: default +spec: + selector: + app: nginx-storage + ports: + - protocol: TCP + port: 80 + targetPort: 80 diff --git a/vagrant/dev/ubuntu/k8s/traefik-tlsstore.yaml b/vagrant/dev/ubuntu/k8s/traefik-tlsstore.yaml new file mode 100644 index 0000000..146c038 --- /dev/null +++ b/vagrant/dev/ubuntu/k8s/traefik-tlsstore.yaml @@ -0,0 +1,8 @@ +apiVersion: traefik.io/v1alpha1 +kind: TLSStore +metadata: + name: default + namespace: traefik +spec: + defaultCertificate: + secretName: wildcard-headshed-it-tls diff --git a/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json b/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json new file mode 100644 index 0000000..837242f --- /dev/null +++ b/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json @@ -0,0 +1,20 @@ +[ + { + "name": "Install Helm", + "function": "RunCommand", + "params": [ + "./scripts/helm_check_install.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "Install traefik", + "function": "RunCommand", + "params": [ + "./scripts/install_traefik.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/vagrant/dev/ubuntu/pipelines/vagrant-longhorn.json b/vagrant/dev/ubuntu/pipelines/vagrant-longhorn.json new file mode 100644 index 0000000..85e4322 --- /dev/null +++ b/vagrant/dev/ubuntu/pipelines/vagrant-longhorn.json @@ -0,0 +1,29 @@ +[ + { + "name": "Install Longhorn pre-requisites", + "function": "RunCommand", + "params": [ + "./scripts/longhorn_prereqs.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "Install Longhorn", + "function": "RunCommand", + "params": [ + "./scripts/install_longhorn.sh" + ], + "retryCount": 0, + "shouldAbort": true + }, + { + "name": "Wait for Longhorn pods to come up", + "function": "RunCommand", + "params": [ + "./scripts/wait_for_longhorn.sh" + ], + "retryCount": 10, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/vagrant/dev/ubuntu/pipelines/vagrant-metallb.json b/vagrant/dev/ubuntu/pipelines/vagrant-metallb.json new file mode 100644 index 0000000..b859f72 --- /dev/null +++ b/vagrant/dev/ubuntu/pipelines/vagrant-metallb.json @@ -0,0 +1,11 @@ +[ + { + "name": "Install metallb", + "function": "RunCommand", + "params": [ + "./scripts/install_metallb.sh" + ], + "retryCount": 0, + "shouldAbort": true + } +] \ No newline at end of file diff --git a/vagrant/dev/ubuntu/scripts/check_install_infctl.sh b/vagrant/dev/ubuntu/scripts/check_install_infctl.sh new file mode 100755 index 0000000..237ca8c --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/check_install_infctl.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# function to install infctl +install_infctl() { + echo "Installing infctl..." + # Add installation commands here + curl -L https://codeberg.org/headshed/infctl-cli/raw/branch/main/install.sh | bash + +} + +if ! command -v infctl &> /dev/null +then + echo "infctl could not be found, installing..." + install_infctl +fi + + + + + +echo "infctl is installed and ready to use." diff --git a/vagrant/dev/ubuntu/scripts/helm_check_install.sh b/vagrant/dev/ubuntu/scripts/helm_check_install.sh new file mode 100755 index 0000000..947e376 --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/helm_check_install.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# check to see if helm is installed +if ! command -v helm &> /dev/null; then + echo "Helm is not installed. Installing it now ..." + # curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + if [ $? -ne 0 ]; then + echo "Failed to install Helm." + exit 1 + fi +fi + +helm version diff --git a/vagrant/dev/ubuntu/scripts/install_longhorn.sh b/vagrant/dev/ubuntu/scripts/install_longhorn.sh new file mode 100755 index 0000000..51c565e --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/install_longhorn.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +echo +echo "vagrant longhorn installation" +echo + +ssh-add ~/.ssh/vm*_key +source /home/vagrant/ansible/venv/bin/activate +# Check if there are any pods in the longhorn-system namespace +if kubectl -n longhorn-system get pods --no-headers 2>/dev/null | grep -q '^[^ ]'; then + echo "Pods already exist in the longhorn-system namespace. Skipping installation." + exit 0 +fi +# https://github.com/longhorn/longhorn/releases +# v1.8.1 in prod 1.9.1 is latest +LONGHORN_RELEASE="v1.8.1" +LONGHORN_RELEASE_URL="https://raw.githubusercontent.com/longhorn/longhorn/$LONGHORN_RELEASE/deploy/longhorn.yaml" + +echo "Applying Longhorn release $LONGHORN_RELEASE..." +echo "Using Longhorn release URL: $LONGHORN_RELEASE_URL" + +kubectl apply -f $LONGHORN_RELEASE_URL diff --git a/vagrant/dev/ubuntu/scripts/install_metallb.sh b/vagrant/dev/ubuntu/scripts/install_metallb.sh new file mode 100755 index 0000000..838d23a --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/install_metallb.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + + +source /vagrant/.envrc + +# Check if MetalLB is already installed by looking for the controller deployment +if ! kubectl get deployment -n metallb-system controller &>/dev/null; then + echo "Installing MetalLB..." + kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/main/config/manifests/metallb-native.yaml + if [ $? -ne 0 ]; then + echo "Fatal: Failed to apply MetalLB manifest." >&2 + exit 1 + fi + + # Wait for MetalLB components to be ready + echo "Waiting for MetalLB components to be ready..." + kubectl wait --namespace metallb-system \ + --for=condition=ready pod \ + --selector=app=metallb \ + --timeout=90s + +else + echo "MetalLB is already installed." +fi + +# Wait for the webhook service to be ready +echo "Waiting for MetalLB webhook service to be ready..." +kubectl wait --namespace metallb-system \ + --for=condition=ready pod \ + --selector=component=webhook \ + --timeout=90s + +# Check if the IPAddressPool already exists +if ! kubectl get ipaddresspool -n metallb-system default &>/dev/null; then + echo "Creating MetalLB IPAddressPool..." + cat </dev/null; then + echo "Creating MetalLB L2Advertisement..." + cat < "$TMPFILE" < /dev/null; then + echo "Traefik is already installed in the 'traefik' namespace. Upgrading..." + helm upgrade traefik traefik/traefik --namespace traefik -f "$TMPFILE" +else + echo "Installing Traefik..." + helm repo add traefik https://traefik.github.io/charts + helm repo update + # Using --create-namespace is good practice, though traefik will always exist. + helm install traefik traefik/traefik --namespace traefik --create-namespace -f "$TMPFILE" +fi + +# Apply the TLS store configuration +kubectl apply -f k8s/traefik-tlsstore.yaml +if [ $? -ne 0 ]; then + echo "Failed to apply TLS store configuration." + exit 1 +fi + + +echo +echo "To access the dashboard:" +echo "kubectl port-forward -n traefik \$(kubectl get pods -n traefik -l \"app.kubernetes.io/name=traefik\" -o name) 9000:9000" +echo "Then visit http://localhost:9000/dashboard/ in your browser" + diff --git a/vagrant/dev/ubuntu/scripts/install_vagrant_longhorn.sh b/vagrant/dev/ubuntu/scripts/install_vagrant_longhorn.sh new file mode 100755 index 0000000..368519c --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/install_vagrant_longhorn.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +echo +echo "vagrant longhorn installation" +echo + +ssh-add ~/.ssh/vm*_key +source /home/vagrant/ansible/venv/bin/activate +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible --inventory-file /home/vagrant/ansible/ansible_inventory.ini -m ping vm1,vm2,vm3 +if [ $? -ne 0 ]; then + echo "Ansible ping failed. Please check your Vagrant VMs and network configuration." + exit 1 +fi +echo "Ansible ping successful." + +# Check if there are any pods in the longhorn-system namespace +if kubectl -n longhorn-system get pods --no-headers 2>/dev/null | grep -q '^[^ ]'; then + echo "Pods already exist in the longhorn-system namespace. Skipping installation." + exit 0 +fi + +echo "Installing Longhorn prerequisites..." + + +# install_longhorn_prereqs.yaml +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook ~/ansible/install_longhorn_prereqs.yaml --inventory-file /home/vagrant/ansible/ansible_inventory.ini +if [ $? -ne 0 ]; then + echo "Ansible playbook failed. Please check the playbook and your inventory." + exit 1 +fi + +echo "installing Longhorn ..." + +# https://github.com/longhorn/longhorn/releases +# v1.8.1 in prod 1.9.1 is latest +LONGHORN_RELEASE="v1.8.1" +LONGHORN_RELEASE_URL="https://raw.githubusercontent.com/longhorn/longhorn/$LONGHORN_RELEASE/deploy/longhorn.yaml" + +echo "Applying Longhorn release $LONGHORN_RELEASE..." +echo "Using Longhorn release URL: $LONGHORN_RELEASE_URL" + +kubectl apply -f $LONGHORN_RELEASE_URL + +# Wait for all pods in longhorn-system namespace to be ready +echo "Waiting for Longhorn pods to be ready..." +while true; do + not_ready=$(kubectl -n longhorn-system get pods --no-headers 2>/dev/null | grep -vE 'Running|Completed' | wc -l) + total=$(kubectl -n longhorn-system get pods --no-headers 2>/dev/null | wc -l) + if [[ $total -gt 0 && $not_ready -eq 0 ]]; then + echo "All Longhorn pods are ready." + break + fi + sleep 10 +done diff --git a/vagrant/dev/ubuntu/scripts/longhorn_prereqs.sh b/vagrant/dev/ubuntu/scripts/longhorn_prereqs.sh new file mode 100755 index 0000000..9e00ade --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/longhorn_prereqs.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +echo +echo "vagrant longhorn prerequisites" +echo + +ssh-add ~/.ssh/vm*_key +source /home/vagrant/ansible/venv/bin/activate +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible --inventory-file /home/vagrant/ansible/ansible_inventory.ini -m ping vm1,vm2,vm3 +if [ $? -ne 0 ]; then + echo "Ansible ping failed. Please check your Vagrant VMs and network configuration." + exit 1 +fi +echo "Ansible ping successful." + +# Check if there are any pods in the longhorn-system namespace +if kubectl -n longhorn-system get pods --no-headers 2>/dev/null | grep -q '^[^ ]'; then + echo "Pods already exist in the longhorn-system namespace. Skipping installation." + exit 0 +fi + +exit + +echo "Installing Longhorn prerequisites..." + + +# install_longhorn_prereqs.yaml +ANSIBLE_SUPPRESS_INTERPRETER_DISCOVERY_WARNING=1 ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook ~/ansible/install_longhorn_prereqs.yaml --inventory-file /home/vagrant/ansible/ansible_inventory.ini +if [ $? -ne 0 ]; then + echo "Ansible playbook failed. Please check the playbook and your inventory." + exit 1 +fi diff --git a/vagrant/dev/ubuntu/scripts/wait_for_longhorn.sh b/vagrant/dev/ubuntu/scripts/wait_for_longhorn.sh new file mode 100755 index 0000000..b07aa3b --- /dev/null +++ b/vagrant/dev/ubuntu/scripts/wait_for_longhorn.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +echo +echo "wait for longhorn installation" +echo + +ssh-add ~/.ssh/vm*_key +source /home/vagrant/ansible/venv/bin/activate +while true; do + not_ready=$(kubectl -n longhorn-system get pods --no-headers 2>/dev/null | grep -vE 'Running|Completed' | wc -l) + total=$(kubectl -n longhorn-system get pods --no-headers 2>/dev/null | wc -l) + if [[ $total -gt 0 && $not_ready -eq 0 ]]; then + echo "All Longhorn pods are ready." + break + fi + sleep 10 +done