diff --git a/.gitignore b/.gitignore index 34f8004..644c688 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ 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 764fbc7..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,28 +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 [-f|--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)) - os.Exit(1) + 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)) @@ -91,134 +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 - } - - 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 9deb6ea..2940e16 100644 --- a/app/k8s.go +++ b/app/k8s.go @@ -1,7 +1,6 @@ package app import ( - "bufio" "bytes" "fmt" "log/slog" @@ -64,56 +63,3 @@ 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) - - 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) - } - - // 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\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 -} 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 e8b82d4..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 71f2b4b..2a2977c 100644 --- a/config/base.go +++ b/config/base.go @@ -7,41 +7,30 @@ import ( "os" ) -const Version = "v0.0.4" +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 { @@ -50,17 +39,20 @@ func ReadBaseConfig(path string) (BaseConfig, error) { os.Exit(0) } - // Handle version flags if *versionFlag || *vFlag { fmt.Println("infctl-cli version:", Version) os.Exit(0) } +} - var config BaseConfig - if *deploymentFileShorthand != "" { - config.DeploymentFile = *deploymentFileShorthand - } else if *deploymentFile != "" { - config.DeploymentFile = *deploymentFile +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) @@ -69,9 +61,57 @@ func ReadBaseConfig(path string) (BaseConfig, error) { } 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 9fd2bee..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 customer configfile: %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 af328d5..239a360 100755 --- a/install.sh +++ b/install.sh @@ -25,7 +25,7 @@ case "$OS" in esac # Construct the download URL -VERSION="v0.0.4" +VERSION="v0.0.5" BINARY_URL="https://codeberg.org/headshed/infctl-cli/releases/download/$VERSION/infctl-$OS-$ARCH" # Download the binary 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 index d369347..4839fb1 100644 --- a/pipelines/dev/failing.json +++ b/pipelines/dev/failing.json @@ -1,7 +1,6 @@ [ - { - "name": "Create Vagrant nodes", + "name": "run a failing job", "function": "RunCommand", "params": [ "./scripts/failue.sh" @@ -9,7 +8,6 @@ "retryCount": 0, "shouldAbort": true }, - { "name": "Configure Vagrant K3s", "function": "RunCommand", @@ -19,8 +17,6 @@ "retryCount": 0, "shouldAbort": true }, - - { "name": "Create Vagrant workstation", "function": "RunCommand", @@ -30,4 +26,4 @@ "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/scripts/failue.sh b/scripts/failue.sh index 01a9581..238532a 100755 --- a/scripts/failue.sh +++ b/scripts/failue.sh @@ -1,24 +1,21 @@ #!/usr/bin/env bash +# sleep 5 echo "crash" -sleep 1 +# sleep 1 echo "bang" -sleep 2 +# sleep 2 echo "wallop" -echo -echo -echo +# sleep 1 echo "Houston, we have a problem" -echo -echo -echo +sleep 1 exit 1 diff --git a/scripts/success.sh b/scripts/success.sh new file mode 100755 index 0000000..b8883e9 --- /dev/null +++ b/scripts/success.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# sleep 5 + +echo "bish" + +# sleep 1 + +echo "bash" + +# sleep 2 + +echo "bosh" + +# sleep 1 + +echo "lovely jubbly" + +sleep 1 + +exit 0 diff --git a/vagrant/dev/ubuntu/ansible/provision_workstation.sh b/vagrant/dev/ubuntu/ansible/provision_workstation.sh index ac31459..1077193 100644 --- a/vagrant/dev/ubuntu/ansible/provision_workstation.sh +++ b/vagrant/dev/ubuntu/ansible/provision_workstation.sh @@ -106,7 +106,7 @@ if ! grep -qF "$BLOCK_START" "$BASHRC"; then eval `ssh-agent -s` ssh-add ~/machines/*/virtualbox/private_key ssh-add -L -source ~/vagrant/.envrc +source /vagrant/.envrc EOF else echo "Provisioning block already present in $BASHRC" 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/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/pipelines/vagrant-ingress.json b/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json index 78e6720..837242f 100644 --- a/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json +++ b/vagrant/dev/ubuntu/pipelines/vagrant-ingress.json @@ -16,14 +16,5 @@ ], "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/scripts/check_install_infctl.sh b/vagrant/dev/ubuntu/scripts/check_install_infctl.sh index fb7f66f..d4eb497 100755 --- a/vagrant/dev/ubuntu/scripts/check_install_infctl.sh +++ b/vagrant/dev/ubuntu/scripts/check_install_infctl.sh @@ -14,21 +14,6 @@ then install_infctl fi -# base.json.example config.json.example - -# https://codeberg.org/headshed/infctl-cli/raw/branch/main/base.json.example - -# https://codeberg.org/headshed/infctl-cli/raw/branch/main/config.json.example - -if [ ! -f "base.json" ]; then - echo "base.json not found in home directory, downloading..." - curl -o "base.json" https://codeberg.org/headshed/infctl-cli/raw/branch/main/base.json.example -fi - -if [ ! -f "config.json" ]; then - echo "config.json not found in home directory, downloading..." - curl -o "config.json" https://codeberg.org/headshed/infctl-cli/raw/branch/main/config.json.example -fi