changed app to use json config for pipeline steps
readme command line usage - to specify pipeline file name readme updated to include reasoning behind the project use native golang sqlite RunScriptCommand named in functionMap removed unused functions removed unused functions run script and pipeline example renamed functions to drop the word script and add pipeline verb
This commit is contained in:
parent
bd7cee720a
commit
924954d0ff
49 changed files with 2059 additions and 101 deletions
225
app/app.go
Normal file
225
app/app.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"headshed/infctl-cli/config"
|
||||
"headshed/infctl-cli/database"
|
||||
)
|
||||
|
||||
type AppState struct {
|
||||
Config config.BaseConfig
|
||||
Customer config.CustomerConfig
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
type PipelineStep struct {
|
||||
Name string
|
||||
Function string // Name of the function to call
|
||||
Params []string // Parameters for the function
|
||||
RetryCount int
|
||||
ShouldAbort bool
|
||||
}
|
||||
|
||||
var functionMap = map[string]func([]string) error{
|
||||
"k8sNamespaceExists": func(params []string) error {
|
||||
if len(params) != 1 {
|
||||
return fmt.Errorf("invalid parameters for k8sNamespaceExists")
|
||||
}
|
||||
return k8sNamespaceExists(params[0])
|
||||
},
|
||||
"RunCommand": func(params []string) error {
|
||||
if len(params) != 1 {
|
||||
return fmt.Errorf("invalid parameters for RunCommand")
|
||||
}
|
||||
return RunCommand(params[0])
|
||||
},
|
||||
// Add more functions as needed
|
||||
}
|
||||
|
||||
func parseStepsFromJSON(filePath string) ([]PipelineStep, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read JSON file: %w", err)
|
||||
}
|
||||
|
||||
var steps []PipelineStep
|
||||
if err := json.Unmarshal(data, &steps); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (app *AppState) ToDoDeployment() []PipelineStep {
|
||||
slog.Info("ToDo deployment is not implemented yet")
|
||||
return []PipelineStep{}
|
||||
}
|
||||
|
||||
func (app *AppState) RunJsonDeployment() []PipelineStep {
|
||||
|
||||
jsonFile := app.Config.DeploymentFile
|
||||
if jsonFile == "" {
|
||||
log.Fatal("no config specified with --deployment-file=<path_to_config_file>")
|
||||
}
|
||||
|
||||
file, err := os.Open(jsonFile)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintf("Failed to open JSON file: %s", err))
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
steps, err := parseStepsFromJSON(jsonFile)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintf("Failed to parse JSON file: %s", err))
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
slog.Info(fmt.Sprintf("🔄 Running step: %s", step.Name))
|
||||
function, exists := functionMap[step.Function]
|
||||
if !exists {
|
||||
slog.Error(fmt.Sprintf("Unknown function: %s", step.Function))
|
||||
continue
|
||||
}
|
||||
|
||||
err := function(step.Params)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintf("❌ Step failed: %s, error: %v", step.Name, err))
|
||||
if step.ShouldAbort {
|
||||
log.Fatalf("🚨Critical failure at step: %s", step.Name)
|
||||
}
|
||||
} else {
|
||||
slog.Info(fmt.Sprintf("✅ Step completed: %s", step.Name))
|
||||
}
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
func (app *AppState) getPipeline() []PipelineStep {
|
||||
|
||||
switch app.Config.DeploymentType {
|
||||
|
||||
case "api":
|
||||
return app.ToDoDeployment()
|
||||
|
||||
case "json":
|
||||
return app.RunJsonDeployment()
|
||||
|
||||
default:
|
||||
return app.RunJsonDeployment()
|
||||
}
|
||||
}
|
||||
|
||||
func NewAppState(cust config.CustomerConfig, config config.BaseConfig, dbPath string) (*AppState, error) {
|
||||
db, err := database.NewDatabase(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AppState{
|
||||
Config: config,
|
||||
Customer: cust,
|
||||
DB: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (app *AppState) runPipeline(steps []PipelineStep) error {
|
||||
for _, step := range steps {
|
||||
slog.Info(fmt.Sprintf("🔄 Running step: %s\n", step.Name))
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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("🎉 Customer setup complete!"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *AppState) CreatePipeline() error {
|
||||
isNew, err := database.CheckProjectName(app.DB, app.Customer.Project)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check project name: %w", err)
|
||||
}
|
||||
|
||||
if isNew {
|
||||
|
||||
port, err := database.GetNextPortNumber(app.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get next port number: %w", err)
|
||||
}
|
||||
err = database.AddProjectName(app.DB, app.Customer.Project, port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add project name: %w", err)
|
||||
}
|
||||
slog.Info(fmt.Sprintln("Project name added:", app.Customer.Project))
|
||||
fmt.Printf("Port number assigned: %d\n", port)
|
||||
app.Config.Port = port
|
||||
} else {
|
||||
slog.Info(fmt.Sprintln("Project name already exists:", app.Customer.Project))
|
||||
}
|
||||
|
||||
err = app.SetUpNewCustomer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set up new customer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
158
app/app_test.go
Normal file
158
app/app_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"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)
|
||||
}
|
||||
|
||||
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 JSON pipeline file
|
||||
pipeline := []PipelineStep{
|
||||
{Name: "Ensure Namespace Exists", Function: "k8sNamespaceExists", Params: []string{"test-namespace"}, 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.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)
|
||||
}
|
||||
}
|
||||
85
app/k8s.go
Normal file
85
app/k8s.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func k8sNamespaceExists(project string) error {
|
||||
|
||||
commandString := "kubectl get ns " + project
|
||||
|
||||
cmd := exec.Command("sh", "-c", commandString)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
|
||||
if strings.Contains(stderr.String(), "not found") {
|
||||
err := k8sCreateNamespace(project)
|
||||
if err != nil {
|
||||
slog.Error(fmt.Sprintf("Failed to create namespace: %s", project))
|
||||
return fmt.Errorf("failed to create namespace: %w", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
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 kubectl command: %w", err)
|
||||
}
|
||||
}
|
||||
output := stdout.String()
|
||||
if !strings.Contains(output, project) {
|
||||
return fmt.Errorf("namespace %s does not exist", project)
|
||||
}
|
||||
slog.Info(fmt.Sprintf("k8sNamespaceExists nothing to do - project: %s eists ...", project))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func k8sCreateNamespace(project string) error {
|
||||
slog.Info(fmt.Sprintf("in k8sCreateNamespace with project: %s", project))
|
||||
commandString := "kubectl create ns " + project
|
||||
cmd := exec.Command("sh", "-c", commandString)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
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 kubectl command: %w", err)
|
||||
}
|
||||
output := stdout.String()
|
||||
if !strings.Contains(output, project) {
|
||||
return fmt.Errorf("failed to create namespace %s", project)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunCommand(command string) error {
|
||||
slog.Debug(fmt.Sprintf("🐞 Running script command: %s", command))
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
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)
|
||||
}
|
||||
output := stdout.String()
|
||||
slog.Debug(fmt.Sprintf("RunCommand command executed successfully: %s", command))
|
||||
slog.Debug(fmt.Sprintf("RunCommand command output: %s", output))
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue