edge-connect-client/internal/adapters/driving/cli/apply.go
Stephan Lo f1ee439c61 refactor: structure core logic by application use cases
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.
2025-10-09 00:00:51 +02:00

216 lines
6.7 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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))
}
}