Merge branch 'feature/enhanced-configuration'
This commit is contained in:
commit
c23d2aef9f
34 changed files with 790 additions and 643 deletions
217
app/app.go
217
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]=<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)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
138
app/app_test.go
138
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
|
||||
|
|
|
|||
54
app/k8s.go
54
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue