Restructures the internal business logic from a generic `services` package to a use-case-driven design under `internal/application`. Each primary function of the application (`app`, `instance`, `cloudlet`, `apply`) now resides in its own package. This clarifies the architecture and makes it easier to navigate and extend. - Moved service implementations to `internal/application/<usecase>/`. - Kept ports and domain models in `internal/core/`. - Updated `main.go` and CLI adapters to reflect the new paths. - Added missing `RefreshAppInstance` method to satisfy the service interface. - Verified the change with a full build and test run.
216 lines
6.7 KiB
Go
216 lines
6.7 KiB
Go
// ABOUTME: CLI command for declarative deployment of EdgeConnect applications from YAML configuration
|
||
// ABOUTME: Integrates config parser, deployment planner, and resource manager for complete deployment workflow
|
||
package cli
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/driven/edgeconnect"
|
||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/application/apply"
|
||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
var (
|
||
configFile string
|
||
dryRun bool
|
||
autoApprove bool
|
||
)
|
||
|
||
var applyCmd = &cobra.Command{
|
||
Use: "apply",
|
||
Short: "Deploy EdgeConnect applications from configuration files",
|
||
Long: `Deploy EdgeConnect applications and their instances from YAML configuration files.
|
||
This command reads a configuration file, analyzes the current state, and applies
|
||
the necessary changes to deploy your applications across multiple cloudlets.`,
|
||
Run: func(cmd *cobra.Command, args []string) {
|
||
if configFile == "" {
|
||
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
||
if err := cmd.Usage(); err != nil {
|
||
fmt.Fprintf(os.Stderr, "Failed to display usage: %v\n", err)
|
||
}
|
||
os.Exit(1)
|
||
}
|
||
|
||
if err := runApply(configFile, dryRun, autoApprove); err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
},
|
||
}
|
||
|
||
func runApply(configPath string, isDryRun bool, autoApprove bool) error {
|
||
// Step 1: Validate and resolve config file path
|
||
absPath, err := filepath.Abs(configPath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to resolve config file path: %w", err)
|
||
}
|
||
|
||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||
return fmt.Errorf("configuration file not found: %s", absPath)
|
||
}
|
||
|
||
fmt.Printf("📄 Loading configuration from: %s\n", absPath)
|
||
|
||
// Step 2: Parse and validate configuration
|
||
parser := config.NewParser()
|
||
cfg, manifestContent, err := parser.ParseFile(absPath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse configuration: %w", err)
|
||
}
|
||
|
||
if err := parser.Validate(cfg); err != nil {
|
||
return fmt.Errorf("configuration validation failed: %w", err)
|
||
}
|
||
|
||
fmt.Printf("✅ Configuration loaded successfully: %s\n", cfg.Metadata.Name)
|
||
|
||
// Step 3: Create EdgeConnect client
|
||
baseURL := getEnvOrDefault("EDGEXR_BASE_URL", "https://hub.apps.edge.platform.mg3.mdb.osc.live")
|
||
token := getEnvOrDefault("EDGEXR_TOKEN", "")
|
||
username := getEnvOrDefault("EDGEXR_USERNAME", "")
|
||
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
|
||
|
||
var client *edgeconnect.Client
|
||
|
||
if token != "" {
|
||
fmt.Println("🔐 Using Bearer token authentication")
|
||
client = edgeconnect.NewClient(baseURL,
|
||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||
edgeconnect.WithAuthProvider(edgeconnect.NewStaticTokenProvider(token)),
|
||
edgeconnect.WithLogger(log.Default()),
|
||
)
|
||
} else if username != "" && password != "" {
|
||
fmt.Println("🔐 Using username/password authentication")
|
||
client = edgeconnect.NewClientWithCredentials(baseURL, username, password,
|
||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||
edgeconnect.WithLogger(log.Default()),
|
||
)
|
||
} else {
|
||
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
|
||
}
|
||
|
||
// Step 4: Create deployment planner
|
||
planner := apply.NewPlanner(client, client)
|
||
|
||
// Step 5: Generate deployment plan
|
||
fmt.Println("🔍 Analyzing current state and generating deployment plan...")
|
||
|
||
planOptions := apply.DefaultPlanOptions()
|
||
planOptions.DryRun = isDryRun
|
||
|
||
result, err := planner.PlanWithOptions(context.Background(), cfg, planOptions)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to generate deployment plan: %w", err)
|
||
}
|
||
|
||
// Step 6: Display plan summary
|
||
fmt.Println("\n📋 Deployment Plan:")
|
||
fmt.Println(strings.Repeat("=", 50))
|
||
fmt.Println(result.Plan.Summary)
|
||
fmt.Println(strings.Repeat("=", 50))
|
||
|
||
// Display warnings if any
|
||
if len(result.Warnings) > 0 {
|
||
fmt.Println("\n⚠️ Warnings:")
|
||
for _, warning := range result.Warnings {
|
||
fmt.Printf(" • %s\n", warning)
|
||
}
|
||
}
|
||
|
||
// Step 7: If dry-run, stop here
|
||
if isDryRun {
|
||
fmt.Println("\n🔍 Dry-run complete. No changes were made.")
|
||
return nil
|
||
}
|
||
|
||
// Step 8: Confirm deployment (in non-dry-run mode)
|
||
if result.Plan.TotalActions == 0 {
|
||
fmt.Println("\n✅ No changes needed. Resources are already in desired state.")
|
||
return nil
|
||
}
|
||
|
||
fmt.Printf("\nThis will perform %d actions. Estimated time: %v\n",
|
||
result.Plan.TotalActions, result.Plan.EstimatedDuration)
|
||
|
||
if !autoApprove && !confirmDeployment() {
|
||
fmt.Println("Deployment cancelled.")
|
||
return nil
|
||
}
|
||
|
||
// Step 9: Execute deployment
|
||
fmt.Println("\n🚀 Starting deployment...")
|
||
|
||
manager := apply.NewResourceManager(client, client, apply.WithLogger(log.Default()))
|
||
deployResult, err := manager.ApplyDeployment(context.Background(), result.Plan, cfg, manifestContent)
|
||
if err != nil {
|
||
return fmt.Errorf("deployment failed: %w", err)
|
||
}
|
||
|
||
// Step 10: Display results
|
||
if deployResult.Success {
|
||
fmt.Printf("\n✅ Deployment completed successfully in %v\n", deployResult.Duration)
|
||
if len(deployResult.CompletedActions) > 0 {
|
||
fmt.Println("\nCompleted actions:")
|
||
for _, action := range deployResult.CompletedActions {
|
||
fmt.Printf(" ✅ %s %s\n", action.Type, action.Target)
|
||
}
|
||
}
|
||
} else {
|
||
fmt.Printf("\n❌ Deployment failed after %v\n", deployResult.Duration)
|
||
if deployResult.Error != nil {
|
||
fmt.Printf("Error: %v\n", deployResult.Error)
|
||
}
|
||
if len(deployResult.FailedActions) > 0 {
|
||
fmt.Println("\nFailed actions:")
|
||
for _, action := range deployResult.FailedActions {
|
||
fmt.Printf(" ❌ %s %s: %v\n", action.Type, action.Target, action.Error)
|
||
}
|
||
}
|
||
return fmt.Errorf("deployment failed with %d failed actions", len(deployResult.FailedActions))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func confirmDeployment() bool {
|
||
fmt.Print("Do you want to proceed? (yes/no): ")
|
||
var response string
|
||
if _, err := fmt.Scanln(&response); err != nil {
|
||
fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
|
||
return false
|
||
}
|
||
|
||
switch response {
|
||
case "yes", "y", "YES", "Y":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func getEnvOrDefault(key, defaultValue string) string {
|
||
if value := os.Getenv(key); value != "" {
|
||
return value
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
func init() {
|
||
rootCmd.AddCommand(applyCmd)
|
||
|
||
applyCmd.Flags().StringVarP(&configFile, "file", "f", "", "configuration file path (required)")
|
||
applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without applying them")
|
||
applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan")
|
||
|
||
if err := applyCmd.MarkFlagRequired("file"); err != nil {
|
||
panic(fmt.Sprintf("Failed to mark 'file' flag as required: %v", err))
|
||
}
|
||
}
|