Merge branch 'feature/enhanced-configuration'
This commit is contained in:
commit
c23d2aef9f
34 changed files with 790 additions and 643 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -23,3 +23,4 @@ scripts/ansible_inventory.ini
|
||||||
vagrant/dev/ubuntu/ansible/ansible_inventory.ini
|
vagrant/dev/ubuntu/ansible/ansible_inventory.ini
|
||||||
*.cast
|
*.cast
|
||||||
vagrant/dev/ubuntu/certs/
|
vagrant/dev/ubuntu/certs/
|
||||||
|
vagrant/dev/ubuntu/config-dev
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -145,9 +145,9 @@ Key configuration options:
|
||||||
- `env`: Environment file path
|
- `env`: Environment file path
|
||||||
- `preview_path`: Path for preview functionality
|
- `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
|
```bash
|
||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
|
|
@ -155,7 +155,7 @@ cp config.json.example config.json
|
||||||
|
|
||||||
Key configuration options:
|
Key configuration options:
|
||||||
- `project`: Project name/identifier (used as Kubernetes namespace)
|
- `project`: Project name/identifier (used as Kubernetes namespace)
|
||||||
- `customer_directory`: Customer-specific directory
|
- `project_directory`: Project-specific directory
|
||||||
- `ui_url`: UI service URL
|
- `ui_url`: UI service URL
|
||||||
- `static_url`: Static content URL
|
- `static_url`: Static content URL
|
||||||
- `port`: Service port
|
- `port`: Service port
|
||||||
|
|
@ -186,7 +186,7 @@ Run the CLI by providing a path to your pipeline JSON file:
|
||||||
|
|
||||||
The tool will automatically:
|
The tool will automatically:
|
||||||
|
|
||||||
1. Load base and customer configurations
|
1. Load base and project configurations
|
||||||
2. Initialize SQLite database for state management
|
2. Initialize SQLite database for state management
|
||||||
3. Execute the deployment pipeline defined in your JSON file
|
3. Execute the deployment pipeline defined in your JSON file
|
||||||
4. Run scripts from the `scripts/` directory
|
4. Run scripts from the `scripts/` directory
|
||||||
|
|
@ -292,13 +292,13 @@ infctl-cli/
|
||||||
├── main.go # Application entry point
|
├── main.go # Application entry point
|
||||||
├── go.mod # Go module definition
|
├── go.mod # Go module definition
|
||||||
├── base.json.example # Base configuration template
|
├── base.json.example # Base configuration template
|
||||||
├── config.json.example # Customer configuration template
|
├── config.json.example # Project configuration template
|
||||||
├── app/ # Core application logic
|
├── app/ # Core application logic
|
||||||
│ ├── app.go # Pipeline orchestration and state management
|
│ ├── app.go # Pipeline orchestration and state management
|
||||||
│ └── k8s.go # Kubernetes operations (kubectl, kustomize)
|
│ └── k8s.go # Kubernetes operations (kubectl, kustomize)
|
||||||
├── config/ # Configuration management
|
├── config/ # Configuration management
|
||||||
│ ├── base.go # Base configuration handling
|
│ ├── base.go # Base configuration handling
|
||||||
│ └── customer.go # Customer configuration handling
|
│ └── project.go # Project configuration handling
|
||||||
├── database/ # SQLite database operations
|
├── database/ # SQLite database operations
|
||||||
├── scripts/ # Shell scripts executed by the CLI
|
├── scripts/ # Shell scripts executed by the CLI
|
||||||
│ ├── install_*.sh # Infrastructure installation scripts
|
│ ├── install_*.sh # Infrastructure installation scripts
|
||||||
|
|
|
||||||
217
app/app.go
217
app/app.go
|
|
@ -1,22 +1,21 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"headshed/infctl-cli/config"
|
"headshed/infctl-cli/config"
|
||||||
"headshed/infctl-cli/database"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppState struct {
|
type AppState struct {
|
||||||
Config config.BaseConfig
|
Config config.BaseConfig
|
||||||
Customer config.CustomerConfig
|
Project config.ProjectConfig
|
||||||
DB *sql.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PipelineStep struct {
|
type PipelineStep struct {
|
||||||
|
|
@ -62,28 +61,31 @@ func (app *AppState) ToDoDeployment() []PipelineStep {
|
||||||
return []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 == "" {
|
if jsonFile == "" {
|
||||||
log.Fatal("no config specified with [-f|--deployment-file]=<path_to_config_file>")
|
return fmt.Errorf("no config specified with [-f|-deployment-file]=<path_to_config_file> => for all options see help with -h")
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(jsonFile)
|
file, err := os.Open(jsonFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(fmt.Sprintf("Failed to open JSON file: %s", err))
|
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()
|
defer file.Close()
|
||||||
|
|
||||||
|
// fmt.Printf("jsonFile is : %s\n", jsonFile)
|
||||||
|
slog.Info(fmt.Sprintf("Using jsonFile: %s", jsonFile))
|
||||||
steps, err := parseStepsFromJSON(jsonFile)
|
steps, err := parseStepsFromJSON(jsonFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(fmt.Sprintf("Failed to parse JSON file: %s", err))
|
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 {
|
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]
|
function, exists := functionMap[step.Function]
|
||||||
if !exists {
|
if !exists {
|
||||||
slog.Error(fmt.Sprintf("Unknown function: %s", step.Function))
|
slog.Error(fmt.Sprintf("Unknown function: %s", step.Function))
|
||||||
|
|
@ -91,134 +93,121 @@ func (app *AppState) RunJsonDeployment() []PipelineStep {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := function(step.Params)
|
err := function(step.Params)
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
if step.ShouldAbort {
|
||||||
log.Fatalf("🚨Critical failure at step: %s", step.Name)
|
return fmt.Errorf("critical failure at step: %s", step.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
slog.Info(fmt.Sprintf("✅ Step completed: %s", step.Name))
|
slog.Info(fmt.Sprintf("✅ Step completed: %s", step.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return steps
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *AppState) getPipeline() []PipelineStep {
|
func (app *AppState) getPipeline() error {
|
||||||
|
switch app.Project.DeploymentMode {
|
||||||
switch app.Config.DeploymentType {
|
|
||||||
|
|
||||||
case "api":
|
case "api":
|
||||||
return app.ToDoDeployment()
|
return fmt.Errorf("api mode is not yet implemented")
|
||||||
|
|
||||||
case "json":
|
case "json":
|
||||||
return app.RunJsonDeployment()
|
return app.RunJsonDeployment()
|
||||||
|
|
||||||
default:
|
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) {
|
func NewAppState(cust config.ProjectConfig, config config.BaseConfig) (*AppState, error) {
|
||||||
db, err := database.NewDatabase(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AppState{
|
return &AppState{
|
||||||
Config: config,
|
Config: config,
|
||||||
Customer: cust,
|
Project: cust,
|
||||||
DB: db,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *AppState) runPipeline(steps []PipelineStep) error {
|
func RunCommand(command string) error {
|
||||||
for _, step := range steps {
|
slog.Debug(fmt.Sprintf("🐞 Running command: %s", command))
|
||||||
slog.Info(fmt.Sprintf("🔄 Running step: %s\n", step.Name))
|
cmd := exec.Command("sh", "-c", command)
|
||||||
|
|
||||||
// Look up the function in the functionMap
|
var stdout, stderr bytes.Buffer
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the function with the provided parameters
|
// Get pipes for real-time reading
|
||||||
err := function(step.Params)
|
stdoutPipe, err := cmd.StdoutPipe()
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check project name: %w", err)
|
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
stderrPipe, err := cmd.StderrPipe()
|
||||||
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()
|
|
||||||
if err != nil {
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
138
app/app_test.go
138
app/app_test.go
|
|
@ -2,129 +2,31 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math/rand"
|
"headshed/infctl-cli/config"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"headshed/infctl-cli/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
// Test only pipeline execution and shell command running
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunPipeline(t *testing.T) {
|
func TestRunPipeline(t *testing.T) {
|
||||||
// Create a temporary directory for test assets
|
|
||||||
tempDir, err := os.MkdirTemp("", "smoke-test")
|
tempDir, err := os.MkdirTemp("", "smoke-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create temp directory: %v", err)
|
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)
|
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
|
// Create a test JSON pipeline file
|
||||||
pipeline := []PipelineStep{
|
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")
|
pipelineFile := filepath.Join(tempDir, "pipeline.json")
|
||||||
pipelineData, err := json.Marshal(pipeline)
|
pipelineData, err := json.Marshal(pipeline)
|
||||||
|
|
@ -137,22 +39,20 @@ func TestK3DNamespaceCreation(t *testing.T) {
|
||||||
|
|
||||||
// Set up AppState
|
// Set up AppState
|
||||||
app := &AppState{
|
app := &AppState{
|
||||||
Config: config.BaseConfig{
|
Config: config.BaseConfig{},
|
||||||
|
Project: config.ProjectConfig{
|
||||||
DeploymentFile: pipelineFile,
|
DeploymentFile: pipelineFile,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the pipeline
|
// Run the pipeline
|
||||||
err = app.runPipeline(pipeline)
|
err = app.RunJsonDeployment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Pipeline execution failed: %v", err)
|
t.Errorf("Expected no error, got: %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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
|
||||||
54
app/k8s.go
54
app/k8s.go
|
|
@ -1,7 +1,6 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
@ -64,56 +63,3 @@ func k8sCreateNamespace(project string) error {
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
6
build.sh
6
build.sh
|
|
@ -5,6 +5,12 @@ mkdir -p bin
|
||||||
echo "Building for Linux..."
|
echo "Building for Linux..."
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/infctl-linux-amd64
|
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..."
|
echo "Building for Windows..."
|
||||||
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/infctl-windows-amd64.exe
|
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o bin/infctl-windows-amd64.exe
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
112
config/base.go
112
config/base.go
|
|
@ -7,41 +7,30 @@ import (
|
||||||
"os"
|
"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 {
|
type BaseConfig struct {
|
||||||
ProjectsDirectory string `json:"projects_directory"`
|
RetryDelaySenconds int `json:"retry_delay_seconds"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadBaseConfig(path string) (BaseConfig, error) {
|
// ParseFlags parses all command-line flags and handles help/version flags
|
||||||
|
func ParseFlags() {
|
||||||
deploymentType := os.Getenv("DEPLOYMENT_TYPE")
|
|
||||||
|
|
||||||
deploymentFile := flag.String("deployment-file", "", "path to config file")
|
|
||||||
deploymentFileShorthand := flag.String("f", "", "shorthand for -deployment-file")
|
|
||||||
|
|
||||||
helpFlag := flag.Bool("help", false, "show help")
|
helpFlag := flag.Bool("help", false, "show help")
|
||||||
versionFlag := flag.Bool("version", false, "show version")
|
versionFlag := flag.Bool("version", false, "show version")
|
||||||
vFlag := flag.Bool("v", false, "show version (shorthand)")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
if *helpFlag {
|
if *helpFlag {
|
||||||
|
|
@ -50,17 +39,20 @@ func ReadBaseConfig(path string) (BaseConfig, error) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle version flags
|
|
||||||
if *versionFlag || *vFlag {
|
if *versionFlag || *vFlag {
|
||||||
fmt.Println("infctl-cli version:", Version)
|
fmt.Println("infctl-cli version:", Version)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var config BaseConfig
|
func ReadBaseConfig(path string) (BaseConfig, error) {
|
||||||
if *deploymentFileShorthand != "" {
|
config := BaseConfig{}
|
||||||
config.DeploymentFile = *deploymentFileShorthand
|
|
||||||
} else if *deploymentFile != "" {
|
// If base.json does not exist, create it with default value
|
||||||
config.DeploymentFile = *deploymentFile
|
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)
|
data, err := os.ReadFile(path)
|
||||||
|
|
@ -69,9 +61,57 @@ func ReadBaseConfig(path string) (BaseConfig, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
50
config/base_test.go
Normal file
50
config/base_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
108
config/project.go
Normal file
108
config/project.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
46
config/project_test.go
Normal file
46
config/project_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
15
go.mod
15
go.mod
|
|
@ -1,18 +1,3 @@
|
||||||
module headshed/infctl-cli
|
module headshed/infctl-cli
|
||||||
|
|
||||||
go 1.23.3
|
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
|
|
||||||
)
|
|
||||||
|
|
|
||||||
47
go.sum
47
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=
|
|
||||||
|
|
@ -25,7 +25,7 @@ case "$OS" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Construct the download URL
|
# 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"
|
BINARY_URL="https://codeberg.org/headshed/infctl-cli/releases/download/$VERSION/infctl-$OS-$ARCH"
|
||||||
|
|
||||||
# Download the binary
|
# Download the binary
|
||||||
|
|
|
||||||
119
logger/logger.go
Normal file
119
logger/logger.go
Normal file
|
|
@ -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})
|
||||||
|
}
|
||||||
42
logger/logger_test.go
Normal file
42
logger/logger_test.go
Normal file
|
|
@ -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]))
|
||||||
|
}
|
||||||
|
}
|
||||||
106
main.go
106
main.go
|
|
@ -14,101 +14,61 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"encoding/json"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"headshed/infctl-cli/app"
|
"headshed/infctl-cli/app"
|
||||||
"headshed/infctl-cli/config"
|
"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() {
|
func main() {
|
||||||
|
|
||||||
var levelVar slog.LevelVar
|
config.ParseFlags()
|
||||||
levelVar.Set(slog.LevelDebug)
|
baseConfig, projectConfig, err := config.LoadConfigs()
|
||||||
|
if err != nil {
|
||||||
var logger *slog.Logger
|
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
|
||||||
if os.Getenv("LOG_FORMAT") == "basic" {
|
os.Exit(1)
|
||||||
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}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
if err := run(); err != nil {
|
if err := run(projectConfig, baseConfig); err != nil {
|
||||||
log.Fatalf("Application error: %v", err)
|
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()
|
appState, err := app.NewAppState(projectConfig, baseConfig)
|
||||||
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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize app state: %w", err)
|
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 {
|
// Pretty-print appState as JSON
|
||||||
return fmt.Errorf("failed to create customer project: %w", err)
|
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
|
return nil
|
||||||
|
|
|
||||||
10
main_test.go
Normal file
10
main_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
20
pipelines/dev/carry_on.json
Normal file
20
pipelines/dev/carry_on.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
20
pipelines/dev/do_not_carry_on.json
Normal file
20
pipelines/dev/do_not_carry_on.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[
|
[
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Create Vagrant nodes",
|
"name": "run a failing job",
|
||||||
"function": "RunCommand",
|
"function": "RunCommand",
|
||||||
"params": [
|
"params": [
|
||||||
"./scripts/failue.sh"
|
"./scripts/failue.sh"
|
||||||
|
|
@ -9,7 +8,6 @@
|
||||||
"retryCount": 0,
|
"retryCount": 0,
|
||||||
"shouldAbort": true
|
"shouldAbort": true
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Configure Vagrant K3s",
|
"name": "Configure Vagrant K3s",
|
||||||
"function": "RunCommand",
|
"function": "RunCommand",
|
||||||
|
|
@ -19,8 +17,6 @@
|
||||||
"retryCount": 0,
|
"retryCount": 0,
|
||||||
"shouldAbort": true
|
"shouldAbort": true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Create Vagrant workstation",
|
"name": "Create Vagrant workstation",
|
||||||
"function": "RunCommand",
|
"function": "RunCommand",
|
||||||
|
|
|
||||||
38
pipelines/dev/multiple_pipelines.json
Normal file
38
pipelines/dev/multiple_pipelines.json
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
11
pipelines/dev/retry.json
Normal file
11
pipelines/dev/retry.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "run a failing job",
|
||||||
|
"function": "RunCommand",
|
||||||
|
"params": [
|
||||||
|
"./scripts/failue.sh"
|
||||||
|
],
|
||||||
|
"retryCount": 1,
|
||||||
|
"shouldAbort": true
|
||||||
|
}
|
||||||
|
]
|
||||||
11
pipelines/dev/succeeding.json
Normal file
11
pipelines/dev/succeeding.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "run a successful job",
|
||||||
|
"function": "RunCommand",
|
||||||
|
"params": [
|
||||||
|
"./scripts/success.sh"
|
||||||
|
],
|
||||||
|
"retryCount": 0,
|
||||||
|
"shouldAbort": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# sleep 5
|
||||||
|
|
||||||
echo "crash"
|
echo "crash"
|
||||||
|
|
||||||
sleep 1
|
# sleep 1
|
||||||
|
|
||||||
echo "bang"
|
echo "bang"
|
||||||
|
|
||||||
sleep 2
|
# sleep 2
|
||||||
|
|
||||||
echo "wallop"
|
echo "wallop"
|
||||||
|
|
||||||
echo
|
# sleep 1
|
||||||
echo
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "Houston, we have a problem"
|
echo "Houston, we have a problem"
|
||||||
|
|
||||||
echo
|
sleep 1
|
||||||
echo
|
|
||||||
echo
|
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
21
scripts/success.sh
Executable file
21
scripts/success.sh
Executable file
|
|
@ -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
|
||||||
|
|
@ -106,7 +106,7 @@ if ! grep -qF "$BLOCK_START" "$BASHRC"; then
|
||||||
eval `ssh-agent -s`
|
eval `ssh-agent -s`
|
||||||
ssh-add ~/machines/*/virtualbox/private_key
|
ssh-add ~/machines/*/virtualbox/private_key
|
||||||
ssh-add -L
|
ssh-add -L
|
||||||
source ~/vagrant/.envrc
|
source /vagrant/.envrc
|
||||||
EOF
|
EOF
|
||||||
else
|
else
|
||||||
echo "Provisioning block already present in $BASHRC"
|
echo "Provisioning block already present in $BASHRC"
|
||||||
|
|
|
||||||
34
vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml
Normal file
34
vagrant/dev/ubuntu/k8s/nginx-test/deployment_infmon.yaml
Normal file
|
|
@ -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
|
||||||
7
vagrant/dev/ubuntu/k8s/nginx-test/install.sh
Executable file
7
vagrant/dev/ubuntu/k8s/nginx-test/install.sh
Executable file
|
|
@ -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
|
||||||
|
|
@ -16,14 +16,5 @@
|
||||||
],
|
],
|
||||||
"retryCount": 0,
|
"retryCount": 0,
|
||||||
"shouldAbort": true
|
"shouldAbort": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Wait for Longhorn pods to come up",
|
|
||||||
"function": "RunCommand",
|
|
||||||
"params": [
|
|
||||||
"./scripts/wait_for_longhorn.sh"
|
|
||||||
],
|
|
||||||
"retryCount": 10,
|
|
||||||
"shouldAbort": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -14,21 +14,6 @@ then
|
||||||
install_infctl
|
install_infctl
|
||||||
fi
|
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue