feat(arch) added hexagonal arch impl done with ai
This commit is contained in:
parent
ce801f30d0
commit
ac4001b6f6
42 changed files with 2220 additions and 1459 deletions
BIN
edge-connect-client
Executable file
BIN
edge-connect-client
Executable file
Binary file not shown.
76
hexagonal-architecture-proposal.md
Normal file
76
hexagonal-architecture-proposal.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Proposal: Refactor to Hexagonal Architecture
|
||||
|
||||
This document proposes a refactoring of the `edge-connect-client` project to a Hexagonal Architecture (also known as Ports and Adapters). This will improve the project's maintainability, testability, and flexibility.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
The current project structure is a mix of concerns. The `cmd` package contains both CLI handling and business logic, the `sdk` package is a client for the EdgeXR API, and the `internal` package contains some business logic and configuration handling. This makes it difficult to test the business logic in isolation and to adapt the application to different use cases.
|
||||
|
||||
## Proposed Hexagonal Architecture
|
||||
|
||||
The hexagonal architecture separates the application's core business logic from the outside world. The core communicates with the outside world through ports (interfaces), which are implemented by adapters.
|
||||
|
||||
Here is the proposed directory structure:
|
||||
|
||||
```
|
||||
.
|
||||
├── cmd/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── core/
|
||||
│ │ ├── domain/
|
||||
│ │ │ ├── app.go
|
||||
│ │ │ └── instance.go
|
||||
│ │ ├── ports/
|
||||
│ │ │ ├── driven/
|
||||
│ │ │ │ ├── app_repository.go
|
||||
│ │ │ │ └── instance_repository.go
|
||||
│ │ │ └── driving/
|
||||
│ │ │ ├── app_service.go
|
||||
│ │ │ └── instance_service.go
|
||||
│ │ └── services/
|
||||
│ │ ├── app_service.go
|
||||
│ │ └── instance_service.go
|
||||
│ └── adapters/
|
||||
│ ├── cli/
|
||||
│ │ ├── app.go
|
||||
│ │ └── instance.go
|
||||
│ └── edgeconnect/
|
||||
│ ├── app.go
|
||||
│ └── instance.go
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
### Core
|
||||
|
||||
* `internal/core/domain`: Contains the core domain objects (e.g., `App`, `AppInstance`). These are plain Go structs with no external dependencies.
|
||||
* `internal/core/ports`: Defines the interfaces for communication with the outside world.
|
||||
* `driving`: Interfaces for the services offered by the application (e.g., `AppService`, `InstanceService`).
|
||||
* `driven`: Interfaces for the services the application needs (e.g., `AppRepository`, `InstanceRepository`).
|
||||
* `internal/core/services`: Implements the `driving` port interfaces. This is where the core business logic resides.
|
||||
|
||||
### Adapters
|
||||
|
||||
* `internal/adapters/cli`: The CLI adapter. It implements the user interface and calls the `driving` ports of the core.
|
||||
* `internal/adapters/edgeconnect`: The EdgeXR API adapter. It implements the `driven` port interfaces and communicates with the EdgeXR API.
|
||||
|
||||
### `cmd`
|
||||
|
||||
* `cmd/main.go`: The main entry point of the application. It is responsible for wiring everything together: creating the adapters, injecting them into the core services, and starting the CLI.
|
||||
|
||||
## Refactoring Steps
|
||||
|
||||
1. **Define domain models:** Create the domain models in `internal/core/domain`.
|
||||
2. **Define ports:** Define the `driving` and `driven` port interfaces in `internal/core/ports`.
|
||||
3. **Implement core services:** Implement the core business logic in `internal/core/services`.
|
||||
4. **Create adapters:**
|
||||
* Move the existing CLI code from `cmd` to `internal/adapters/cli` and adapt it to call the core services.
|
||||
* Move the existing `sdk` code to `internal/adapters/edgeconnect` and adapt it to implement the repository interfaces.
|
||||
5. **Wire everything together:** Update `cmd/main.go` to create the adapters and inject them into the core services.
|
||||
|
||||
## Benefits
|
||||
|
||||
* **Improved Testability:** The core business logic can be tested in isolation, without the need for the CLI framework or the EdgeXR API.
|
||||
* **Increased Flexibility:** The application can be easily adapted to different use cases by creating new adapters. For example, we could add a REST API by creating a new adapter.
|
||||
* **Better Separation of Concerns:** The hexagonal architecture enforces a clear separation between the business logic and the infrastructure, making the code easier to understand and maintain.
|
||||
|
|
@ -1,16 +1,12 @@
|
|||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -20,58 +16,6 @@ var (
|
|||
region string
|
||||
)
|
||||
|
||||
func validateBaseURL(baseURL string) error {
|
||||
url, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding error '%s'", err.Error())
|
||||
}
|
||||
|
||||
if url.Scheme == "" {
|
||||
return fmt.Errorf("schema should be set (add https://)")
|
||||
}
|
||||
|
||||
if len(url.User.Username()) > 0 {
|
||||
return fmt.Errorf("user and or password should not be set")
|
||||
}
|
||||
|
||||
if !(url.Path == "" || url.Path == "/") {
|
||||
return fmt.Errorf("should not contain any path '%s'", url.Path)
|
||||
}
|
||||
|
||||
if len(url.Query()) > 0 {
|
||||
return fmt.Errorf("should not contain any queries '%s'", url.RawQuery)
|
||||
}
|
||||
|
||||
if len(url.Fragment) > 0 {
|
||||
return fmt.Errorf("should not contain any fragment '%s'", url.Fragment)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSDKClient() *edgeconnect.Client {
|
||||
baseURL := viper.GetString("base_url")
|
||||
username := viper.GetString("username")
|
||||
password := viper.GetString("password")
|
||||
|
||||
err := validateBaseURL(baseURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing baseURL: '%s' with error: %s\n", baseURL, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if username != "" && password != "" {
|
||||
return edgeconnect.NewClientWithCredentials(baseURL, username, password,
|
||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback to no auth for now - in production should require auth
|
||||
return edgeconnect.NewClient(baseURL,
|
||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||
)
|
||||
}
|
||||
|
||||
var appCmd = &cobra.Command{
|
||||
Use: "app",
|
||||
Short: "Manage Edge Connect applications",
|
||||
|
|
@ -82,19 +26,15 @@ var createAppCmd = &cobra.Command{
|
|||
Use: "create",
|
||||
Short: "Create a new Edge Connect application",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
input := &edgeconnect.NewAppInput{
|
||||
Region: region,
|
||||
App: edgeconnect.App{
|
||||
Key: edgeconnect.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
},
|
||||
app := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
},
|
||||
}
|
||||
|
||||
err := c.CreateApp(context.Background(), input)
|
||||
err := appService.CreateApp(context.Background(), region, app)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating app: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -107,14 +47,13 @@ var showAppCmd = &cobra.Command{
|
|||
Use: "show",
|
||||
Short: "Show details of an Edge Connect application",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
}
|
||||
|
||||
app, err := c.ShowApp(context.Background(), appKey, region)
|
||||
app, err := appService.ShowApp(context.Background(), region, appKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error showing app: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -127,14 +66,13 @@ var listAppsCmd = &cobra.Command{
|
|||
Use: "list",
|
||||
Short: "List Edge Connect applications",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
}
|
||||
|
||||
apps, err := c.ShowApps(context.Background(), appKey, region)
|
||||
apps, err := appService.ShowApps(context.Background(), region, appKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error listing apps: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -150,14 +88,13 @@ var deleteAppCmd = &cobra.Command{
|
|||
Use: "delete",
|
||||
Short: "Delete an Edge Connect application",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
}
|
||||
|
||||
err := c.DeleteApp(context.Background(), appKey, region)
|
||||
err := appService.DeleteApp(context.Background(), region, appKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting app: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -185,4 +122,4 @@ func init() {
|
|||
for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} {
|
||||
cmd.MarkFlagRequired("name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
// 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 cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/apply"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -68,10 +71,32 @@ func runApply(configPath string, isDryRun bool, autoApprove bool) error {
|
|||
fmt.Printf("✅ Configuration loaded successfully: %s\n", cfg.Metadata.Name)
|
||||
|
||||
// Step 3: Create EdgeConnect client
|
||||
client := newSDKClient()
|
||||
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)
|
||||
planner := apply.NewPlanner(client, client)
|
||||
|
||||
// Step 5: Generate deployment plan
|
||||
fmt.Println("🔍 Analyzing current state and generating deployment plan...")
|
||||
|
|
@ -121,7 +146,7 @@ func runApply(configPath string, isDryRun bool, autoApprove bool) error {
|
|||
// Step 9: Execute deployment
|
||||
fmt.Println("\n🚀 Starting deployment...")
|
||||
|
||||
manager := apply.NewResourceManager(client, apply.WithLogger(log.Default()))
|
||||
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)
|
||||
|
|
@ -166,6 +191,13 @@ func confirmDeployment() bool {
|
|||
}
|
||||
}
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(applyCmd)
|
||||
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -26,30 +26,26 @@ var createInstanceCmd = &cobra.Command{
|
|||
Use: "create",
|
||||
Short: "Create a new Edge Connect application instance",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
input := &edgeconnect.NewAppInstanceInput{
|
||||
Region: region,
|
||||
AppInst: edgeconnect.AppInstance{
|
||||
Key: edgeconnect.AppInstanceKey{
|
||||
Organization: organization,
|
||||
Name: instanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
Organization: cloudletOrg,
|
||||
Name: cloudletName,
|
||||
},
|
||||
},
|
||||
AppKey: edgeconnect.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
},
|
||||
Flavor: edgeconnect.Flavor{
|
||||
Name: flavorName,
|
||||
appInst := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: organization,
|
||||
Name: instanceName,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: cloudletOrg,
|
||||
Name: cloudletName,
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: organization,
|
||||
Name: appName,
|
||||
Version: appVersion,
|
||||
},
|
||||
Flavor: domain.Flavor{
|
||||
Name: flavorName,
|
||||
},
|
||||
}
|
||||
|
||||
err := c.CreateAppInstance(context.Background(), input)
|
||||
err := instanceService.CreateAppInstance(context.Background(), region, appInst)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating app instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -62,17 +58,16 @@ var showInstanceCmd = &cobra.Command{
|
|||
Use: "show",
|
||||
Short: "Show details of an Edge Connect application instance",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: organization,
|
||||
Name: instanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: cloudletOrg,
|
||||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
instance, err := c.ShowAppInstance(context.Background(), instanceKey, region)
|
||||
instance, err := instanceService.ShowAppInstance(context.Background(), region, instanceKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error showing app instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -85,17 +80,16 @@ var listInstancesCmd = &cobra.Command{
|
|||
Use: "list",
|
||||
Short: "List Edge Connect application instances",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: organization,
|
||||
Name: instanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: cloudletOrg,
|
||||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
instances, err := c.ShowAppInstances(context.Background(), instanceKey, region)
|
||||
instances, err := instanceService.ShowAppInstances(context.Background(), region, instanceKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error listing app instances: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -111,17 +105,16 @@ var deleteInstanceCmd = &cobra.Command{
|
|||
Use: "delete",
|
||||
Short: "Delete an Edge Connect application instance",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
c := newSDKClient()
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: organization,
|
||||
Name: instanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: cloudletOrg,
|
||||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
err := c.DeleteAppInstance(context.Background(), instanceKey, region)
|
||||
err := instanceService.DeleteAppInstance(context.Background(), region, instanceKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting app instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -156,4 +149,4 @@ func init() {
|
|||
createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)")
|
||||
createInstanceCmd.MarkFlagRequired("app")
|
||||
createInstanceCmd.MarkFlagRequired("flavor")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package cmd
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driving"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
|
@ -13,8 +14,27 @@ var (
|
|||
baseURL string
|
||||
username string
|
||||
password string
|
||||
|
||||
appService driving.AppService
|
||||
instanceService driving.AppInstanceService
|
||||
cloudletService driving.CloudletService
|
||||
)
|
||||
|
||||
// SetAppService injects the application service
|
||||
func SetAppService(service driving.AppService) {
|
||||
appService = service
|
||||
}
|
||||
|
||||
// SetInstanceService injects the instance service
|
||||
func SetInstanceService(service driving.AppInstanceService) {
|
||||
instanceService = service
|
||||
}
|
||||
|
||||
// SetCloudletService injects the cloudlet service
|
||||
func SetCloudletService(service driving.CloudletService) {
|
||||
cloudletService = service
|
||||
}
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "edge-connect",
|
||||
|
|
@ -69,4 +89,4 @@ func initConfig() {
|
|||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,13 +9,19 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/internal/http"
|
||||
)
|
||||
|
||||
// CreateAppInstance creates a new application instance in the specified region
|
||||
// Maps to POST /auth/ctrl/CreateAppInst
|
||||
func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInput) error {
|
||||
func (c *Client) CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
transport := c.getTransport()
|
||||
apiAppInst := ToAPIAppInstance(appInst)
|
||||
input := &NewAppInstanceInput{
|
||||
Region: region,
|
||||
AppInst: *apiAppInst,
|
||||
}
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/CreateAppInst"
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
|
|
@ -36,52 +42,55 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
|
|||
|
||||
// ShowAppInstance retrieves a single application instance by key and region
|
||||
// Maps to POST /auth/ctrl/ShowAppInst
|
||||
func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) (AppInstance, error) {
|
||||
func (c *Client) ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
apiAppInstKey := toAPIAppInstanceKey(appInstKey)
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: *apiAppInstKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||
if err != nil {
|
||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||
return nil, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
||||
return nil, fmt.Errorf("app instance %s/%s: %w",
|
||||
appInstKey.Organization, appInstKey.Name, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return AppInstance{}, c.handleErrorResponse(resp, "ShowAppInstance")
|
||||
return nil, c.handleErrorResponse(resp, "ShowAppInstance")
|
||||
}
|
||||
|
||||
// Parse streaming JSON response
|
||||
var appInstances []AppInstance
|
||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||
return nil, fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if len(appInstances) == 0 {
|
||||
return AppInstance{}, fmt.Errorf("app instance %s/%s in region %s: %w",
|
||||
return nil, fmt.Errorf("app instance %s/%s in region %s: %w",
|
||||
appInstKey.Organization, appInstKey.Name, region, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
return appInstances[0], nil
|
||||
domainAppInst := toDomainAppInstance(&appInstances[0])
|
||||
return &domainAppInst, nil
|
||||
}
|
||||
|
||||
// ShowAppInstances retrieves all application instances matching the filter criteria
|
||||
// Maps to POST /auth/ctrl/ShowAppInst
|
||||
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) {
|
||||
func (c *Client) ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
apiAppInstKey := toAPIAppInstanceKey(appInstKey)
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: *apiAppInstKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +106,7 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
|||
|
||||
var appInstances []AppInstance
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return appInstances, nil // Return empty slice for not found
|
||||
return []domain.AppInstance{}, nil // Return empty slice for not found
|
||||
}
|
||||
|
||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||
|
|
@ -105,15 +114,26 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
|||
}
|
||||
|
||||
c.logf("ShowAppInstances: found %d app instances matching criteria", len(appInstances))
|
||||
return appInstances, nil
|
||||
|
||||
domainAppInsts := make([]domain.AppInstance, len(appInstances))
|
||||
for i := range appInstances {
|
||||
domainAppInsts[i] = toDomainAppInstance(&appInstances[i])
|
||||
}
|
||||
|
||||
return domainAppInsts, nil
|
||||
}
|
||||
|
||||
// UpdateAppInstance updates an application instance and then refreshes it
|
||||
// Maps to POST /auth/ctrl/UpdateAppInst
|
||||
func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstanceInput) error {
|
||||
func (c *Client) UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/UpdateAppInst"
|
||||
|
||||
apiAppInst := ToAPIAppInstance(appInst)
|
||||
input := &UpdateAppInstanceInput{
|
||||
Region: region,
|
||||
AppInst: *apiAppInst,
|
||||
}
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||
|
|
@ -132,12 +152,13 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
|
|||
|
||||
// RefreshAppInstance refreshes an application instance's state
|
||||
// Maps to POST /auth/ctrl/RefreshAppInst
|
||||
func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
||||
func (c *Client) RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/RefreshAppInst"
|
||||
|
||||
apiAppInstKey := toAPIAppInstanceKey(appInstKey)
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: *apiAppInstKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -159,12 +180,13 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
|||
|
||||
// DeleteAppInstance removes an application instance from the specified region
|
||||
// Maps to POST /auth/ctrl/DeleteAppInst
|
||||
func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
||||
func (c *Client) DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst"
|
||||
|
||||
apiAppInstKey := toAPIAppInstanceKey(appInstKey)
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: *apiAppInstKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -233,3 +255,55 @@ func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result i
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToAPIAppInstance(appInst *domain.AppInstance) *AppInstance {
|
||||
return &AppInstance{
|
||||
Key: *toAPIAppInstanceKey(appInst.Key),
|
||||
AppKey: *toAPIAppKey(appInst.AppKey),
|
||||
Flavor: toAPIFlavor(appInst.Flavor),
|
||||
State: appInst.State,
|
||||
PowerState: appInst.PowerState,
|
||||
Fields: appInst.Fields,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainAppInstance(appInst *AppInstance) domain.AppInstance {
|
||||
return domain.AppInstance{
|
||||
Key: toDomainAppInstanceKey(appInst.Key),
|
||||
AppKey: toDomainAppKey(appInst.AppKey),
|
||||
Flavor: toDomainFlavor(appInst.Flavor),
|
||||
State: appInst.State,
|
||||
PowerState: appInst.PowerState,
|
||||
Fields: appInst.Fields,
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIAppInstanceKey(key domain.AppInstanceKey) *AppInstanceKey {
|
||||
return &AppInstanceKey{
|
||||
Organization: key.Organization,
|
||||
Name: key.Name,
|
||||
CloudletKey: toAPICloudletKey(key.CloudletKey),
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainAppInstanceKey(key AppInstanceKey) domain.AppInstanceKey {
|
||||
return domain.AppInstanceKey{
|
||||
Organization: key.Organization,
|
||||
Name: key.Name,
|
||||
CloudletKey: toDomainCloudletKey(key.CloudletKey),
|
||||
}
|
||||
}
|
||||
|
||||
func toAPICloudletKey(key domain.CloudletKey) CloudletKey {
|
||||
return CloudletKey{
|
||||
Organization: key.Organization,
|
||||
Name: key.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainCloudletKey(key CloudletKey) domain.CloudletKey {
|
||||
return domain.CloudletKey{
|
||||
Organization: key.Organization,
|
||||
Name: key.Name,
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -86,7 +87,23 @@ func TestCreateAppInstance(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
err := client.CreateAppInstance(ctx, tt.input)
|
||||
domainAppInst := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: tt.input.AppInst.Key.Organization,
|
||||
Name: tt.input.AppInst.Key.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: tt.input.AppInst.Key.CloudletKey.Organization,
|
||||
Name: tt.input.AppInst.Key.CloudletKey.Name,
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: tt.input.AppInst.AppKey.Organization,
|
||||
Name: tt.input.AppInst.AppKey.Name,
|
||||
Version: tt.input.AppInst.AppKey.Version,
|
||||
},
|
||||
Flavor: domain.Flavor{Name: tt.input.AppInst.Flavor.Name},
|
||||
}
|
||||
err := client.CreateAppInstance(ctx, tt.input.Region, domainAppInst)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -164,7 +181,15 @@ func TestShowAppInstance(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
|
||||
domainAppInstKey := domain.AppInstanceKey{
|
||||
Organization: tt.appInstKey.Organization,
|
||||
Name: tt.appInstKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: tt.appInstKey.CloudletKey.Organization,
|
||||
Name: tt.appInstKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.region, domainAppInstKey)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -206,7 +231,8 @@ func TestShowAppInstances(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
|
||||
domainAppInstKey := domain.AppInstanceKey{Organization: "testorg"}
|
||||
appInstances, err := client.ShowAppInstances(ctx, "us-west", domainAppInstKey)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, appInstances, 2)
|
||||
|
|
@ -318,7 +344,24 @@ func TestUpdateAppInstance(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
err := client.UpdateAppInstance(ctx, tt.input)
|
||||
domainAppInst := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: tt.input.AppInst.Key.Organization,
|
||||
Name: tt.input.AppInst.Key.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: tt.input.AppInst.Key.CloudletKey.Organization,
|
||||
Name: tt.input.AppInst.Key.CloudletKey.Name,
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: tt.input.AppInst.AppKey.Organization,
|
||||
Name: tt.input.AppInst.AppKey.Name,
|
||||
Version: tt.input.AppInst.AppKey.Version,
|
||||
},
|
||||
Flavor: domain.Flavor{Name: tt.input.AppInst.Flavor.Name},
|
||||
PowerState: tt.input.AppInst.PowerState,
|
||||
}
|
||||
err := client.UpdateAppInstance(ctx, tt.input.Region, domainAppInst)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -381,7 +424,15 @@ func TestRefreshAppInstance(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.RefreshAppInstance(ctx, tt.appInstKey, tt.region)
|
||||
domainAppInstKey := domain.AppInstanceKey{
|
||||
Organization: tt.appInstKey.Organization,
|
||||
Name: tt.appInstKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: tt.appInstKey.CloudletKey.Organization,
|
||||
Name: tt.appInstKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
err := client.RefreshAppInstance(ctx, tt.region, domainAppInstKey)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -457,7 +508,15 @@ func TestDeleteAppInstance(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.DeleteAppInstance(ctx, tt.appInstKey, tt.region)
|
||||
domainAppInstKey := domain.AppInstanceKey{
|
||||
Organization: tt.appInstKey.Organization,
|
||||
Name: tt.appInstKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: tt.appInstKey.CloudletKey.Organization,
|
||||
Name: tt.appInstKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
err := client.DeleteAppInstance(ctx, tt.region, domainAppInstKey)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -10,7 +10,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -20,10 +21,16 @@ var (
|
|||
|
||||
// CreateApp creates a new application in the specified region
|
||||
// Maps to POST /auth/ctrl/CreateApp
|
||||
func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
||||
func (c *Client) CreateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/CreateApp"
|
||||
|
||||
apiApp := toAPIApp(app)
|
||||
input := &NewAppInput{
|
||||
Region: region,
|
||||
App: *apiApp,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateApp failed: %w", err)
|
||||
|
|
@ -42,52 +49,55 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
|||
|
||||
// ShowApp retrieves a single application by key and region
|
||||
// Maps to POST /auth/ctrl/ShowApp
|
||||
func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App, error) {
|
||||
func (c *Client) ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
|
||||
|
||||
apiAppKey := toAPIAppKey(appKey)
|
||||
filter := AppFilter{
|
||||
App: App{Key: appKey},
|
||||
App: App{Key: *apiAppKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||
return nil, fmt.Errorf("ShowApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
return nil, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return App{}, c.handleErrorResponse(resp, "ShowApp")
|
||||
return nil, c.handleErrorResponse(resp, "ShowApp")
|
||||
}
|
||||
|
||||
// Parse streaming JSON response
|
||||
var apps []App
|
||||
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||
return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err)
|
||||
return nil, fmt.Errorf("ShowApp failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if len(apps) == 0 {
|
||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
return nil, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
return apps[0], nil
|
||||
domainApp := toDomainApp(&apps[0])
|
||||
return &domainApp, nil
|
||||
}
|
||||
|
||||
// ShowApps retrieves all applications matching the filter criteria
|
||||
// Maps to POST /auth/ctrl/ShowApp
|
||||
func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]App, error) {
|
||||
func (c *Client) ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
|
||||
|
||||
apiAppKey := toAPIAppKey(appKey)
|
||||
filter := AppFilter{
|
||||
App: App{Key: appKey},
|
||||
App: App{Key: *apiAppKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +113,7 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
|
|||
|
||||
var apps []App
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return apps, nil // Return empty slice for not found
|
||||
return []domain.App{}, nil // Return empty slice for not found
|
||||
}
|
||||
|
||||
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||
|
|
@ -111,15 +121,27 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
|
|||
}
|
||||
|
||||
c.logf("ShowApps: found %d apps matching criteria", len(apps))
|
||||
return apps, nil
|
||||
|
||||
domainApps := make([]domain.App, len(apps))
|
||||
for i := range apps {
|
||||
domainApps[i] = toDomainApp(&apps[i])
|
||||
}
|
||||
|
||||
return domainApps, nil
|
||||
}
|
||||
|
||||
// UpdateApp updates the definition of an application
|
||||
// Maps to POST /auth/ctrl/UpdateApp
|
||||
func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
||||
func (c *Client) UpdateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/UpdateApp"
|
||||
|
||||
apiApp := toAPIApp(app)
|
||||
input := &UpdateAppInput{
|
||||
Region: region,
|
||||
App: *apiApp,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||
|
|
@ -138,12 +160,13 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
|||
|
||||
// DeleteApp removes an application from the specified region
|
||||
// Maps to POST /auth/ctrl/DeleteApp
|
||||
func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) error {
|
||||
func (c *Client) DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
|
||||
|
||||
apiAppKey := toAPIAppKey(appKey)
|
||||
filter := AppFilter{
|
||||
App: App{Key: appKey},
|
||||
App: App{Key: *apiAppKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -249,3 +272,85 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
|
|||
Body: bodyBytes,
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIApp(app *domain.App) *App {
|
||||
return &App{
|
||||
Key: *toAPIAppKey(app.Key),
|
||||
Deployment: app.Deployment,
|
||||
ImageType: app.ImageType,
|
||||
ImagePath: app.ImagePath,
|
||||
AllowServerless: app.AllowServerless,
|
||||
DefaultFlavor: toAPIFlavor(app.DefaultFlavor),
|
||||
ServerlessConfig: app.ServerlessConfig,
|
||||
DeploymentGenerator: app.DeploymentGenerator,
|
||||
DeploymentManifest: app.DeploymentManifest,
|
||||
RequiredOutboundConnections: toAPISecurityRules(app.RequiredOutboundConnections),
|
||||
Fields: app.Fields,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainApp(app *App) domain.App {
|
||||
return domain.App{
|
||||
Key: toDomainAppKey(app.Key),
|
||||
Deployment: app.Deployment,
|
||||
ImageType: app.ImageType,
|
||||
ImagePath: app.ImagePath,
|
||||
AllowServerless: app.AllowServerless,
|
||||
DefaultFlavor: toDomainFlavor(app.DefaultFlavor),
|
||||
ServerlessConfig: app.ServerlessConfig,
|
||||
DeploymentGenerator: app.DeploymentGenerator,
|
||||
DeploymentManifest: app.DeploymentManifest,
|
||||
RequiredOutboundConnections: ToDomainSecurityRules(app.RequiredOutboundConnections),
|
||||
Fields: app.Fields,
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIAppKey(appKey domain.AppKey) *AppKey {
|
||||
return &AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainAppKey(appKey AppKey) domain.AppKey {
|
||||
return domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIFlavor(flavor domain.Flavor) Flavor {
|
||||
return Flavor{Name: flavor.Name}
|
||||
}
|
||||
|
||||
func toDomainFlavor(flavor Flavor) domain.Flavor {
|
||||
return domain.Flavor{Name: flavor.Name}
|
||||
}
|
||||
|
||||
func toAPISecurityRules(rules []domain.SecurityRule) []SecurityRule {
|
||||
apiRules := make([]SecurityRule, len(rules))
|
||||
for i, r := range rules {
|
||||
apiRules[i] = SecurityRule{
|
||||
PortRangeMax: r.PortRangeMax,
|
||||
PortRangeMin: r.PortRangeMin,
|
||||
Protocol: r.Protocol,
|
||||
RemoteCIDR: r.RemoteCIDR,
|
||||
}
|
||||
}
|
||||
return apiRules
|
||||
}
|
||||
|
||||
func ToDomainSecurityRules(rules []SecurityRule) []domain.SecurityRule {
|
||||
domainRules := make([]domain.SecurityRule, len(rules))
|
||||
for i, r := range rules {
|
||||
domainRules[i] = domain.SecurityRule{
|
||||
PortRangeMax: r.PortRangeMax,
|
||||
PortRangeMin: r.PortRangeMin,
|
||||
Protocol: r.Protocol,
|
||||
RemoteCIDR: r.RemoteCIDR,
|
||||
}
|
||||
}
|
||||
return domainRules
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -79,7 +80,20 @@ func TestCreateApp(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
err := client.CreateApp(ctx, tt.input)
|
||||
domainApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: tt.input.App.Key.Organization,
|
||||
Name: tt.input.App.Key.Name,
|
||||
Version: tt.input.App.Key.Version,
|
||||
},
|
||||
Deployment: tt.input.App.Deployment,
|
||||
ImageType: tt.input.App.ImageType,
|
||||
ImagePath: tt.input.App.ImagePath,
|
||||
DefaultFlavor: domain.Flavor{Name: tt.input.App.DefaultFlavor.Name},
|
||||
ServerlessConfig: tt.input.App.ServerlessConfig,
|
||||
AllowServerless: tt.input.App.AllowServerless,
|
||||
}
|
||||
err := client.CreateApp(ctx, tt.input.Region, domainApp)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -151,7 +165,12 @@ func TestShowApp(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
app, err := client.ShowApp(ctx, tt.appKey, tt.region)
|
||||
domainAppKey := domain.AppKey{
|
||||
Organization: tt.appKey.Organization,
|
||||
Name: tt.appKey.Name,
|
||||
Version: tt.appKey.Version,
|
||||
}
|
||||
app, err := client.ShowApp(ctx, tt.region, domainAppKey)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -193,7 +212,8 @@ func TestShowApps(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
apps, err := client.ShowApps(ctx, AppKey{Organization: "testorg"}, "us-west")
|
||||
domainAppKey := domain.AppKey{Organization: "testorg"}
|
||||
apps, err := client.ShowApps(ctx, "us-west", domainAppKey)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, apps, 2)
|
||||
|
|
@ -289,7 +309,20 @@ func TestUpdateApp(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
err := client.UpdateApp(ctx, tt.input)
|
||||
domainApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: tt.input.App.Key.Organization,
|
||||
Name: tt.input.App.Key.Name,
|
||||
Version: tt.input.App.Key.Version,
|
||||
},
|
||||
Deployment: tt.input.App.Deployment,
|
||||
ImageType: tt.input.App.ImageType,
|
||||
ImagePath: tt.input.App.ImagePath,
|
||||
DefaultFlavor: domain.Flavor{Name: tt.input.App.DefaultFlavor.Name},
|
||||
ServerlessConfig: tt.input.App.ServerlessConfig,
|
||||
AllowServerless: tt.input.App.AllowServerless,
|
||||
}
|
||||
err := client.UpdateApp(ctx, tt.input.Region, domainApp)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -357,7 +390,12 @@ func TestDeleteApp(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.DeleteApp(ctx, tt.appKey, tt.region)
|
||||
domainAppKey := domain.AppKey{
|
||||
Organization: tt.appKey.Organization,
|
||||
Name: tt.appKey.Name,
|
||||
Version: tt.appKey.Version,
|
||||
}
|
||||
err := client.DeleteApp(ctx, tt.region, domainAppKey)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -9,15 +9,22 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/internal/http"
|
||||
)
|
||||
|
||||
// CreateCloudlet creates a new cloudlet in the specified region
|
||||
// Maps to POST /auth/ctrl/CreateCloudlet
|
||||
func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) error {
|
||||
func (c *Client) CreateCloudlet(ctx context.Context, region string, cloudlet *domain.Cloudlet) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/CreateCloudlet"
|
||||
|
||||
apiCloudlet := toAPICloudlet(cloudlet)
|
||||
input := &NewCloudletInput{
|
||||
Region: region,
|
||||
Cloudlet: *apiCloudlet,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||
|
|
@ -36,52 +43,55 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
|
|||
|
||||
// ShowCloudlet retrieves a single cloudlet by key and region
|
||||
// Maps to POST /auth/ctrl/ShowCloudlet
|
||||
func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, region string) (Cloudlet, error) {
|
||||
func (c *Client) ShowCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) (*domain.Cloudlet, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowCloudlet"
|
||||
|
||||
apiCloudletKey := toAPICloudletKey(cloudletKey)
|
||||
filter := CloudletFilter{
|
||||
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||
Cloudlet: Cloudlet{Key: apiCloudletKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||
if err != nil {
|
||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||
return nil, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
return nil, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return Cloudlet{}, c.handleErrorResponse(resp, "ShowCloudlet")
|
||||
return nil, c.handleErrorResponse(resp, "ShowCloudlet")
|
||||
}
|
||||
|
||||
// Parse streaming JSON response
|
||||
var cloudlets []Cloudlet
|
||||
if err := c.parseStreamingCloudletResponse(resp, &cloudlets); err != nil {
|
||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed to parse response: %w", err)
|
||||
return nil, fmt.Errorf("ShowCloudlet failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if len(cloudlets) == 0 {
|
||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
return nil, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||
}
|
||||
|
||||
return cloudlets[0], nil
|
||||
domainCloudlet := toDomainCloudlet(&cloudlets[0])
|
||||
return &domainCloudlet, nil
|
||||
}
|
||||
|
||||
// ShowCloudlets retrieves all cloudlets matching the filter criteria
|
||||
// Maps to POST /auth/ctrl/ShowCloudlet
|
||||
func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, region string) ([]Cloudlet, error) {
|
||||
func (c *Client) ShowCloudlets(ctx context.Context, region string, cloudletKey domain.CloudletKey) ([]domain.Cloudlet, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowCloudlet"
|
||||
|
||||
apiCloudletKey := toAPICloudletKey(cloudletKey)
|
||||
filter := CloudletFilter{
|
||||
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||
Cloudlet: Cloudlet{Key: apiCloudletKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +107,7 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
|
|||
|
||||
var cloudlets []Cloudlet
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return cloudlets, nil // Return empty slice for not found
|
||||
return []domain.Cloudlet{}, nil // Return empty slice for not found
|
||||
}
|
||||
|
||||
if err := c.parseStreamingCloudletResponse(resp, &cloudlets); err != nil {
|
||||
|
|
@ -105,17 +115,24 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
|
|||
}
|
||||
|
||||
c.logf("ShowCloudlets: found %d cloudlets matching criteria", len(cloudlets))
|
||||
return cloudlets, nil
|
||||
|
||||
domainCloudlets := make([]domain.Cloudlet, len(cloudlets))
|
||||
for i := range cloudlets {
|
||||
domainCloudlets[i] = toDomainCloudlet(&cloudlets[i])
|
||||
}
|
||||
|
||||
return domainCloudlets, nil
|
||||
}
|
||||
|
||||
// DeleteCloudlet removes a cloudlet from the specified region
|
||||
// Maps to POST /auth/ctrl/DeleteCloudlet
|
||||
func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, region string) error {
|
||||
func (c *Client) DeleteCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) error {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteCloudlet"
|
||||
|
||||
apiCloudletKey := toAPICloudletKey(cloudletKey)
|
||||
filter := CloudletFilter{
|
||||
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||
Cloudlet: Cloudlet{Key: apiCloudletKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -138,12 +155,13 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
|
|||
|
||||
// GetCloudletManifest retrieves the deployment manifest for a cloudlet
|
||||
// Maps to POST /auth/ctrl/GetCloudletManifest
|
||||
func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKey, region string) (*CloudletManifest, error) {
|
||||
func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey domain.CloudletKey, region string) (*CloudletManifest, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/GetCloudletManifest"
|
||||
|
||||
apiCloudletKey := toAPICloudletKey(cloudletKey)
|
||||
filter := CloudletFilter{
|
||||
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||
Cloudlet: Cloudlet{Key: apiCloudletKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -176,12 +194,13 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
|
|||
|
||||
// GetCloudletResourceUsage retrieves resource usage information for a cloudlet
|
||||
// Maps to POST /auth/ctrl/GetCloudletResourceUsage
|
||||
func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey CloudletKey, region string) (*CloudletResourceUsage, error) {
|
||||
func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey domain.CloudletKey, region string) (*CloudletResourceUsage, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/GetCloudletResourceUsage"
|
||||
|
||||
apiCloudletKey := toAPICloudletKey(cloudletKey)
|
||||
filter := CloudletFilter{
|
||||
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||
Cloudlet: Cloudlet{Key: apiCloudletKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -269,3 +288,45 @@ func (c *Client) parseDirectJSONResponse(resp *http.Response, result interface{}
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toAPICloudlet(cloudlet *domain.Cloudlet) *Cloudlet {
|
||||
return &Cloudlet{
|
||||
Key: toAPICloudletKey(cloudlet.Key),
|
||||
Location: toAPILocation(cloudlet.Location),
|
||||
IpSupport: cloudlet.IpSupport,
|
||||
NumDynamicIps: cloudlet.NumDynamicIps,
|
||||
State: cloudlet.State,
|
||||
Flavor: toAPIFlavor(cloudlet.Flavor),
|
||||
PhysicalName: cloudlet.PhysicalName,
|
||||
Region: cloudlet.Region,
|
||||
NotifySrvAddr: cloudlet.NotifySrvAddr,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainCloudlet(cloudlet *Cloudlet) domain.Cloudlet {
|
||||
return domain.Cloudlet{
|
||||
Key: toDomainCloudletKey(cloudlet.Key),
|
||||
Location: toDomainLocation(cloudlet.Location),
|
||||
IpSupport: cloudlet.IpSupport,
|
||||
NumDynamicIps: cloudlet.NumDynamicIps,
|
||||
State: cloudlet.State,
|
||||
Flavor: toDomainFlavor(cloudlet.Flavor),
|
||||
PhysicalName: cloudlet.PhysicalName,
|
||||
Region: cloudlet.Region,
|
||||
NotifySrvAddr: cloudlet.NotifySrvAddr,
|
||||
}
|
||||
}
|
||||
|
||||
func toAPILocation(location domain.Location) Location {
|
||||
return Location{
|
||||
Latitude: location.Latitude,
|
||||
Longitude: location.Longitude,
|
||||
}
|
||||
}
|
||||
|
||||
func toDomainLocation(location Location) domain.Location {
|
||||
return domain.Location{
|
||||
Latitude: location.Latitude,
|
||||
Longitude: location.Longitude,
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -82,7 +83,19 @@ func TestCreateCloudlet(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
err := client.CreateCloudlet(ctx, tt.input)
|
||||
domainCloudlet := &domain.Cloudlet{
|
||||
Key: domain.CloudletKey{
|
||||
Organization: tt.input.Cloudlet.Key.Organization,
|
||||
Name: tt.input.Cloudlet.Key.Name,
|
||||
},
|
||||
Location: domain.Location{
|
||||
Latitude: tt.input.Cloudlet.Location.Latitude,
|
||||
Longitude: tt.input.Cloudlet.Location.Longitude,
|
||||
},
|
||||
IpSupport: tt.input.Cloudlet.IpSupport,
|
||||
NumDynamicIps: tt.input.Cloudlet.NumDynamicIps,
|
||||
}
|
||||
err := client.CreateCloudlet(ctx, tt.input.Region, domainCloudlet)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -152,7 +165,11 @@ func TestShowCloudlet(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
cloudlet, err := client.ShowCloudlet(ctx, tt.cloudletKey, tt.region)
|
||||
domainCloudletKey := domain.CloudletKey{
|
||||
Organization: tt.cloudletKey.Organization,
|
||||
Name: tt.cloudletKey.Name,
|
||||
}
|
||||
cloudlet, err := client.ShowCloudlet(ctx, tt.region, domainCloudletKey)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -194,7 +211,8 @@ func TestShowCloudlets(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
cloudlets, err := client.ShowCloudlets(ctx, CloudletKey{Organization: "cloudletorg"}, "us-west")
|
||||
domainCloudletKey := domain.CloudletKey{Organization: "cloudletorg"}
|
||||
cloudlets, err := client.ShowCloudlets(ctx, "us-west", domainCloudletKey)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cloudlets, 2)
|
||||
|
|
@ -257,7 +275,11 @@ func TestDeleteCloudlet(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
err := client.DeleteCloudlet(ctx, tt.cloudletKey, tt.region)
|
||||
domainCloudletKey := domain.CloudletKey{
|
||||
Organization: tt.cloudletKey.Organization,
|
||||
Name: tt.cloudletKey.Name,
|
||||
}
|
||||
err := client.DeleteCloudlet(ctx, tt.region, domainCloudletKey)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -320,7 +342,11 @@ func TestGetCloudletManifest(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
manifest, err := client.GetCloudletManifest(ctx, tt.cloudletKey, tt.region)
|
||||
domainCloudletKey := domain.CloudletKey{
|
||||
Organization: tt.cloudletKey.Organization,
|
||||
Name: tt.cloudletKey.Name,
|
||||
}
|
||||
manifest, err := client.GetCloudletManifest(ctx, domainCloudletKey, tt.region)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -388,7 +414,11 @@ func TestGetCloudletResourceUsage(t *testing.T) {
|
|||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
usage, err := client.GetCloudletResourceUsage(ctx, tt.cloudletKey, tt.region)
|
||||
domainCloudletKey := domain.CloudletKey{
|
||||
Organization: tt.cloudletKey.Organization,
|
||||
Name: tt.cloudletKey.Name,
|
||||
}
|
||||
usage, err := client.GetCloudletResourceUsage(ctx, domainCloudletKey, tt.region)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -7,6 +7,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
||||
)
|
||||
|
||||
// App field constants for partial updates (based on EdgeXR API specification)
|
||||
|
|
@ -358,4 +360,4 @@ type CloudletResourceUsage struct {
|
|||
CloudletKey CloudletKey `json:"cloudlet_key"`
|
||||
Region string `json:"region"`
|
||||
Usage map[string]interface{} `json:"usage"`
|
||||
}
|
||||
}
|
||||
|
|
@ -1,663 +0,0 @@
|
|||
// ABOUTME: Comprehensive tests for EdgeConnect deployment planner with mock scenarios
|
||||
// ABOUTME: Tests planning logic, state comparison, and various deployment scenarios
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockEdgeConnectClient is a mock implementation of the EdgeConnect client
|
||||
type MockEdgeConnectClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error) {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return edgeconnect.App{}, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(edgeconnect.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return edgeconnect.AppInstance{}, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(edgeconnect.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error {
|
||||
args := m.Called(ctx, input)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error {
|
||||
args := m.Called(ctx, input)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error {
|
||||
args := m.Called(ctx, input)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error {
|
||||
args := m.Called(ctx, input)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey edgeconnect.AppKey, region string) ([]edgeconnect.App, error) {
|
||||
args := m.Called(ctx, appKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]edgeconnect.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]edgeconnect.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func TestNewPlanner(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
|
||||
assert.NotNil(t, planner)
|
||||
assert.IsType(t, &EdgeConnectPlanner{}, planner)
|
||||
}
|
||||
|
||||
func TestDefaultPlanOptions(t *testing.T) {
|
||||
opts := DefaultPlanOptions()
|
||||
|
||||
assert.False(t, opts.DryRun)
|
||||
assert.False(t, opts.Force)
|
||||
assert.False(t, opts.SkipStateCheck)
|
||||
assert.True(t, opts.ParallelQueries)
|
||||
assert.Equal(t, 30*time.Second, opts.Timeout)
|
||||
}
|
||||
|
||||
func createTestConfig(t *testing.T) *config.EdgeConnectConfig {
|
||||
// Create temporary manifest file
|
||||
tempDir := t.TempDir()
|
||||
manifestFile := filepath.Join(tempDir, "test-manifest.yaml")
|
||||
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
||||
err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &config.EdgeConnectConfig{
|
||||
Kind: "edgeconnect-deployment",
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "testorg",
|
||||
},
|
||||
Spec: config.Spec{
|
||||
K8sApp: &config.K8sApp{
|
||||
ManifestFile: manifestFile,
|
||||
},
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "US",
|
||||
CloudletOrg: "TestCloudletOrg",
|
||||
CloudletName: "TestCloudlet",
|
||||
FlavorName: "small",
|
||||
},
|
||||
},
|
||||
Network: &config.NetworkConfig{
|
||||
OutboundConnections: []config.OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanNewDeployment(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Mock API calls to return "not found" errors
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||
|
||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
require.NoError(t, result.Error)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Equal(t, "test-app", plan.ConfigName)
|
||||
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
||||
assert.Equal(t, "Application does not exist", plan.AppAction.Reason)
|
||||
|
||||
require.Len(t, plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, "Instance does not exist", plan.InstanceActions[0].Reason)
|
||||
|
||||
assert.Equal(t, 2, plan.TotalActions) // 1 app + 1 instance
|
||||
assert.False(t, plan.IsEmpty())
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanExistingDeploymentNoChanges(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Note: We would calculate expected manifest hash here when API supports it
|
||||
|
||||
// Mock existing app with same manifest hash and outbound connections
|
||||
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
||||
existingApp := &edgeconnect.App{
|
||||
Key: edgeconnect.AppKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
DeploymentManifest: manifestContent,
|
||||
RequiredOutboundConnections: []edgeconnect.SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
// Note: Manifest hash tracking would be implemented when API supports annotations
|
||||
}
|
||||
|
||||
// Mock existing instance
|
||||
existingInstance := &edgeconnect.AppInstance{
|
||||
Key: edgeconnect.AppInstanceKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
Organization: "TestCloudletOrg",
|
||||
Name: "TestCloudlet",
|
||||
},
|
||||
},
|
||||
AppKey: edgeconnect.AppKey{
|
||||
Organization: "testorg",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: edgeconnect.Flavor{
|
||||
Name: "small",
|
||||
},
|
||||
State: "Ready",
|
||||
PowerState: "PowerOn",
|
||||
}
|
||||
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
||||
Return(*existingApp, nil)
|
||||
|
||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
||||
Return(*existingInstance, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Equal(t, ActionNone, plan.AppAction.Type)
|
||||
assert.Len(t, plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionNone, plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, 0, plan.TotalActions)
|
||||
assert.True(t, plan.IsEmpty())
|
||||
assert.Contains(t, plan.Summary, "No changes required")
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanWithOptions(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
opts := PlanOptions{
|
||||
DryRun: true,
|
||||
SkipStateCheck: true,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.PlanWithOptions(ctx, testConfig, opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.True(t, plan.DryRun)
|
||||
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
||||
assert.Contains(t, plan.AppAction.Reason, "state check skipped")
|
||||
|
||||
// No API calls should be made when SkipStateCheck is true
|
||||
mockClient.AssertNotCalled(t, "ShowApp")
|
||||
mockClient.AssertNotCalled(t, "ShowAppInstance")
|
||||
}
|
||||
|
||||
func TestPlanMultipleInfrastructures(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Add a second infrastructure target
|
||||
testConfig.Spec.InfraTemplate = append(testConfig.Spec.InfraTemplate, config.InfraTemplate{
|
||||
Region: "EU",
|
||||
CloudletOrg: "EUCloudletOrg",
|
||||
CloudletName: "EUCloudlet",
|
||||
FlavorName: "medium",
|
||||
})
|
||||
|
||||
// Mock API calls to return "not found" errors
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||
|
||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||
|
||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "EU").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.NotNil(t, result.Plan)
|
||||
|
||||
plan := result.Plan
|
||||
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
||||
|
||||
// Should have 2 instance actions, one for each infrastructure
|
||||
require.Len(t, plan.InstanceActions, 2)
|
||||
assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, ActionCreate, plan.InstanceActions[1].Type)
|
||||
|
||||
assert.Equal(t, 3, plan.TotalActions) // 1 app + 2 instances
|
||||
|
||||
// Test cloudlet and region aggregation
|
||||
cloudlets := plan.GetTargetCloudlets()
|
||||
regions := plan.GetTargetRegions()
|
||||
assert.Len(t, cloudlets, 2)
|
||||
assert.Len(t, regions, 2)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestCalculateManifestHash(t *testing.T) {
|
||||
planner := &EdgeConnectPlanner{}
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create test file
|
||||
testFile := filepath.Join(tempDir, "test.yaml")
|
||||
content := "test content for hashing"
|
||||
err := os.WriteFile(testFile, []byte(content), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
hash1, err := planner.calculateManifestHash(testFile)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, hash1)
|
||||
assert.Len(t, hash1, 64) // SHA256 hex string length
|
||||
|
||||
// Same content should produce same hash
|
||||
hash2, err := planner.calculateManifestHash(testFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, hash1, hash2)
|
||||
|
||||
// Different content should produce different hash
|
||||
err = os.WriteFile(testFile, []byte("different content"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
hash3, err := planner.calculateManifestHash(testFile)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, hash1, hash3)
|
||||
|
||||
// Empty file path should return empty hash
|
||||
hash4, err := planner.calculateManifestHash("")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, hash4)
|
||||
|
||||
// Non-existent file should return error
|
||||
_, err = planner.calculateManifestHash("/non/existent/file")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCompareAppStates(t *testing.T) {
|
||||
planner := &EdgeConnectPlanner{}
|
||||
|
||||
current := &AppState{
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
AppType: AppTypeK8s,
|
||||
ManifestHash: "old-hash",
|
||||
}
|
||||
|
||||
desired := &AppState{
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
AppType: AppTypeK8s,
|
||||
ManifestHash: "new-hash",
|
||||
}
|
||||
|
||||
changes, manifestChanged := planner.compareAppStates(current, desired)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.True(t, manifestChanged)
|
||||
assert.Contains(t, changes[0], "Manifest hash changed")
|
||||
|
||||
// Test no changes
|
||||
desired.ManifestHash = "old-hash"
|
||||
changes, manifestChanged = planner.compareAppStates(current, desired)
|
||||
assert.Empty(t, changes)
|
||||
assert.False(t, manifestChanged)
|
||||
|
||||
// Test app type change
|
||||
desired.AppType = AppTypeDocker
|
||||
changes, manifestChanged = planner.compareAppStates(current, desired)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.False(t, manifestChanged)
|
||||
assert.Contains(t, changes[0], "App type changed")
|
||||
}
|
||||
|
||||
func TestCompareAppStatesOutboundConnections(t *testing.T) {
|
||||
planner := &EdgeConnectPlanner{}
|
||||
|
||||
// Test with no outbound connections
|
||||
current := &AppState{
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
AppType: AppTypeK8s,
|
||||
OutboundConnections: nil,
|
||||
}
|
||||
|
||||
desired := &AppState{
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
AppType: AppTypeK8s,
|
||||
OutboundConnections: nil,
|
||||
}
|
||||
|
||||
changes, _ := planner.compareAppStates(current, desired)
|
||||
assert.Empty(t, changes, "No changes expected when both have no outbound connections")
|
||||
|
||||
// Test adding outbound connections
|
||||
desired.OutboundConnections = []SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
}
|
||||
|
||||
changes, _ = planner.compareAppStates(current, desired)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Contains(t, changes[0], "Outbound connections changed")
|
||||
|
||||
// Test identical outbound connections
|
||||
current.OutboundConnections = []SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
}
|
||||
|
||||
changes, _ = planner.compareAppStates(current, desired)
|
||||
assert.Empty(t, changes, "No changes expected when outbound connections are identical")
|
||||
|
||||
// Test different outbound connections (different port)
|
||||
desired.OutboundConnections[0].PortRangeMin = 443
|
||||
desired.OutboundConnections[0].PortRangeMax = 443
|
||||
|
||||
changes, _ = planner.compareAppStates(current, desired)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Contains(t, changes[0], "Outbound connections changed")
|
||||
|
||||
// Test same connections but different order (should be considered equal)
|
||||
current.OutboundConnections = []SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 443,
|
||||
PortRangeMax: 443,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
}
|
||||
|
||||
desired.OutboundConnections = []SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 443,
|
||||
PortRangeMax: 443,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
}
|
||||
|
||||
changes, _ = planner.compareAppStates(current, desired)
|
||||
assert.Empty(t, changes, "No changes expected when outbound connections are same but in different order")
|
||||
|
||||
// Test removing outbound connections
|
||||
desired.OutboundConnections = nil
|
||||
|
||||
changes, _ = planner.compareAppStates(current, desired)
|
||||
assert.Len(t, changes, 1)
|
||||
assert.Contains(t, changes[0], "Outbound connections changed")
|
||||
}
|
||||
|
||||
func TestCompareInstanceStates(t *testing.T) {
|
||||
planner := &EdgeConnectPlanner{}
|
||||
|
||||
current := &InstanceState{
|
||||
Name: "test-instance",
|
||||
FlavorName: "small",
|
||||
CloudletName: "oldcloudlet",
|
||||
CloudletOrg: "oldorg",
|
||||
}
|
||||
|
||||
desired := &InstanceState{
|
||||
Name: "test-instance",
|
||||
FlavorName: "medium",
|
||||
CloudletName: "newcloudlet",
|
||||
CloudletOrg: "neworg",
|
||||
}
|
||||
|
||||
changes := planner.compareInstanceStates(current, desired)
|
||||
assert.Len(t, changes, 3)
|
||||
assert.Contains(t, changes[0], "Flavor changed")
|
||||
assert.Contains(t, changes[1], "Cloudlet changed")
|
||||
assert.Contains(t, changes[2], "Cloudlet org changed")
|
||||
|
||||
// Test no changes
|
||||
desired.FlavorName = "small"
|
||||
desired.CloudletName = "oldcloudlet"
|
||||
desired.CloudletOrg = "oldorg"
|
||||
changes = planner.compareInstanceStates(current, desired)
|
||||
assert.Empty(t, changes)
|
||||
}
|
||||
|
||||
func TestDeploymentPlanMethods(t *testing.T) {
|
||||
plan := &DeploymentPlan{
|
||||
ConfigName: "test-plan",
|
||||
AppAction: AppAction{
|
||||
Type: ActionCreate,
|
||||
Desired: &AppState{Name: "test-app"},
|
||||
},
|
||||
InstanceActions: []InstanceAction{
|
||||
{
|
||||
Type: ActionCreate,
|
||||
Target: config.InfraTemplate{
|
||||
CloudletOrg: "org1",
|
||||
CloudletName: "cloudlet1",
|
||||
Region: "US",
|
||||
},
|
||||
InstanceName: "instance1",
|
||||
Desired: &InstanceState{Name: "instance1"},
|
||||
},
|
||||
{
|
||||
Type: ActionUpdate,
|
||||
Target: config.InfraTemplate{
|
||||
CloudletOrg: "org2",
|
||||
CloudletName: "cloudlet2",
|
||||
Region: "EU",
|
||||
},
|
||||
InstanceName: "instance2",
|
||||
Desired: &InstanceState{Name: "instance2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test IsEmpty
|
||||
assert.False(t, plan.IsEmpty())
|
||||
|
||||
// Test GetTargetCloudlets
|
||||
cloudlets := plan.GetTargetCloudlets()
|
||||
assert.Len(t, cloudlets, 2)
|
||||
assert.Contains(t, cloudlets, "org1:cloudlet1")
|
||||
assert.Contains(t, cloudlets, "org2:cloudlet2")
|
||||
|
||||
// Test GetTargetRegions
|
||||
regions := plan.GetTargetRegions()
|
||||
assert.Len(t, regions, 2)
|
||||
assert.Contains(t, regions, "US")
|
||||
assert.Contains(t, regions, "EU")
|
||||
|
||||
// Test GenerateSummary
|
||||
summary := plan.GenerateSummary()
|
||||
assert.Contains(t, summary, "test-plan")
|
||||
assert.Contains(t, summary, "CREATE application")
|
||||
assert.Contains(t, summary, "CREATE 1 instance")
|
||||
assert.Contains(t, summary, "UPDATE 1 instance")
|
||||
|
||||
// Test Validate
|
||||
err := plan.Validate()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test validation failure
|
||||
plan.AppAction.Desired = nil
|
||||
err = plan.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must have desired state")
|
||||
}
|
||||
|
||||
func TestEstimateDeploymentDuration(t *testing.T) {
|
||||
planner := &EdgeConnectPlanner{}
|
||||
|
||||
plan := &DeploymentPlan{
|
||||
AppAction: AppAction{Type: ActionCreate},
|
||||
InstanceActions: []InstanceAction{
|
||||
{Type: ActionCreate},
|
||||
{Type: ActionUpdate},
|
||||
},
|
||||
}
|
||||
|
||||
duration := planner.estimateDeploymentDuration(plan)
|
||||
assert.Greater(t, duration, time.Duration(0))
|
||||
assert.Less(t, duration, 10*time.Minute) // Reasonable upper bound
|
||||
|
||||
// Test with no actions
|
||||
emptyPlan := &DeploymentPlan{
|
||||
AppAction: AppAction{Type: ActionNone},
|
||||
InstanceActions: []InstanceAction{},
|
||||
}
|
||||
|
||||
emptyDuration := planner.estimateDeploymentDuration(emptyPlan)
|
||||
assert.Greater(t, emptyDuration, time.Duration(0))
|
||||
assert.Less(t, emptyDuration, duration) // Should be less than plan with actions
|
||||
}
|
||||
|
||||
func TestIsResourceNotFoundError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{"nil error", nil, false},
|
||||
{"not found error", &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Resource not found"}}, true},
|
||||
{"does not exist error", &edgeconnect.APIError{Messages: []string{"App does not exist"}}, true},
|
||||
{"404 in message", &edgeconnect.APIError{Messages: []string{"HTTP 404 error"}}, true},
|
||||
{"other error", &edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isResourceNotFoundError(tt.err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanErrorHandling(t *testing.T) {
|
||||
mockClient := &MockEdgeConnectClient{}
|
||||
planner := NewPlanner(mockClient)
|
||||
testConfig := createTestConfig(t)
|
||||
|
||||
// Mock API call to return a non-404 error
|
||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
||||
Return(nil, &edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := planner.Plan(ctx, testConfig)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Error)
|
||||
assert.Contains(t, err.Error(), "failed to query current app state")
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
// ABOUTME: Deployment planning types for EdgeConnect apply command with state management
|
||||
// ABOUTME: Defines structures for deployment plans, actions, and state comparison results
|
||||
package apply
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// SecurityRule defines network access rules (alias to SDK type for consistency)
|
||||
type SecurityRule = edgeconnect.SecurityRule
|
||||
|
||||
// ActionType represents the type of action to be performed
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
// ActionCreate indicates a resource needs to be created
|
||||
ActionCreate ActionType = "CREATE"
|
||||
// ActionUpdate indicates a resource needs to be updated
|
||||
ActionUpdate ActionType = "UPDATE"
|
||||
// ActionNone indicates no action is needed
|
||||
ActionNone ActionType = "NONE"
|
||||
// ActionDelete indicates a resource needs to be deleted (for rollback scenarios)
|
||||
ActionDelete ActionType = "DELETE"
|
||||
)
|
||||
|
||||
// String returns the string representation of ActionType
|
||||
func (a ActionType) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// DeploymentPlan represents the complete deployment plan for a configuration
|
||||
type DeploymentPlan struct {
|
||||
// ConfigName is the name from metadata
|
||||
ConfigName string
|
||||
|
||||
// AppAction defines what needs to be done with the application
|
||||
AppAction AppAction
|
||||
|
||||
// InstanceActions defines what needs to be done with each instance
|
||||
InstanceActions []InstanceAction
|
||||
|
||||
// Summary provides a human-readable summary of the plan
|
||||
Summary string
|
||||
|
||||
// TotalActions is the count of all actions that will be performed
|
||||
TotalActions int
|
||||
|
||||
// EstimatedDuration is the estimated time to complete the deployment
|
||||
EstimatedDuration time.Duration
|
||||
|
||||
// CreatedAt timestamp when the plan was created
|
||||
CreatedAt time.Time
|
||||
|
||||
// DryRun indicates if this is a dry-run plan
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// AppAction represents an action to be performed on an application
|
||||
type AppAction struct {
|
||||
// Type of action to perform
|
||||
Type ActionType
|
||||
|
||||
// Current state of the app (nil if doesn't exist)
|
||||
Current *AppState
|
||||
|
||||
// Desired state of the app
|
||||
Desired *AppState
|
||||
|
||||
// Changes describes what will change
|
||||
Changes []string
|
||||
|
||||
// Reason explains why this action is needed
|
||||
Reason string
|
||||
|
||||
// ManifestHash is the hash of the current manifest file
|
||||
ManifestHash string
|
||||
|
||||
// ManifestChanged indicates if the manifest content has changed
|
||||
ManifestChanged bool
|
||||
}
|
||||
|
||||
// InstanceAction represents an action to be performed on an application instance
|
||||
type InstanceAction struct {
|
||||
// Type of action to perform
|
||||
Type ActionType
|
||||
|
||||
// Target infrastructure where the instance will be deployed
|
||||
Target config.InfraTemplate
|
||||
|
||||
// Current state of the instance (nil if doesn't exist)
|
||||
Current *InstanceState
|
||||
|
||||
// Desired state of the instance
|
||||
Desired *InstanceState
|
||||
|
||||
// Changes describes what will change
|
||||
Changes []string
|
||||
|
||||
// Reason explains why this action is needed
|
||||
Reason string
|
||||
|
||||
// InstanceName is the generated name for this instance
|
||||
InstanceName string
|
||||
|
||||
// Dependencies lists other instances this depends on
|
||||
Dependencies []string
|
||||
}
|
||||
|
||||
// AppState represents the current state of an application
|
||||
type AppState struct {
|
||||
// Name of the application
|
||||
Name string
|
||||
|
||||
// Version of the application
|
||||
Version string
|
||||
|
||||
// Organization that owns the app
|
||||
Organization string
|
||||
|
||||
// Region where the app is deployed
|
||||
Region string
|
||||
|
||||
// ManifestHash is the stored hash of the manifest file
|
||||
ManifestHash string
|
||||
|
||||
// LastUpdated timestamp when the app was last modified
|
||||
LastUpdated time.Time
|
||||
|
||||
// Exists indicates if the app currently exists
|
||||
Exists bool
|
||||
|
||||
// AppType indicates whether this is a k8s or docker app
|
||||
AppType AppType
|
||||
|
||||
// OutboundConnections contains the required outbound network connections
|
||||
OutboundConnections []SecurityRule
|
||||
}
|
||||
|
||||
// InstanceState represents the current state of an application instance
|
||||
type InstanceState struct {
|
||||
// Name of the instance
|
||||
Name string
|
||||
|
||||
// AppName that this instance belongs to
|
||||
AppName string
|
||||
|
||||
// AppVersion of the associated app
|
||||
AppVersion string
|
||||
|
||||
// Organization that owns the instance
|
||||
Organization string
|
||||
|
||||
// Region where the instance is deployed
|
||||
Region string
|
||||
|
||||
// CloudletOrg that hosts the cloudlet
|
||||
CloudletOrg string
|
||||
|
||||
// CloudletName where the instance is running
|
||||
CloudletName string
|
||||
|
||||
// FlavorName used for the instance
|
||||
FlavorName string
|
||||
|
||||
// State of the instance (e.g., "Ready", "Pending", "Error")
|
||||
State string
|
||||
|
||||
// PowerState of the instance
|
||||
PowerState string
|
||||
|
||||
// LastUpdated timestamp when the instance was last modified
|
||||
LastUpdated time.Time
|
||||
|
||||
// Exists indicates if the instance currently exists
|
||||
Exists bool
|
||||
}
|
||||
|
||||
// AppType represents the type of application
|
||||
type AppType string
|
||||
|
||||
const (
|
||||
// AppTypeK8s represents a Kubernetes application
|
||||
AppTypeK8s AppType = "k8s"
|
||||
// AppTypeDocker represents a Docker application
|
||||
AppTypeDocker AppType = "docker"
|
||||
)
|
||||
|
||||
// String returns the string representation of AppType
|
||||
func (a AppType) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// DeploymentSummary provides a high-level overview of the deployment plan
|
||||
type DeploymentSummary struct {
|
||||
// TotalActions is the total number of actions to be performed
|
||||
TotalActions int
|
||||
|
||||
// ActionCounts breaks down actions by type
|
||||
ActionCounts map[ActionType]int
|
||||
|
||||
// EstimatedDuration for the entire deployment
|
||||
EstimatedDuration time.Duration
|
||||
|
||||
// ResourceSummary describes the resources involved
|
||||
ResourceSummary ResourceSummary
|
||||
|
||||
// Warnings about potential issues
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// ResourceSummary provides details about resources in the deployment
|
||||
type ResourceSummary struct {
|
||||
// AppsToCreate number of apps that will be created
|
||||
AppsToCreate int
|
||||
|
||||
// AppsToUpdate number of apps that will be updated
|
||||
AppsToUpdate int
|
||||
|
||||
// InstancesToCreate number of instances that will be created
|
||||
InstancesToCreate int
|
||||
|
||||
// InstancesToUpdate number of instances that will be updated
|
||||
InstancesToUpdate int
|
||||
|
||||
// CloudletsAffected number of unique cloudlets involved
|
||||
CloudletsAffected int
|
||||
|
||||
// RegionsAffected number of unique regions involved
|
||||
RegionsAffected int
|
||||
}
|
||||
|
||||
// PlanResult represents the result of a deployment planning operation
|
||||
type PlanResult struct {
|
||||
// Plan is the generated deployment plan
|
||||
Plan *DeploymentPlan
|
||||
|
||||
// Error if planning failed
|
||||
Error error
|
||||
|
||||
// Warnings encountered during planning
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// ExecutionResult represents the result of executing a deployment plan
|
||||
type ExecutionResult struct {
|
||||
// Plan that was executed
|
||||
Plan *DeploymentPlan
|
||||
|
||||
// Success indicates if the deployment was successful
|
||||
Success bool
|
||||
|
||||
// CompletedActions lists actions that were successfully completed
|
||||
CompletedActions []ActionResult
|
||||
|
||||
// FailedActions lists actions that failed
|
||||
FailedActions []ActionResult
|
||||
|
||||
// Error that caused the deployment to fail (if any)
|
||||
Error error
|
||||
|
||||
// Duration taken to execute the plan
|
||||
Duration time.Duration
|
||||
|
||||
// RollbackPerformed indicates if rollback was executed
|
||||
RollbackPerformed bool
|
||||
|
||||
// RollbackSuccess indicates if rollback was successful
|
||||
RollbackSuccess bool
|
||||
}
|
||||
|
||||
// ActionResult represents the result of executing a single action
|
||||
type ActionResult struct {
|
||||
// Type of action that was attempted
|
||||
Type ActionType
|
||||
|
||||
// Target describes what was being acted upon
|
||||
Target string
|
||||
|
||||
// Success indicates if the action succeeded
|
||||
Success bool
|
||||
|
||||
// Error if the action failed
|
||||
Error error
|
||||
|
||||
// Duration taken to complete the action
|
||||
Duration time.Duration
|
||||
|
||||
// Details provides additional information about the action
|
||||
Details string
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the deployment plan has no actions to perform
|
||||
func (dp *DeploymentPlan) IsEmpty() bool {
|
||||
if dp.AppAction.Type != ActionNone {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, action := range dp.InstanceActions {
|
||||
if action.Type != ActionNone {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// HasErrors returns true if the plan contains any error conditions
|
||||
func (dp *DeploymentPlan) HasErrors() bool {
|
||||
// Check for conflicting actions or invalid states
|
||||
return false // Implementation would check for various error conditions
|
||||
}
|
||||
|
||||
// GetTargetCloudlets returns a list of unique cloudlets that will be affected
|
||||
func (dp *DeploymentPlan) GetTargetCloudlets() []string {
|
||||
cloudletSet := make(map[string]bool)
|
||||
var cloudlets []string
|
||||
|
||||
for _, action := range dp.InstanceActions {
|
||||
if action.Type != ActionNone {
|
||||
key := fmt.Sprintf("%s:%s", action.Target.CloudletOrg, action.Target.CloudletName)
|
||||
if !cloudletSet[key] {
|
||||
cloudletSet[key] = true
|
||||
cloudlets = append(cloudlets, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cloudlets
|
||||
}
|
||||
|
||||
// GetTargetRegions returns a list of unique regions that will be affected
|
||||
func (dp *DeploymentPlan) GetTargetRegions() []string {
|
||||
regionSet := make(map[string]bool)
|
||||
var regions []string
|
||||
|
||||
for _, action := range dp.InstanceActions {
|
||||
if action.Type != ActionNone && !regionSet[action.Target.Region] {
|
||||
regionSet[action.Target.Region] = true
|
||||
regions = append(regions, action.Target.Region)
|
||||
}
|
||||
}
|
||||
|
||||
return regions
|
||||
}
|
||||
|
||||
// GenerateSummary creates a human-readable summary of the deployment plan
|
||||
func (dp *DeploymentPlan) GenerateSummary() string {
|
||||
if dp.IsEmpty() {
|
||||
return "No changes required - configuration matches current state"
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Deployment plan for '%s':\n", dp.ConfigName))
|
||||
|
||||
// App actions
|
||||
if dp.AppAction.Type != ActionNone {
|
||||
sb.WriteString(fmt.Sprintf("- %s application '%s'\n", dp.AppAction.Type, dp.AppAction.Desired.Name))
|
||||
if len(dp.AppAction.Changes) > 0 {
|
||||
for _, change := range dp.AppAction.Changes {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", change))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance actions
|
||||
createCount := 0
|
||||
updateActions := []InstanceAction{}
|
||||
for _, action := range dp.InstanceActions {
|
||||
switch action.Type {
|
||||
case ActionCreate:
|
||||
createCount++
|
||||
case ActionUpdate:
|
||||
updateActions = append(updateActions, action)
|
||||
}
|
||||
}
|
||||
|
||||
if createCount > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- CREATE %d instance(s) across %d cloudlet(s)\n", createCount, len(dp.GetTargetCloudlets())))
|
||||
}
|
||||
|
||||
if len(updateActions) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- UPDATE %d instance(s)\n", len(updateActions)))
|
||||
for _, action := range updateActions {
|
||||
if len(action.Changes) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" - Instance '%s':\n", action.InstanceName))
|
||||
for _, change := range action.Changes {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", change))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("Estimated duration: %s", dp.EstimatedDuration.String()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Validate checks if the deployment plan is valid and safe to execute
|
||||
func (dp *DeploymentPlan) Validate() error {
|
||||
if dp.ConfigName == "" {
|
||||
return fmt.Errorf("deployment plan must have a config name")
|
||||
}
|
||||
|
||||
// Validate app action
|
||||
if dp.AppAction.Type != ActionNone && dp.AppAction.Desired == nil {
|
||||
return fmt.Errorf("app action of type %s must have desired state", dp.AppAction.Type)
|
||||
}
|
||||
|
||||
// Validate instance actions
|
||||
for i, action := range dp.InstanceActions {
|
||||
if action.Type != ActionNone {
|
||||
if action.Desired == nil {
|
||||
return fmt.Errorf("instance action %d of type %s must have desired state", i, action.Type)
|
||||
}
|
||||
if action.InstanceName == "" {
|
||||
return fmt.Errorf("instance action %d must have an instance name", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the deployment plan
|
||||
func (dp *DeploymentPlan) Clone() *DeploymentPlan {
|
||||
clone := &DeploymentPlan{
|
||||
ConfigName: dp.ConfigName,
|
||||
Summary: dp.Summary,
|
||||
TotalActions: dp.TotalActions,
|
||||
EstimatedDuration: dp.EstimatedDuration,
|
||||
CreatedAt: dp.CreatedAt,
|
||||
DryRun: dp.DryRun,
|
||||
AppAction: dp.AppAction, // Struct copy is sufficient for this use case
|
||||
}
|
||||
|
||||
// Deep copy instance actions
|
||||
clone.InstanceActions = make([]InstanceAction, len(dp.InstanceActions))
|
||||
copy(clone.InstanceActions, dp.InstanceActions)
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// convertNetworkRules converts config network rules to EdgeConnect SecurityRules
|
||||
func convertNetworkRules(network *config.NetworkConfig) []edgeconnect.SecurityRule {
|
||||
rules := make([]edgeconnect.SecurityRule, len(network.OutboundConnections))
|
||||
|
||||
for i, conn := range network.OutboundConnections {
|
||||
rules[i] = edgeconnect.SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
RemoteCIDR: conn.RemoteCIDR,
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ func TestGetDeploymentType(t *testing.T) {
|
|||
K8sApp: &K8sApp{},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "kubernetes", k8sConfig.GetDeploymentType())
|
||||
assert.Equal(t, "docker", k8sConfig.GetDeploymentType())
|
||||
|
||||
// Test docker app
|
||||
dockerConfig := &EdgeConnectConfig{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
|
|
@ -25,7 +26,8 @@ type ResourceManagerInterface interface {
|
|||
|
||||
// EdgeConnectResourceManager implements resource management for EdgeConnect
|
||||
type EdgeConnectResourceManager struct {
|
||||
client EdgeConnectClientInterface
|
||||
appRepo driven.AppRepository
|
||||
appInstRepo driven.AppInstanceRepository
|
||||
parallelLimit int
|
||||
rollbackOnFail bool
|
||||
logger Logger
|
||||
|
|
@ -66,14 +68,15 @@ func DefaultResourceManagerOptions() ResourceManagerOptions {
|
|||
}
|
||||
|
||||
// NewResourceManager creates a new EdgeConnect resource manager
|
||||
func NewResourceManager(client EdgeConnectClientInterface, opts ...func(*ResourceManagerOptions)) ResourceManagerInterface {
|
||||
func NewResourceManager(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository, opts ...func(*ResourceManagerOptions)) ResourceManagerInterface {
|
||||
options := DefaultResourceManagerOptions()
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return &EdgeConnectResourceManager{
|
||||
client: client,
|
||||
appRepo: appRepo,
|
||||
appInstRepo: appInstRepo,
|
||||
parallelLimit: options.ParallelLimit,
|
||||
rollbackOnFail: options.RollbackOnFail,
|
||||
logger: options.Logger,
|
||||
|
|
@ -133,7 +136,7 @@ func (rm *EdgeConnectResourceManager) ApplyDeployment(ctx context.Context, plan
|
|||
strategyConfig := rm.strategyConfig
|
||||
strategyConfig.ParallelOperations = rm.parallelLimit > 1
|
||||
|
||||
factory := NewStrategyFactory(rm.client, strategyConfig, rm.logger)
|
||||
factory := NewStrategyFactory(rm.appRepo, rm.appInstRepo, strategyConfig, rm.logger)
|
||||
strategy, err := factory.CreateStrategy(strategyName)
|
||||
if err != nil {
|
||||
result := &ExecutionResult{
|
||||
|
|
@ -190,8 +193,8 @@ func (rm *EdgeConnectResourceManager) ValidatePrerequisites(ctx context.Context,
|
|||
}
|
||||
|
||||
// Validate that we have required client capabilities
|
||||
if rm.client == nil {
|
||||
return fmt.Errorf("EdgeConnect client is not configured")
|
||||
if rm.appRepo == nil || rm.appInstRepo == nil {
|
||||
return fmt.Errorf("repositories are not configured")
|
||||
}
|
||||
|
||||
rm.logf("Prerequisites validation passed")
|
||||
|
|
@ -250,13 +253,13 @@ func (rm *EdgeConnectResourceManager) rollbackCreateAction(ctx context.Context,
|
|||
|
||||
// rollbackApp deletes an application that was created
|
||||
func (rm *EdgeConnectResourceManager) rollbackApp(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: plan.AppAction.Desired.Organization,
|
||||
Name: plan.AppAction.Desired.Name,
|
||||
Version: plan.AppAction.Desired.Version,
|
||||
}
|
||||
|
||||
return rm.client.DeleteApp(ctx, appKey, plan.AppAction.Desired.Region)
|
||||
return rm.appRepo.DeleteApp(ctx, plan.AppAction.Desired.Region, appKey)
|
||||
}
|
||||
|
||||
// rollbackInstance deletes an instance that was created
|
||||
|
|
@ -264,15 +267,15 @@ func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, acti
|
|||
// Find the instance action to get the details
|
||||
for _, instanceAction := range plan.InstanceActions {
|
||||
if instanceAction.InstanceName == action.Target {
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: plan.AppAction.Desired.Organization,
|
||||
Name: instanceAction.InstanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: instanceAction.Target.CloudletOrg,
|
||||
Name: instanceAction.Target.CloudletName,
|
||||
},
|
||||
}
|
||||
return rm.client.DeleteAppInstance(ctx, instanceKey, instanceAction.Target.Region)
|
||||
return rm.appInstRepo.DeleteAppInstance(ctx, instanceAction.Target.Region, instanceKey)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("instance action not found for rollback: %s", action.Target)
|
||||
|
|
@ -283,4 +286,4 @@ func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
|||
if rm.logger != nil {
|
||||
rm.logger.Printf("[ResourceManager] "+format, v...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
139
internal/core/apply/mocks_test.go
Normal file
139
internal/core/apply/mocks_test.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockAppRepository is a mock implementation of driven.AppRepository
|
||||
type MockAppRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAppRepository) CreateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
args := m.Called(ctx, region, app)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAppRepository) ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error) {
|
||||
args := m.Called(ctx, region, appKey)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAppRepository) ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error) {
|
||||
args := m.Called(ctx, region, appKey)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAppRepository) DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error {
|
||||
args := m.Called(ctx, region, appKey)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAppRepository) UpdateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
args := m.Called(ctx, region, app)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// MockAppInstanceRepository is a mock implementation of driven.AppInstanceRepository
|
||||
type MockAppInstanceRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
args := m.Called(ctx, region, appInst)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error) {
|
||||
args := m.Called(ctx, region, appInstKey)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error) {
|
||||
args := m.Called(ctx, region, appInstKey)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.AppInstance), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
args := m.Called(ctx, region, appInstKey)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
args := m.Called(ctx, region, appInst)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAppInstanceRepository) RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
args := m.Called(ctx, region, appInstKey)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// MockConfigRepository is a mock implementation of driven.ConfigRepository
|
||||
type MockConfigRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockConfigRepository) ParseFile(path string) (*config.EdgeConnectConfig, string, error) {
|
||||
args := m.Called(path)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.String(1), args.Error(2)
|
||||
}
|
||||
return args.Get(0).(*config.EdgeConnectConfig), args.String(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *MockConfigRepository) Validate(cfg *config.EdgeConnectConfig) error {
|
||||
args := m.Called(cfg)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// NewTestPlanner creates a planner with mock repositories for testing
|
||||
func NewTestPlanner(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository) Planner {
|
||||
if appRepo == nil {
|
||||
appRepo = new(MockAppRepository)
|
||||
}
|
||||
if appInstRepo == nil {
|
||||
appInstRepo = new(MockAppInstanceRepository)
|
||||
}
|
||||
return NewPlanner(appRepo, appInstRepo)
|
||||
}
|
||||
|
||||
// NewTestResourceManager creates a resource manager with mock repositories for testing
|
||||
func NewTestResourceManager(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository) ResourceManagerInterface {
|
||||
if appRepo == nil {
|
||||
appRepo = new(MockAppRepository)
|
||||
}
|
||||
if appInstRepo == nil {
|
||||
appInstRepo = new(MockAppInstanceRepository)
|
||||
}
|
||||
return NewResourceManager(appRepo, appInstRepo)
|
||||
}
|
||||
|
||||
// NewTestStrategyFactory creates a strategy factory with mock repositories for testing
|
||||
func NewTestStrategyFactory(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository) *StrategyFactory {
|
||||
if appRepo == nil {
|
||||
appRepo = new(MockAppRepository)
|
||||
}
|
||||
if appInstRepo == nil {
|
||||
appInstRepo = new(MockAppInstanceRepository)
|
||||
}
|
||||
return NewStrategyFactory(appRepo, appInstRepo, DefaultStrategyConfig(), nil)
|
||||
}
|
||||
|
|
@ -12,21 +12,10 @@ import (
|
|||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
||||
type EdgeConnectClientInterface interface {
|
||||
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
|
||||
CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error
|
||||
UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error
|
||||
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
|
||||
ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error)
|
||||
CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error
|
||||
UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
|
||||
}
|
||||
|
||||
// Planner defines the interface for deployment planning
|
||||
type Planner interface {
|
||||
// Plan analyzes the configuration and current state to generate a deployment plan
|
||||
|
|
@ -67,13 +56,15 @@ func DefaultPlanOptions() PlanOptions {
|
|||
|
||||
// EdgeConnectPlanner implements the Planner interface for EdgeConnect
|
||||
type EdgeConnectPlanner struct {
|
||||
client EdgeConnectClientInterface
|
||||
appRepo driven.AppRepository
|
||||
appInstRepo driven.AppInstanceRepository
|
||||
}
|
||||
|
||||
// NewPlanner creates a new EdgeConnect deployment planner
|
||||
func NewPlanner(client EdgeConnectClientInterface) Planner {
|
||||
func NewPlanner(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository) Planner {
|
||||
return &EdgeConnectPlanner{
|
||||
client: client,
|
||||
appRepo: appRepo,
|
||||
appInstRepo: appInstRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,9 +139,9 @@ func (p *EdgeConnectPlanner) planAppAction(ctx context.Context, config *config.E
|
|||
|
||||
// Extract outbound connections from config
|
||||
if config.Spec.Network != nil {
|
||||
desired.OutboundConnections = make([]SecurityRule, len(config.Spec.Network.OutboundConnections))
|
||||
desired.OutboundConnections = make([]domain.SecurityRule, len(config.Spec.Network.OutboundConnections))
|
||||
for i, conn := range config.Spec.Network.OutboundConnections {
|
||||
desired.OutboundConnections[i] = SecurityRule{
|
||||
desired.OutboundConnections[i] = domain.SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
|
|
@ -285,13 +276,13 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
|
|||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: desired.Organization,
|
||||
Name: desired.Name,
|
||||
Version: desired.Version,
|
||||
}
|
||||
|
||||
app, err := p.client.ShowApp(timeoutCtx, appKey, desired.Region)
|
||||
app, err := p.appRepo.ShowApp(timeoutCtx, desired.Region, appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -321,9 +312,9 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
|
|||
}
|
||||
|
||||
// Extract outbound connections from the app
|
||||
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
||||
current.OutboundConnections = make([]domain.SecurityRule, len(app.RequiredOutboundConnections))
|
||||
for i, conn := range app.RequiredOutboundConnections {
|
||||
current.OutboundConnections[i] = SecurityRule{
|
||||
current.OutboundConnections[i] = domain.SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
|
|
@ -339,16 +330,16 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
|
|||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: desired.Organization,
|
||||
Name: desired.Name,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: desired.CloudletOrg,
|
||||
Name: desired.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
|
||||
instance, err := p.appInstRepo.ShowAppInstance(timeoutCtx, desired.Region, instanceKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -405,10 +396,10 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str
|
|||
}
|
||||
|
||||
// compareOutboundConnections compares two sets of outbound connections for equality
|
||||
func (p *EdgeConnectPlanner) compareOutboundConnections(current, desired []SecurityRule) []string {
|
||||
func (p *EdgeConnectPlanner) compareOutboundConnections(current, desired []domain.SecurityRule) []string {
|
||||
var changes []string
|
||||
makeMap := func(rules []SecurityRule) map[string]SecurityRule {
|
||||
m := make(map[string]SecurityRule, len(rules))
|
||||
makeMap := func(rules []domain.SecurityRule) map[string]domain.SecurityRule {
|
||||
m := make(map[string]domain.SecurityRule, len(rules))
|
||||
for _, r := range rules {
|
||||
key := fmt.Sprintf("%s:%d-%d:%s",
|
||||
strings.ToLower(r.Protocol),
|
||||
|
|
@ -552,4 +543,4 @@ func max(a, b time.Duration) time.Duration {
|
|||
// getInstanceName generates the instance name following the pattern: appName-appVersion-instance
|
||||
func getInstanceName(appName, appVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
|
||||
}
|
||||
}
|
||||
757
internal/core/apply/planner_test.go
Normal file
757
internal/core/apply/planner_test.go
Normal file
|
|
@ -0,0 +1,757 @@
|
|||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestPlanner_Plan_CreateApp(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock app not found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(nil, fmt.Errorf("resource not found"))
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionCreate, result.Plan.AppAction.Type)
|
||||
assert.Equal(t, "Application does not exist", result.Plan.AppAction.Reason)
|
||||
assert.Len(t, result.Plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionCreate, result.Plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, "Instance does not exist", result.Plan.InstanceActions[0].Reason)
|
||||
assert.Equal(t, 2, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_UpdateApp(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
Network: &config.NetworkConfig{
|
||||
OutboundConnections: []config.OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
RequiredOutboundConnections: []domain.SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 8080,
|
||||
PortRangeMax: 8080,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance not found
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(nil, fmt.Errorf("resource not found"))
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionUpdate, result.Plan.AppAction.Type)
|
||||
assert.Contains(t, result.Plan.AppAction.Reason, "Application configuration has changed")
|
||||
assert.Len(t, result.Plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionCreate, result.Plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, 2, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_NoChange(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
Network: &config.NetworkConfig{
|
||||
OutboundConnections: []config.OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
RequiredOutboundConnections: []domain.SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingInstance := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: domain.Flavor{Name: "m4.small"},
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance found
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org", Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(existingInstance, nil)
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionNone, result.Plan.AppAction.Type)
|
||||
assert.Equal(t, ActionNone, result.Plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, 0, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_SkipStateCheck(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := DefaultPlanOptions()
|
||||
opts.SkipStateCheck = true
|
||||
|
||||
result, err := planner.PlanWithOptions(context.Background(), testConfig, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionCreate, result.Plan.AppAction.Type)
|
||||
assert.Equal(t, "Creating app (state check skipped)", result.Plan.AppAction.Reason)
|
||||
assert.Len(t, result.Plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionCreate, result.Plan.InstanceActions[0].Type)
|
||||
assert.Equal(t, "Creating instance (state check skipped)", result.Plan.InstanceActions[0].Reason)
|
||||
assert.Equal(t, 2, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertNotCalled(t, "ShowApp", mock.Anything, mock.Anything, mock.Anything)
|
||||
mockAppInstRepo.AssertNotCalled(t, "ShowAppInstance", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_ManifestHashChange(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
// Create a temporary manifest file
|
||||
tempDir := t.TempDir()
|
||||
manifestPath := filepath.Join(tempDir, "manifest.yaml")
|
||||
err := os.WriteFile(manifestPath, []byte("new manifest content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
Manifest: manifestPath,
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
DeploymentManifest: "old manifest content", // Different manifest
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance found (no change)
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(&domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: domain.Flavor{Name: "m4.small"},
|
||||
}, nil)
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionUpdate, result.Plan.AppAction.Type)
|
||||
assert.True(t, result.Plan.AppAction.ManifestChanged)
|
||||
assert.Contains(t, result.Plan.AppAction.Reason, "Application configuration has changed")
|
||||
assert.Contains(t, result.Warnings, "Manifest file has changed - instances may need to be recreated")
|
||||
assert.Equal(t, 1, result.Plan.TotalActions) // Only app update, instance is ActionNone
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_InstanceFlavorChange(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.large", // Changed flavor
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
}
|
||||
|
||||
existingInstance := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: domain.Flavor{Name: "m4.small"}, // Old flavor
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance found
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(existingInstance, nil)
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionNone, result.Plan.AppAction.Type)
|
||||
assert.Len(t, result.Plan.InstanceActions, 1)
|
||||
assert.Equal(t, ActionUpdate, result.Plan.InstanceActions[0].Type)
|
||||
assert.Contains(t, result.Plan.InstanceActions[0].Reason, "Instance configuration has changed")
|
||||
assert.Contains(t, result.Plan.InstanceActions[0].Changes, "Flavor changed: m4.small -> m4.large")
|
||||
assert.Equal(t, 1, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_MultipleInstances(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "multi-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org-1",
|
||||
CloudletName: "cloudlet-name-1",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
{
|
||||
Region: "us-east",
|
||||
CloudletOrg: "cloudlet-org-2",
|
||||
CloudletName: "cloudlet-name-2",
|
||||
FlavorName: "m4.medium",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, mock.AnythingOfType("string"), domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance 1 not found
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org-1",
|
||||
Name: "cloudlet-name-1",
|
||||
},
|
||||
}).Return(nil, fmt.Errorf("resource not found"))
|
||||
|
||||
// Mock instance 2 found with different flavor
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-east", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org-2",
|
||||
Name: "cloudlet-name-2",
|
||||
},
|
||||
}).Return(&domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org-2",
|
||||
Name: "cloudlet-name-2",
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "multi-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: domain.Flavor{Name: "m4.small"}, // Different flavor
|
||||
}, nil)
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionNone, result.Plan.AppAction.Type)
|
||||
assert.Len(t, result.Plan.InstanceActions, 2)
|
||||
assert.Equal(t, ActionCreate, result.Plan.InstanceActions[0].Type) // Instance 1 is new
|
||||
assert.Equal(t, ActionUpdate, result.Plan.InstanceActions[1].Type) // Instance 2 has flavor change
|
||||
assert.Contains(t, result.Plan.InstanceActions[1].Changes, "Flavor changed: m4.small -> m4.medium")
|
||||
assert.Equal(t, 2, result.Plan.TotalActions)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_AppQueryError(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock app query error
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(nil, fmt.Errorf("network error"))
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to query current app state: network error")
|
||||
assert.Nil(t, result)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_InstanceQueryError(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance query error
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(nil, fmt.Errorf("database error"))
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to query current instance state: database error")
|
||||
assert.Nil(t, result)
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_ManifestFileError(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "kubernetes",
|
||||
Manifest: "/non/existent/path/manifest.yaml", // Invalid path
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to calculate manifest hash: failed to open manifest file")
|
||||
assert.Nil(t, result)
|
||||
|
||||
mockAppRepo.AssertNotCalled(t, "ShowApp", mock.Anything, mock.Anything, mock.Anything)
|
||||
mockAppInstRepo.AssertNotCalled(t, "ShowAppInstance", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func TestPlanner_Plan_AppTypeChange(t *testing.T) {
|
||||
mockAppRepo := new(MockAppRepository)
|
||||
mockAppInstRepo := new(MockAppInstanceRepository)
|
||||
planner := NewTestPlanner(mockAppRepo, mockAppInstRepo)
|
||||
|
||||
testConfig := &config.EdgeConnectConfig{
|
||||
Metadata: config.Metadata{
|
||||
Name: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Organization: "test-org",
|
||||
},
|
||||
Spec: config.AppSpec{
|
||||
Deployment: "docker", // Desired is docker
|
||||
InfraTemplate: []config.InfraTemplate{
|
||||
{
|
||||
Region: "us-west",
|
||||
CloudletOrg: "cloudlet-org",
|
||||
CloudletName: "cloudlet-name",
|
||||
FlavorName: "m4.small",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
existingApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes", // Current is kubernetes
|
||||
}
|
||||
|
||||
// Mock app found
|
||||
mockAppRepo.On("ShowApp", mock.Anything, "us-west", domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
}).Return(existingApp, nil)
|
||||
|
||||
// Mock instance found (no change)
|
||||
mockAppInstRepo.On("ShowAppInstance", mock.Anything, "us-west", domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
}).Return(&domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: "cloudlet-org",
|
||||
Name: "cloudlet-name",
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: domain.Flavor{Name: "m4.small"},
|
||||
}, nil)
|
||||
|
||||
result, err := planner.Plan(context.Background(), testConfig)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.NotNil(t, result.Plan)
|
||||
assert.Equal(t, ActionUpdate, result.Plan.AppAction.Type)
|
||||
assert.Contains(t, result.Plan.AppAction.Changes, "App type changed: KUBERNETES -> DOCKER")
|
||||
assert.Equal(t, 1, result.Plan.TotalActions) // Only app update, instance is ActionNone
|
||||
|
||||
mockAppRepo.AssertExpectations(t)
|
||||
mockAppInstRepo.AssertExpectations(t)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
)
|
||||
|
||||
// DeploymentStrategy represents the type of deployment strategy
|
||||
|
|
@ -66,17 +67,19 @@ func DefaultStrategyConfig() StrategyConfig {
|
|||
|
||||
// StrategyFactory creates deployment strategy executors
|
||||
type StrategyFactory struct {
|
||||
config StrategyConfig
|
||||
client EdgeConnectClientInterface
|
||||
logger Logger
|
||||
config StrategyConfig
|
||||
appRepo driven.AppRepository
|
||||
appInstRepo driven.AppInstanceRepository
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// NewStrategyFactory creates a new strategy factory
|
||||
func NewStrategyFactory(client EdgeConnectClientInterface, config StrategyConfig, logger Logger) *StrategyFactory {
|
||||
func NewStrategyFactory(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository, config StrategyConfig, logger Logger) *StrategyFactory {
|
||||
return &StrategyFactory{
|
||||
config: config,
|
||||
client: client,
|
||||
logger: logger,
|
||||
config: config,
|
||||
appRepo: appRepo,
|
||||
appInstRepo: appInstRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +87,7 @@ func NewStrategyFactory(client EdgeConnectClientInterface, config StrategyConfig
|
|||
func (f *StrategyFactory) CreateStrategy(strategy DeploymentStrategy) (DeploymentStrategyExecutor, error) {
|
||||
switch strategy {
|
||||
case StrategyRecreate:
|
||||
return NewRecreateStrategy(f.client, f.config, f.logger), nil
|
||||
return NewRecreateStrategy(f.appRepo, f.appInstRepo, f.config, f.logger), nil
|
||||
case StrategyBlueGreen:
|
||||
// TODO: Implement blue-green strategy
|
||||
return nil, fmt.Errorf("blue-green strategy not yet implemented")
|
||||
|
|
@ -103,4 +106,4 @@ func (f *StrategyFactory) GetAvailableStrategies() []DeploymentStrategy {
|
|||
// StrategyBlueGreen, // TODO: Enable when implemented
|
||||
// StrategyRolling, // TODO: Enable when implemented
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,22 +10,25 @@ import (
|
|||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
)
|
||||
|
||||
// RecreateStrategy implements the recreate deployment strategy
|
||||
type RecreateStrategy struct {
|
||||
client EdgeConnectClientInterface
|
||||
config StrategyConfig
|
||||
logger Logger
|
||||
appRepo driven.AppRepository
|
||||
appInstRepo driven.AppInstanceRepository
|
||||
config StrategyConfig
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// NewRecreateStrategy creates a new recreate strategy executor
|
||||
func NewRecreateStrategy(client EdgeConnectClientInterface, config StrategyConfig, logger Logger) *RecreateStrategy {
|
||||
func NewRecreateStrategy(appRepo driven.AppRepository, appInstRepo driven.AppInstanceRepository, config StrategyConfig, logger Logger) *RecreateStrategy {
|
||||
return &RecreateStrategy{
|
||||
client: client,
|
||||
config: config,
|
||||
logger: logger,
|
||||
appRepo: appRepo,
|
||||
appInstRepo: appInstRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,13 +186,13 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP
|
|||
|
||||
r.logf("Phase 2: Deleting existing application")
|
||||
|
||||
appKey := edgeconnect.AppKey{
|
||||
appKey := domain.AppKey{
|
||||
Organization: plan.AppAction.Desired.Organization,
|
||||
Name: plan.AppAction.Desired.Name,
|
||||
Version: plan.AppAction.Desired.Version,
|
||||
}
|
||||
|
||||
if err := r.client.DeleteApp(ctx, appKey, plan.AppAction.Desired.Region); err != nil {
|
||||
if err := r.appRepo.DeleteApp(ctx, plan.AppAction.Desired.Region, appKey); err != nil {
|
||||
result.FailedActions = append(result.FailedActions, ActionResult{
|
||||
Type: ActionDelete,
|
||||
Target: plan.AppAction.Desired.Name,
|
||||
|
|
@ -386,7 +389,12 @@ func (r *RecreateStrategy) executeAppActionWithRetry(ctx context.Context, action
|
|||
}
|
||||
}
|
||||
|
||||
success, err := r.updateApplication(ctx, action, config, manifestContent)
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
// For recreate strategy, we always create the app
|
||||
success, err = r.createApplication(ctx, action, config, manifestContent)
|
||||
|
||||
if success {
|
||||
result.Success = true
|
||||
result.Details = fmt.Sprintf("Successfully updated application %s", action.Desired.Name)
|
||||
|
|
@ -407,16 +415,16 @@ func (r *RecreateStrategy) executeAppActionWithRetry(ctx context.Context, action
|
|||
|
||||
// deleteInstance deletes an instance (reuse existing logic from manager.go)
|
||||
func (r *RecreateStrategy) deleteInstance(ctx context.Context, action InstanceAction) (bool, error) {
|
||||
instanceKey := edgeconnect.AppInstanceKey{
|
||||
instanceKey := domain.AppInstanceKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.InstanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: action.Target.CloudletOrg,
|
||||
Name: action.Target.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
err := r.client.DeleteAppInstance(ctx, instanceKey, action.Target.Region)
|
||||
err := r.appInstRepo.DeleteAppInstance(ctx, action.Target.Region, instanceKey)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to delete instance: %w", err)
|
||||
}
|
||||
|
|
@ -426,30 +434,27 @@ func (r *RecreateStrategy) deleteInstance(ctx context.Context, action InstanceAc
|
|||
|
||||
// createInstance creates an instance (extracted from manager.go logic)
|
||||
func (r *RecreateStrategy) createInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (bool, error) {
|
||||
instanceInput := &edgeconnect.NewAppInstanceInput{
|
||||
Region: action.Target.Region,
|
||||
AppInst: edgeconnect.AppInstance{
|
||||
Key: edgeconnect.AppInstanceKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.InstanceName,
|
||||
CloudletKey: edgeconnect.CloudletKey{
|
||||
Organization: action.Target.CloudletOrg,
|
||||
Name: action.Target.CloudletName,
|
||||
},
|
||||
},
|
||||
AppKey: edgeconnect.AppKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: config.Metadata.Name,
|
||||
Version: config.Metadata.AppVersion,
|
||||
},
|
||||
Flavor: edgeconnect.Flavor{
|
||||
Name: action.Target.FlavorName,
|
||||
appInst := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.InstanceName,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: action.Target.CloudletOrg,
|
||||
Name: action.Target.CloudletName,
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: config.Metadata.Name,
|
||||
Version: config.Metadata.AppVersion,
|
||||
},
|
||||
Flavor: domain.Flavor{
|
||||
Name: action.Target.FlavorName,
|
||||
},
|
||||
}
|
||||
|
||||
// Create the instance
|
||||
if err := r.client.CreateAppInstance(ctx, instanceInput); err != nil {
|
||||
if err := r.appInstRepo.CreateAppInstance(ctx, action.Target.Region, appInst); err != nil {
|
||||
return false, fmt.Errorf("failed to create instance: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -459,35 +464,30 @@ func (r *RecreateStrategy) createInstance(ctx context.Context, action InstanceAc
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// updateApplication creates/recreates an application (always uses CreateApp since we delete first)
|
||||
func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppAction, config *config.EdgeConnectConfig, manifestContent string) (bool, error) {
|
||||
// Build the app create input - always create since recreate strategy deletes first
|
||||
appInput := &edgeconnect.NewAppInput{
|
||||
Region: action.Desired.Region,
|
||||
App: edgeconnect.App{
|
||||
Key: edgeconnect.AppKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.Desired.Name,
|
||||
Version: action.Desired.Version,
|
||||
},
|
||||
Deployment: config.GetDeploymentType(),
|
||||
ImageType: "ImageTypeDocker",
|
||||
ImagePath: config.GetImagePath(),
|
||||
AllowServerless: true,
|
||||
DefaultFlavor: edgeconnect.Flavor{Name: config.Spec.InfraTemplate[0].FlavorName},
|
||||
ServerlessConfig: struct{}{},
|
||||
DeploymentManifest: manifestContent,
|
||||
DeploymentGenerator: "kubernetes-basic",
|
||||
// createApplication creates/recreates an application (always uses CreateApp since we delete first)
|
||||
func (r *RecreateStrategy) createApplication(ctx context.Context, action AppAction, config *config.EdgeConnectConfig, manifestContent string) (bool, error) {
|
||||
app := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.Desired.Name,
|
||||
Version: action.Desired.Version,
|
||||
},
|
||||
Deployment: config.GetDeploymentType(),
|
||||
ImagePath: config.GetImagePath(),
|
||||
AllowServerless: true,
|
||||
DefaultFlavor: domain.Flavor{Name: config.Spec.InfraTemplate[0].FlavorName},
|
||||
ServerlessConfig: struct{}{},
|
||||
DeploymentManifest: manifestContent,
|
||||
DeploymentGenerator: "kubernetes-basic",
|
||||
}
|
||||
|
||||
// Add network configuration if specified
|
||||
if config.Spec.Network != nil {
|
||||
appInput.App.RequiredOutboundConnections = convertNetworkRules(config.Spec.Network)
|
||||
app.RequiredOutboundConnections = convertNetworkRules(config.Spec.Network)
|
||||
}
|
||||
|
||||
// Create the application (recreate strategy always creates from scratch)
|
||||
if err := r.client.CreateApp(ctx, appInput); err != nil {
|
||||
if err := r.appRepo.CreateApp(ctx, action.Desired.Region, app); err != nil {
|
||||
return false, fmt.Errorf("failed to create application: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -497,9 +497,27 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// convertNetworkRules converts config.NetworkConfig to []domain.SecurityRule
|
||||
func convertNetworkRules(network *config.NetworkConfig) []domain.SecurityRule {
|
||||
if network == nil || len(network.OutboundConnections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rules := make([]domain.SecurityRule, len(network.OutboundConnections))
|
||||
for i, conn := range network.OutboundConnections {
|
||||
rules[i] = domain.SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
RemoteCIDR: conn.RemoteCIDR,
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// logf logs a message if a logger is configured
|
||||
func (r *RecreateStrategy) logf(format string, v ...interface{}) {
|
||||
if r.logger != nil {
|
||||
r.logger.Printf("[RecreateStrategy] "+format, v...)
|
||||
}
|
||||
}
|
||||
}
|
||||
161
internal/core/apply/types.go
Normal file
161
internal/core/apply/types.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// ABOUTME: Core types for EdgeConnect deployment planning and execution
|
||||
// ABOUTME: Defines data structures for deployment plans, actions, and results
|
||||
package apply
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
// ActionType defines the type of action to be performed
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionNone ActionType = "NONE"
|
||||
ActionCreate ActionType = "CREATE"
|
||||
ActionUpdate ActionType = "UPDATE"
|
||||
ActionDelete ActionType = "DELETE"
|
||||
)
|
||||
|
||||
// AppType defines the type of application deployment
|
||||
type AppType string
|
||||
|
||||
const (
|
||||
AppTypeK8s AppType = "KUBERNETES"
|
||||
AppTypeDocker AppType = "DOCKER"
|
||||
)
|
||||
|
||||
// AppState represents the desired or current state of an application
|
||||
type AppState struct {
|
||||
Name string
|
||||
Version string
|
||||
Organization string
|
||||
Region string
|
||||
AppType AppType
|
||||
ManifestHash string
|
||||
OutboundConnections []domain.SecurityRule
|
||||
Exists bool
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// InstanceState represents the desired or current state of an application instance
|
||||
type InstanceState struct {
|
||||
Name string
|
||||
AppName string
|
||||
AppVersion string
|
||||
Organization string
|
||||
Region string
|
||||
CloudletOrg string
|
||||
CloudletName string
|
||||
FlavorName string
|
||||
State string
|
||||
PowerState string
|
||||
Exists bool
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// AppAction defines an action to be performed on an application
|
||||
type AppAction struct {
|
||||
Type ActionType
|
||||
Desired *AppState
|
||||
Current *AppState
|
||||
ManifestHash string
|
||||
ManifestChanged bool
|
||||
Reason string
|
||||
Changes []string
|
||||
}
|
||||
|
||||
// InstanceAction defines an action to be performed on an application instance
|
||||
type InstanceAction struct {
|
||||
Type ActionType
|
||||
Target config.InfraTemplate
|
||||
Desired *InstanceState
|
||||
Current *InstanceState
|
||||
InstanceName string
|
||||
Reason string
|
||||
Changes []string
|
||||
}
|
||||
|
||||
// DeploymentPlan represents a plan of actions to achieve the desired state
|
||||
type DeploymentPlan struct {
|
||||
ConfigName string
|
||||
CreatedAt time.Time
|
||||
DryRun bool
|
||||
AppAction AppAction
|
||||
InstanceActions []InstanceAction
|
||||
TotalActions int
|
||||
EstimatedDuration time.Duration
|
||||
Summary string
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the plan contains no actions
|
||||
func (p *DeploymentPlan) IsEmpty() bool {
|
||||
return p.AppAction.Type == ActionNone && len(p.InstanceActions) == 0
|
||||
}
|
||||
|
||||
// Validate checks the validity of the deployment plan
|
||||
func (p *DeploymentPlan) Validate() error {
|
||||
if p.AppAction.Type == ActionNone && len(p.InstanceActions) == 0 {
|
||||
return fmt.Errorf("deployment plan is empty")
|
||||
}
|
||||
// Add more validation rules as needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSummary creates a human-readable summary of the plan
|
||||
func (p *DeploymentPlan) GenerateSummary() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("Plan for '%s' (created: %s, dry-run: %t)\n", p.ConfigName, p.CreatedAt.Format(time.RFC3339), p.DryRun))
|
||||
sb.WriteString("--------------------------------------------------\n")
|
||||
|
||||
if p.AppAction.Type != ActionNone {
|
||||
sb.WriteString(fmt.Sprintf("Application '%s': %s - %s\n", p.AppAction.Desired.Name, p.AppAction.Type, p.AppAction.Reason))
|
||||
for _, change := range p.AppAction.Changes {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", change))
|
||||
}
|
||||
}
|
||||
|
||||
for _, action := range p.InstanceActions {
|
||||
sb.WriteString(fmt.Sprintf("Instance '%s' on '%s': %s - %s\n", action.InstanceName, action.Target.CloudletName, action.Type, action.Reason))
|
||||
for _, change := range action.Changes {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", change))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("--------------------------------------------------\n")
|
||||
sb.WriteString(fmt.Sprintf("Total actions: %d, Estimated duration: %v\n", p.TotalActions, p.EstimatedDuration))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// PlanResult holds the result of a planning operation
|
||||
type PlanResult struct {
|
||||
Plan *DeploymentPlan
|
||||
Warnings []string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ExecutionResult holds the result of a deployment execution
|
||||
type ExecutionResult struct {
|
||||
Plan *DeploymentPlan
|
||||
Success bool
|
||||
Error error
|
||||
Duration time.Duration
|
||||
CompletedActions []ActionResult
|
||||
FailedActions []ActionResult
|
||||
RollbackPerformed bool
|
||||
RollbackSuccess bool
|
||||
}
|
||||
|
||||
// ActionResult details the outcome of a single action
|
||||
type ActionResult struct {
|
||||
Type ActionType
|
||||
Target string
|
||||
Success bool
|
||||
Error error
|
||||
Details string
|
||||
Duration time.Duration
|
||||
}
|
||||
79
internal/core/domain/domain.go
Normal file
79
internal/core/domain/domain.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
package domain
|
||||
|
||||
// AppKey uniquely identifies an application
|
||||
type AppKey struct {
|
||||
Organization string
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
// CloudletKey uniquely identifies a cloudlet
|
||||
type CloudletKey struct {
|
||||
Organization string
|
||||
Name string
|
||||
}
|
||||
|
||||
// AppInstanceKey uniquely identifies an application instance
|
||||
type AppInstanceKey struct {
|
||||
Organization string
|
||||
Name string
|
||||
CloudletKey CloudletKey
|
||||
}
|
||||
|
||||
// Flavor defines resource allocation for instances
|
||||
type Flavor struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// SecurityRule defines network access rules
|
||||
type SecurityRule struct {
|
||||
PortRangeMax int
|
||||
PortRangeMin int
|
||||
Protocol string
|
||||
RemoteCIDR string
|
||||
}
|
||||
|
||||
// App represents an application definition
|
||||
type App struct {
|
||||
Key AppKey
|
||||
Deployment string
|
||||
ImageType string
|
||||
ImagePath string
|
||||
AllowServerless bool
|
||||
DefaultFlavor Flavor
|
||||
ServerlessConfig interface{}
|
||||
DeploymentGenerator string
|
||||
DeploymentManifest string
|
||||
RequiredOutboundConnections []SecurityRule
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// AppInstance represents a deployed application instance
|
||||
type AppInstance struct {
|
||||
Key AppInstanceKey
|
||||
AppKey AppKey
|
||||
Flavor Flavor
|
||||
State string
|
||||
PowerState string
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// Cloudlet represents edge infrastructure
|
||||
type Cloudlet struct {
|
||||
Key CloudletKey
|
||||
Location Location
|
||||
IpSupport string
|
||||
NumDynamicIps int32
|
||||
State string
|
||||
Flavor Flavor
|
||||
PhysicalName string
|
||||
Region string
|
||||
NotifySrvAddr string
|
||||
}
|
||||
|
||||
// Location represents geographical coordinates
|
||||
type Location struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
14
internal/core/ports/driven/app_repository.go
Normal file
14
internal/core/ports/driven/app_repository.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package driven
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type AppRepository interface {
|
||||
CreateApp(ctx context.Context, region string, app *domain.App) error
|
||||
ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error)
|
||||
ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error)
|
||||
DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error
|
||||
UpdateApp(ctx context.Context, region string, app *domain.App) error
|
||||
}
|
||||
13
internal/core/ports/driven/cloudlet_repository.go
Normal file
13
internal/core/ports/driven/cloudlet_repository.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package driven
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type CloudletRepository interface {
|
||||
CreateCloudlet(ctx context.Context, region string, cloudlet *domain.Cloudlet) error
|
||||
ShowCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) (*domain.Cloudlet, error)
|
||||
ShowCloudlets(ctx context.Context, region string, cloudletKey domain.CloudletKey) ([]domain.Cloudlet, error)
|
||||
DeleteCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) error
|
||||
}
|
||||
15
internal/core/ports/driven/instance_repository.go
Normal file
15
internal/core/ports/driven/instance_repository.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package driven
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type AppInstanceRepository interface {
|
||||
CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error
|
||||
ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error)
|
||||
ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error)
|
||||
DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error
|
||||
UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error
|
||||
RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error
|
||||
}
|
||||
14
internal/core/ports/driving/app_service.go
Normal file
14
internal/core/ports/driving/app_service.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package driving
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type AppService interface {
|
||||
CreateApp(ctx context.Context, region string, app *domain.App) error
|
||||
ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error)
|
||||
ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error)
|
||||
DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error
|
||||
UpdateApp(ctx context.Context, region string, app *domain.App) error
|
||||
}
|
||||
13
internal/core/ports/driving/cloudlet_service.go
Normal file
13
internal/core/ports/driving/cloudlet_service.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package driving
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type CloudletService interface {
|
||||
CreateCloudlet(ctx context.Context, region string, cloudlet *domain.Cloudlet) error
|
||||
ShowCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) (*domain.Cloudlet, error)
|
||||
ShowCloudlets(ctx context.Context, region string, cloudletKey domain.CloudletKey) ([]domain.Cloudlet, error)
|
||||
DeleteCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) error
|
||||
}
|
||||
15
internal/core/ports/driving/instance_service.go
Normal file
15
internal/core/ports/driving/instance_service.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package driving
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
type AppInstanceService interface {
|
||||
CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error
|
||||
ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error)
|
||||
ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error)
|
||||
DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error
|
||||
UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error
|
||||
RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error
|
||||
}
|
||||
36
internal/core/services/app_service.go
Normal file
36
internal/core/services/app_service.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driving"
|
||||
)
|
||||
|
||||
type appService struct {
|
||||
appRepo driven.AppRepository
|
||||
}
|
||||
|
||||
func NewAppService(appRepo driven.AppRepository) driving.AppService {
|
||||
return &appService{appRepo: appRepo}
|
||||
}
|
||||
|
||||
func (s *appService) CreateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
return s.appRepo.CreateApp(ctx, region, app)
|
||||
}
|
||||
|
||||
func (s *appService) ShowApp(ctx context.Context, region string, appKey domain.AppKey) (*domain.App, error) {
|
||||
return s.appRepo.ShowApp(ctx, region, appKey)
|
||||
}
|
||||
|
||||
func (s *appService) ShowApps(ctx context.Context, region string, appKey domain.AppKey) ([]domain.App, error) {
|
||||
return s.appRepo.ShowApps(ctx, region, appKey)
|
||||
}
|
||||
|
||||
func (s *appService) DeleteApp(ctx context.Context, region string, appKey domain.AppKey) error {
|
||||
return s.appRepo.DeleteApp(ctx, region, appKey)
|
||||
}
|
||||
|
||||
func (s *appService) UpdateApp(ctx context.Context, region string, app *domain.App) error {
|
||||
return s.appRepo.UpdateApp(ctx, region, app)
|
||||
}
|
||||
32
internal/core/services/cloudlet_service.go
Normal file
32
internal/core/services/cloudlet_service.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driving"
|
||||
)
|
||||
|
||||
type cloudletService struct {
|
||||
cloudletRepo driven.CloudletRepository
|
||||
}
|
||||
|
||||
func NewCloudletService(cloudletRepo driven.CloudletRepository) driving.CloudletService {
|
||||
return &cloudletService{cloudletRepo: cloudletRepo}
|
||||
}
|
||||
|
||||
func (s *cloudletService) CreateCloudlet(ctx context.Context, region string, cloudlet *domain.Cloudlet) error {
|
||||
return s.cloudletRepo.CreateCloudlet(ctx, region, cloudlet)
|
||||
}
|
||||
|
||||
func (s *cloudletService) ShowCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) (*domain.Cloudlet, error) {
|
||||
return s.cloudletRepo.ShowCloudlet(ctx, region, cloudletKey)
|
||||
}
|
||||
|
||||
func (s *cloudletService) ShowCloudlets(ctx context.Context, region string, cloudletKey domain.CloudletKey) ([]domain.Cloudlet, error) {
|
||||
return s.cloudletRepo.ShowCloudlets(ctx, region, cloudletKey)
|
||||
}
|
||||
|
||||
func (s *cloudletService) DeleteCloudlet(ctx context.Context, region string, cloudletKey domain.CloudletKey) error {
|
||||
return s.cloudletRepo.DeleteCloudlet(ctx, region, cloudletKey)
|
||||
}
|
||||
40
internal/core/services/instance_service.go
Normal file
40
internal/core/services/instance_service.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driven"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/ports/driving"
|
||||
)
|
||||
|
||||
type appInstanceService struct {
|
||||
appInstanceRepo driven.AppInstanceRepository
|
||||
}
|
||||
|
||||
func NewAppInstanceService(appInstanceRepo driven.AppInstanceRepository) driving.AppInstanceService {
|
||||
return &appInstanceService{appInstanceRepo: appInstanceRepo}
|
||||
}
|
||||
|
||||
func (s *appInstanceService) CreateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
return s.appInstanceRepo.CreateAppInstance(ctx, region, appInst)
|
||||
}
|
||||
|
||||
func (s *appInstanceService) ShowAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) (*domain.AppInstance, error) {
|
||||
return s.appInstanceRepo.ShowAppInstance(ctx, region, appInstKey)
|
||||
}
|
||||
|
||||
func (s *appInstanceService) ShowAppInstances(ctx context.Context, region string, appInstKey domain.AppInstanceKey) ([]domain.AppInstance, error) {
|
||||
return s.appInstanceRepo.ShowAppInstances(ctx, region, appInstKey)
|
||||
}
|
||||
|
||||
func (s *appInstanceService) DeleteAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
return s.appInstanceRepo.DeleteAppInstance(ctx, region, appInstKey)
|
||||
}
|
||||
|
||||
func (s *appInstanceService) UpdateAppInstance(ctx context.Context, region string, appInst *domain.AppInstance) error {
|
||||
return s.appInstanceRepo.UpdateAppInstance(ctx, region, appInst)
|
||||
}
|
||||
|
||||
func (s *appInstanceService) RefreshAppInstance(ctx context.Context, region string, appInstKey domain.AppInstanceKey) error {
|
||||
return s.appInstanceRepo.RefreshAppInstance(ctx, region, appInstKey)
|
||||
}
|
||||
4
main.go
4
main.go
|
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
|
||||
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/cmd"
|
||||
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/cli"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
cli.Execute()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -86,6 +87,10 @@ type WorkflowConfig struct {
|
|||
}
|
||||
|
||||
func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config WorkflowConfig) error {
|
||||
var domainAppKey domain.AppKey
|
||||
var domainInstanceKey domain.AppInstanceKey
|
||||
var domainCloudletKey domain.CloudletKey
|
||||
var domainAppInstKey domain.AppInstanceKey
|
||||
fmt.Println("═══ Phase 1: Application Management ═══")
|
||||
|
||||
// 1. Create Application
|
||||
|
|
@ -121,7 +126,22 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
},
|
||||
}
|
||||
|
||||
if err := c.CreateApp(ctx, app); err != nil {
|
||||
domainApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: app.App.Key.Organization,
|
||||
Name: app.App.Key.Name,
|
||||
Version: app.App.Key.Version,
|
||||
},
|
||||
Deployment: app.App.Deployment,
|
||||
ImageType: app.App.ImageType,
|
||||
ImagePath: app.App.ImagePath,
|
||||
DefaultFlavor: domain.Flavor{Name: app.App.DefaultFlavor.Name},
|
||||
ServerlessConfig: app.App.ServerlessConfig,
|
||||
AllowServerless: app.App.AllowServerless,
|
||||
RequiredOutboundConnections: edgeconnect.ToDomainSecurityRules(app.App.RequiredOutboundConnections),
|
||||
}
|
||||
|
||||
if err := c.CreateApp(ctx, app.Region, domainApp); err != nil {
|
||||
return fmt.Errorf("failed to create app: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ App created: %s/%s v%s\n", config.Organization, config.AppName, config.AppVersion)
|
||||
|
|
@ -134,7 +154,12 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
Version: config.AppVersion,
|
||||
}
|
||||
|
||||
appDetails, err := c.ShowApp(ctx, appKey, config.Region)
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
appDetails, err := c.ShowApp(ctx, config.Region, domainAppKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to show app: %w", err)
|
||||
}
|
||||
|
|
@ -146,8 +171,8 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 3. List Applications in Organization
|
||||
fmt.Println("\n3️⃣ Listing applications in organization...")
|
||||
filter := edgeconnect.AppKey{Organization: config.Organization}
|
||||
apps, err := c.ShowApps(ctx, filter, config.Region)
|
||||
filter := domain.AppKey{Organization: config.Organization}
|
||||
apps, err := c.ShowApps(ctx, config.Region, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list apps: %w", err)
|
||||
}
|
||||
|
|
@ -176,7 +201,23 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
},
|
||||
}
|
||||
|
||||
if err := c.CreateAppInstance(ctx, instance); err != nil {
|
||||
domainAppInst := &domain.AppInstance{
|
||||
Key: domain.AppInstanceKey{
|
||||
Organization: instance.AppInst.Key.Organization,
|
||||
Name: instance.AppInst.Key.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: instance.AppInst.Key.CloudletKey.Organization,
|
||||
Name: instance.AppInst.Key.CloudletKey.Name,
|
||||
},
|
||||
},
|
||||
AppKey: domain.AppKey{
|
||||
Organization: instance.AppInst.AppKey.Organization,
|
||||
Name: instance.AppInst.AppKey.Name,
|
||||
Version: instance.AppInst.AppKey.Version,
|
||||
},
|
||||
Flavor: domain.Flavor{Name: instance.AppInst.Flavor.Name},
|
||||
}
|
||||
if err := c.CreateAppInstance(ctx, instance.Region, domainAppInst); err != nil {
|
||||
return fmt.Errorf("failed to create app instance: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ App instance created: %s on cloudlet %s/%s\n",
|
||||
|
|
@ -207,7 +248,8 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 6. List Application Instances
|
||||
fmt.Println("\n6️⃣ Listing application instances...")
|
||||
instances, err := c.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{Organization: config.Organization}, config.Region)
|
||||
domainAppInstKey = domain.AppInstanceKey{Organization: config.Organization}
|
||||
instances, err := c.ShowAppInstances(ctx, config.Region, domainAppInstKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list app instances: %w", err)
|
||||
}
|
||||
|
|
@ -219,7 +261,15 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 7. Refresh Application Instance
|
||||
fmt.Println("\n7️⃣ Refreshing application instance...")
|
||||
if err := c.RefreshAppInstance(ctx, instanceKey, config.Region); err != nil {
|
||||
domainInstanceKey = domain.AppInstanceKey{
|
||||
Organization: instanceKey.Organization,
|
||||
Name: instanceKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: instanceKey.CloudletKey.Organization,
|
||||
Name: instanceKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
if err := c.RefreshAppInstance(ctx, config.Region, domainInstanceKey); err != nil {
|
||||
return fmt.Errorf("failed to refresh app instance: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ Instance refreshed: %s\n", config.InstanceName)
|
||||
|
|
@ -233,7 +283,11 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
Name: config.CloudletName,
|
||||
}
|
||||
|
||||
cloudlets, err := c.ShowCloudlets(ctx, cloudletKey, config.Region)
|
||||
domainCloudletKey = domain.CloudletKey{
|
||||
Organization: cloudletKey.Organization,
|
||||
Name: cloudletKey.Name,
|
||||
}
|
||||
cloudlets, err := c.ShowCloudlets(ctx, config.Region, domainCloudletKey)
|
||||
if err != nil {
|
||||
// This might fail in demo environment, so we'll continue
|
||||
fmt.Printf("⚠️ Could not retrieve cloudlet details: %v\n", err)
|
||||
|
|
@ -249,7 +303,11 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 9. Try to Get Cloudlet Manifest (may not be available in demo)
|
||||
fmt.Println("\n9️⃣ Attempting to retrieve cloudlet manifest...")
|
||||
manifest, err := c.GetCloudletManifest(ctx, cloudletKey, config.Region)
|
||||
domainCloudletKey = domain.CloudletKey{
|
||||
Organization: cloudletKey.Organization,
|
||||
Name: cloudletKey.Name,
|
||||
}
|
||||
manifest, err := c.GetCloudletManifest(ctx, domainCloudletKey, config.Region)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ Could not retrieve cloudlet manifest: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -258,8 +316,12 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 10. Try to Get Cloudlet Resource Usage (may not be available in demo)
|
||||
fmt.Println("\n🔟 Attempting to retrieve cloudlet resource usage...")
|
||||
usage, err := c.GetCloudletResourceUsage(ctx, cloudletKey, config.Region)
|
||||
if err != nil {
|
||||
domainCloudletKey = domain.CloudletKey{
|
||||
Organization: cloudletKey.Organization,
|
||||
Name: cloudletKey.Name,
|
||||
}
|
||||
usage, err := c.GetCloudletResourceUsage(ctx, domainCloudletKey, config.Region)
|
||||
if err != nil {
|
||||
fmt.Printf("⚠️ Could not retrieve cloudlet usage: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("✅ Cloudlet resource usage retrieved\n")
|
||||
|
|
@ -272,21 +334,39 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
|
||||
// 11. Delete Application Instance
|
||||
fmt.Println("\n1️⃣1️⃣ Cleaning up application instance...")
|
||||
if err := c.DeleteAppInstance(ctx, instanceKey, config.Region); err != nil {
|
||||
domainInstanceKey = domain.AppInstanceKey{
|
||||
Organization: instanceKey.Organization,
|
||||
Name: instanceKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: instanceKey.CloudletKey.Organization,
|
||||
Name: instanceKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
if err := c.DeleteAppInstance(ctx, config.Region, domainInstanceKey); err != nil {
|
||||
return fmt.Errorf("failed to delete app instance: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ App instance deleted: %s\n", config.InstanceName)
|
||||
|
||||
// 12. Delete Application
|
||||
fmt.Println("\n1️⃣2️⃣ Cleaning up application...")
|
||||
if err := c.DeleteApp(ctx, appKey, config.Region); err != nil {
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
if err := c.DeleteApp(ctx, config.Region, domainAppKey); err != nil {
|
||||
return fmt.Errorf("failed to delete app: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ App deleted: %s/%s v%s\n", config.Organization, config.AppName, config.AppVersion)
|
||||
|
||||
// 13. Verify Cleanup
|
||||
fmt.Println("\n1️⃣3️⃣ Verifying cleanup...")
|
||||
_, err = c.ShowApp(ctx, appKey, config.Region)
|
||||
fmt.Println("\n1️⃣3️⃣ Verifying cleanup...")
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
_, err = c.ShowApp(ctx, config.Region, domainAppKey)
|
||||
if err != nil && fmt.Sprintf("%v", err) == edgeconnect.ErrResourceNotFound.Error() {
|
||||
fmt.Printf("✅ Cleanup verified - app no longer exists\n")
|
||||
} else if err != nil {
|
||||
|
|
@ -321,7 +401,15 @@ func waitForInstanceReady(ctx context.Context, c *edgeconnect.Client, instanceKe
|
|||
return edgeconnect.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
|
||||
|
||||
case <-ticker.C:
|
||||
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
|
||||
domainInstanceKey := domain.AppInstanceKey{
|
||||
Organization: instanceKey.Organization,
|
||||
Name: instanceKey.Name,
|
||||
CloudletKey: domain.CloudletKey{
|
||||
Organization: instanceKey.CloudletKey.Organization,
|
||||
Name: instanceKey.CloudletKey.Name,
|
||||
},
|
||||
}
|
||||
instance, err := c.ShowAppInstance(timeoutCtx, region, domainInstanceKey)
|
||||
if err != nil {
|
||||
// Log error but continue polling
|
||||
fmt.Printf(" ⚠️ Error checking instance state: %v\n", err)
|
||||
|
|
@ -338,14 +426,12 @@ func waitForInstanceReady(ctx context.Context, c *edgeconnect.Client, instanceKe
|
|||
state := strings.ToLower(instance.State)
|
||||
if state != "" && state != "creating" && state != "create requested" {
|
||||
if state == "ready" || state == "running" {
|
||||
fmt.Printf(" ✅ Instance reached ready state: %s\n", instance.State)
|
||||
return instance, nil
|
||||
} else if state == "error" || state == "failed" || strings.Contains(state, "error") {
|
||||
return instance, fmt.Errorf("instance entered error state: %s", instance.State)
|
||||
return *edgeconnect.ToAPIAppInstance(instance), nil } else if state == "error" || state == "failed" || strings.Contains(state, "error") {
|
||||
return *edgeconnect.ToAPIAppInstance(instance), fmt.Errorf("instance entered error state: %s", instance.State)
|
||||
} else {
|
||||
// Instance is in some other stable state (not creating)
|
||||
fmt.Printf(" ✅ Instance reached stable state: %s\n", instance.State)
|
||||
return instance, nil
|
||||
return *edgeconnect.ToAPIAppInstance(instance), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/adapters/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/core/domain"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -76,20 +77,39 @@ func main() {
|
|||
func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client, input *edgeconnect.NewAppInput) error {
|
||||
appKey := input.App.Key
|
||||
region := input.Region
|
||||
var domainAppKey domain.AppKey
|
||||
|
||||
fmt.Printf("🚀 Demonstrating EdgeXR SDK with app: %s/%s v%s\n",
|
||||
appKey.Organization, appKey.Name, appKey.Version)
|
||||
|
||||
// Step 1: Create the application
|
||||
fmt.Println("\n1. Creating application...")
|
||||
if err := edgeClient.CreateApp(ctx, input); err != nil {
|
||||
domainApp := &domain.App{
|
||||
Key: domain.AppKey{
|
||||
Organization: input.App.Key.Organization,
|
||||
Name: input.App.Key.Name,
|
||||
Version: input.App.Key.Version,
|
||||
},
|
||||
Deployment: input.App.Deployment,
|
||||
ImageType: input.App.ImageType,
|
||||
ImagePath: input.App.ImagePath,
|
||||
DefaultFlavor: domain.Flavor{Name: input.App.DefaultFlavor.Name},
|
||||
ServerlessConfig: input.App.ServerlessConfig,
|
||||
AllowServerless: input.App.AllowServerless,
|
||||
}
|
||||
if err := edgeClient.CreateApp(ctx, input.Region, domainApp); err != nil {
|
||||
return fmt.Errorf("failed to create app: %+v", err)
|
||||
}
|
||||
fmt.Printf("✅ App created: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
|
||||
|
||||
// Step 2: Query the application
|
||||
fmt.Println("\n2. Querying application...")
|
||||
app, err := edgeClient.ShowApp(ctx, appKey, region)
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
app, err := edgeClient.ShowApp(ctx, region, domainAppKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to show app: %w", err)
|
||||
}
|
||||
|
|
@ -98,8 +118,8 @@ func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client
|
|||
|
||||
// Step 3: List applications in the organization
|
||||
fmt.Println("\n3. Listing applications...")
|
||||
filter := edgeconnect.AppKey{Organization: appKey.Organization}
|
||||
apps, err := edgeClient.ShowApps(ctx, filter, region)
|
||||
filter := domain.AppKey{Organization: appKey.Organization}
|
||||
apps, err := edgeClient.ShowApps(ctx, region, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list apps: %w", err)
|
||||
}
|
||||
|
|
@ -107,14 +127,24 @@ func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client
|
|||
|
||||
// Step 4: Clean up - delete the application
|
||||
fmt.Println("\n4. Cleaning up...")
|
||||
if err := edgeClient.DeleteApp(ctx, appKey, region); err != nil {
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
if err := edgeClient.DeleteApp(ctx, region, domainAppKey); err != nil {
|
||||
return fmt.Errorf("failed to delete app: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ App deleted: %s/%s v%s\n", appKey.Organization, appKey.Name, appKey.Version)
|
||||
|
||||
// Step 5: Verify deletion
|
||||
fmt.Println("\n5. Verifying deletion...")
|
||||
_, err = edgeClient.ShowApp(ctx, appKey, region)
|
||||
domainAppKey = domain.AppKey{
|
||||
Organization: appKey.Organization,
|
||||
Name: appKey.Name,
|
||||
Version: appKey.Version,
|
||||
}
|
||||
_, err = edgeClient.ShowApp(ctx, region, domainAppKey)
|
||||
if err != nil {
|
||||
if strings.Contains(fmt.Sprintf("%v", err), edgeconnect.ErrResourceNotFound.Error()) {
|
||||
fmt.Printf("✅ App successfully deleted (not found)\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue