Compare commits
12 commits
v2.0.0-pre
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02856be541 | |||
| e38d7e84d5 | |||
| 2909e0d1b4 | |||
| ece2955a2a | |||
| a51e2ae454 | |||
| ece3dddfe6 | |||
| 9772a072e8 | |||
| f3cbfa3723 | |||
| 26ba07200e | |||
| 716c8e79e4 | |||
| 9cb9f97a1f | |||
| 65e0185064 |
61 changed files with 1121 additions and 559 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ dist/
|
|||
### direnv ###
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
edge-connect-client
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ builds:
|
|||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
#- darwin
|
||||
#- windows
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
#- arm64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
|
|
@ -43,9 +43,6 @@ signs:
|
|||
- "--detach-sign"
|
||||
- "${artifact}"
|
||||
|
||||
#binary_signs:
|
||||
# - {}
|
||||
|
||||
changelog:
|
||||
abbrev: 10
|
||||
filters:
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -28,7 +28,7 @@ clean:
|
|||
|
||||
# Lint the code
|
||||
lint:
|
||||
golangci-lint run
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2 run
|
||||
|
||||
# Run all checks (generate, test, lint)
|
||||
check: test lint
|
||||
|
|
|
|||
18
cmd/app.go
18
cmd/app.go
|
|
@ -10,8 +10,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
|
@ -37,7 +37,7 @@ func validateBaseURL(baseURL string) error {
|
|||
return fmt.Errorf("user and or password should not be set")
|
||||
}
|
||||
|
||||
if !(url.Path == "" || url.Path == "/") {
|
||||
if url.Path != "" && url.Path != "/" {
|
||||
return fmt.Errorf("should not contain any path '%s'", url.Path)
|
||||
}
|
||||
|
||||
|
|
@ -291,12 +291,18 @@ func init() {
|
|||
cmd.Flags().StringVarP(&appName, "name", "n", "", "application name")
|
||||
cmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
||||
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
||||
cmd.MarkFlagRequired("org")
|
||||
cmd.MarkFlagRequired("region")
|
||||
if err := cmd.MarkFlagRequired("org"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("region"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add required name flag for specific commands
|
||||
for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} {
|
||||
cmd.MarkFlagRequired("name")
|
||||
if err := cmd.MarkFlagRequired("name"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
cmd/apply.go
28
cmd/apply.go
|
|
@ -10,9 +10,9 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v1"
|
||||
applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v1"
|
||||
applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ the necessary changes to deploy your applications across multiple cloudlets.`,
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if configFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
||||
cmd.Usage()
|
||||
_ = cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -208,20 +208,6 @@ func runApplyV2(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun
|
|||
return displayDeploymentResults(deployResult)
|
||||
}
|
||||
|
||||
type deploymentResult interface {
|
||||
IsSuccess() bool
|
||||
GetDuration() string
|
||||
GetCompletedActions() []actionResult
|
||||
GetFailedActions() []actionResult
|
||||
GetError() error
|
||||
}
|
||||
|
||||
type actionResult interface {
|
||||
GetType() string
|
||||
GetTarget() string
|
||||
GetError() error
|
||||
}
|
||||
|
||||
func displayDeploymentResults(result interface{}) error {
|
||||
// Use reflection or type assertion to handle both v1 and v2 result types
|
||||
// For now, we'll use a simple approach that works with both
|
||||
|
|
@ -288,7 +274,7 @@ func displayDeploymentResultsV2(deployResult *applyv2.ExecutionResult) error {
|
|||
func confirmDeployment() bool {
|
||||
fmt.Print("Do you want to proceed? (yes/no): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
_, _ = fmt.Scanln(&response)
|
||||
|
||||
switch response {
|
||||
case "yes", "y", "YES", "Y":
|
||||
|
|
@ -305,5 +291,7 @@ func init() {
|
|||
applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without applying them")
|
||||
applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan")
|
||||
|
||||
applyCmd.MarkFlagRequired("file")
|
||||
if err := applyCmd.MarkFlagRequired("file"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v1"
|
||||
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v1"
|
||||
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ Instances are always deleted before the application.`,
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if deleteConfigFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
||||
cmd.Usage()
|
||||
_ = cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +273,7 @@ func displayDeletionResultsV2(deleteResult *deletev2.DeletionResult) error {
|
|||
func confirmDeletion() bool {
|
||||
fmt.Print("Do you want to proceed with deletion? (yes/no): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
_, _ = fmt.Scanln(&response)
|
||||
|
||||
switch response {
|
||||
case "yes", "y", "YES", "Y":
|
||||
|
|
@ -290,5 +290,7 @@ func init() {
|
|||
deleteCmd.Flags().BoolVar(&deleteDryRun, "dry-run", false, "preview deletion without actually deleting resources")
|
||||
deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan")
|
||||
|
||||
deleteCmd.MarkFlagRequired("file")
|
||||
if err := deleteCmd.MarkFlagRequired("file"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ var (
|
|||
cloudletOrg string
|
||||
instanceName string
|
||||
flavorName string
|
||||
appId string
|
||||
)
|
||||
|
||||
var appInstanceCmd = &cobra.Command{
|
||||
|
|
@ -104,7 +105,8 @@ var showInstanceCmd = &cobra.Command{
|
|||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
instance, err := c.ShowAppInstance(context.Background(), instanceKey, region)
|
||||
appkey := edgeconnect.AppKey{Name: appId}
|
||||
instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region)
|
||||
if err != nil {
|
||||
fmt.Printf("Error showing app instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -120,7 +122,8 @@ var showInstanceCmd = &cobra.Command{
|
|||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
instance, err := c.ShowAppInstance(context.Background(), instanceKey, region)
|
||||
appkey := v2.AppKey{Name: appId}
|
||||
instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region)
|
||||
if err != nil {
|
||||
fmt.Printf("Error showing app instance: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -146,7 +149,8 @@ var listInstancesCmd = &cobra.Command{
|
|||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
instances, err := c.ShowAppInstances(context.Background(), instanceKey, region)
|
||||
appKey := edgeconnect.AppKey{Name: appId}
|
||||
instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region)
|
||||
if err != nil {
|
||||
fmt.Printf("Error listing app instances: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -165,7 +169,8 @@ var listInstancesCmd = &cobra.Command{
|
|||
Name: cloudletName,
|
||||
},
|
||||
}
|
||||
instances, err := c.ShowAppInstances(context.Background(), instanceKey, region)
|
||||
appKey := v2.AppKey{Name: appId}
|
||||
instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region)
|
||||
if err != nil {
|
||||
fmt.Printf("Error listing app instances: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -229,18 +234,33 @@ func init() {
|
|||
cmd.Flags().StringVarP(&cloudletName, "cloudlet", "c", "", "cloudlet name (required)")
|
||||
cmd.Flags().StringVarP(&cloudletOrg, "cloudlet-org", "", "", "cloudlet organization (required)")
|
||||
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
||||
cmd.Flags().StringVarP(&appId, "app-id", "i", "", "application id")
|
||||
|
||||
cmd.MarkFlagRequired("org")
|
||||
cmd.MarkFlagRequired("name")
|
||||
cmd.MarkFlagRequired("cloudlet")
|
||||
cmd.MarkFlagRequired("cloudlet-org")
|
||||
cmd.MarkFlagRequired("region")
|
||||
if err := cmd.MarkFlagRequired("org"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("name"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("cloudlet"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("cloudlet-org"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("region"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add additional flags for create command
|
||||
createInstanceCmd.Flags().StringVarP(&appName, "app", "a", "", "application name (required)")
|
||||
createInstanceCmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
||||
createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)")
|
||||
createInstanceCmd.MarkFlagRequired("app")
|
||||
createInstanceCmd.MarkFlagRequired("flavor")
|
||||
if err := createInstanceCmd.MarkFlagRequired("app"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := createInstanceCmd.MarkFlagRequired("flavor"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
cmd/root.go
32
cmd/root.go
|
|
@ -44,19 +44,35 @@ func init() {
|
|||
rootCmd.PersistentFlags().StringVar(&apiVersion, "api-version", "v2", "API version to use (v1 or v2)")
|
||||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
|
||||
|
||||
viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
|
||||
viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username"))
|
||||
viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password"))
|
||||
viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version"))
|
||||
if err := viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("EDGE_CONNECT")
|
||||
viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL")
|
||||
viper.BindEnv("username", "EDGE_CONNECT_USERNAME")
|
||||
viper.BindEnv("password", "EDGE_CONNECT_PASSWORD")
|
||||
viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION")
|
||||
if err := viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindEnv("username", "EDGE_CONNECT_USERNAME"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindEnv("password", "EDGE_CONNECT_PASSWORD"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,4 +1,4 @@
|
|||
module edp.buildth.ing/DevFW-CICD/edge-connect-client
|
||||
module edp.buildth.ing/DevFW-CICD/edge-connect-client/v2
|
||||
|
||||
go 1.25.1
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
"fmt"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"testing"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
"strings"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
||||
|
|
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
|
|||
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)
|
||||
ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, 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
|
||||
|
|
@ -323,12 +323,7 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
|
|||
// Extract outbound connections from the app
|
||||
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
||||
for i, conn := range app.RequiredOutboundConnections {
|
||||
current.OutboundConnections[i] = SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
RemoteCIDR: conn.RemoteCIDR,
|
||||
}
|
||||
current.OutboundConnections[i] = SecurityRule(conn)
|
||||
}
|
||||
|
||||
return current, nil
|
||||
|
|
@ -347,8 +342,11 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
|
|||
Name: desired.CloudletName,
|
||||
},
|
||||
}
|
||||
appKey := edgeconnect.AppKey{
|
||||
Name: desired.AppName,
|
||||
}
|
||||
|
||||
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
|
||||
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -392,7 +390,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str
|
|||
// Compare outbound connections
|
||||
outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
|
||||
if len(outboundChanges) > 0 {
|
||||
sb:= strings.Builder{}
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("Outbound connections changed:\n")
|
||||
for _, change := range outboundChanges {
|
||||
sb.WriteString(change)
|
||||
|
|
@ -470,7 +468,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
|
|
@ -505,18 +505,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
|
|||
var duration time.Duration
|
||||
|
||||
// App operations
|
||||
if plan.AppAction.Type == ActionCreate {
|
||||
switch plan.AppAction.Type {
|
||||
case ActionCreate:
|
||||
duration += 30 * time.Second
|
||||
} else if plan.AppAction.Type == ActionUpdate {
|
||||
case ActionUpdate:
|
||||
duration += 15 * time.Second
|
||||
}
|
||||
|
||||
// Instance operations (can be done in parallel)
|
||||
instanceDuration := time.Duration(0)
|
||||
for _, action := range plan.InstanceActions {
|
||||
if action.Type == ActionCreate {
|
||||
switch action.Type {
|
||||
case ActionCreate:
|
||||
instanceDuration = max(instanceDuration, 2*time.Minute)
|
||||
} else if action.Type == ActionUpdate {
|
||||
case ActionUpdate:
|
||||
instanceDuration = max(instanceDuration, 1*time.Minute)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"testing"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect.
|
|||
return args.Get(0).(edgeconnect.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) {
|
||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return edgeconnect.AppInstance{}, args.Error(1)
|
||||
|
|
@ -75,14 +75,6 @@ func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey edgeconnect
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
)
|
||||
|
||||
// DeploymentStrategy represents the type of deployment strategy
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"sync"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// RecreateStrategy implements the recreate deployment strategy
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strings"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// SecurityRule defines network access rules (alias to SDK type for consistency)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ package v2
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
|
|
@ -204,7 +206,8 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re
|
|||
|
||||
rollbackErrors := []error{}
|
||||
|
||||
// Rollback completed instances (in reverse order)
|
||||
// Phase 1: Delete resources that were created in this deployment attempt (in reverse order)
|
||||
rm.logf("Phase 1: Rolling back created resources")
|
||||
for i := len(result.CompletedActions) - 1; i >= 0; i-- {
|
||||
action := result.CompletedActions[i]
|
||||
|
||||
|
|
@ -218,6 +221,32 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 2: Restore resources that were deleted before the failed deployment
|
||||
// This is critical for RecreateStrategy which deletes everything before recreating
|
||||
if result.DeletedAppBackup != nil || len(result.DeletedInstancesBackup) > 0 {
|
||||
rm.logf("Phase 2: Restoring deleted resources")
|
||||
|
||||
// Restore app first (must exist before instances can be created)
|
||||
if result.DeletedAppBackup != nil {
|
||||
if err := rm.restoreApp(ctx, result.DeletedAppBackup); err != nil {
|
||||
rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore app: %w", err))
|
||||
rm.logf("Failed to restore app: %v", err)
|
||||
} else {
|
||||
rm.logf("Successfully restored app: %s", result.DeletedAppBackup.App.Key.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore instances
|
||||
for _, backup := range result.DeletedInstancesBackup {
|
||||
if err := rm.restoreInstance(ctx, &backup); err != nil {
|
||||
rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore instance %s: %w", backup.Instance.Key.Name, err))
|
||||
rm.logf("Failed to restore instance %s: %v", backup.Instance.Key.Name, err)
|
||||
} else {
|
||||
rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(rollbackErrors) > 0 {
|
||||
return fmt.Errorf("rollback encountered %d errors: %v", len(rollbackErrors), rollbackErrors)
|
||||
}
|
||||
|
|
@ -278,6 +307,125 @@ func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, acti
|
|||
return fmt.Errorf("instance action not found for rollback: %s", action.Target)
|
||||
}
|
||||
|
||||
// restoreApp recreates an app that was deleted during deployment
|
||||
func (rm *EdgeConnectResourceManager) restoreApp(ctx context.Context, backup *AppBackup) error {
|
||||
rm.logf("Restoring app: %s/%s version %s",
|
||||
backup.App.Key.Organization, backup.App.Key.Name, backup.App.Key.Version)
|
||||
|
||||
// Build a clean app input with only creation-safe fields
|
||||
// We must exclude read-only fields like CreatedAt, UpdatedAt, etc.
|
||||
appInput := &v2.NewAppInput{
|
||||
Region: backup.Region,
|
||||
App: v2.App{
|
||||
Key: backup.App.Key,
|
||||
Deployment: backup.App.Deployment,
|
||||
ImageType: backup.App.ImageType,
|
||||
ImagePath: backup.App.ImagePath,
|
||||
AllowServerless: backup.App.AllowServerless,
|
||||
DefaultFlavor: backup.App.DefaultFlavor,
|
||||
ServerlessConfig: backup.App.ServerlessConfig,
|
||||
DeploymentManifest: backup.App.DeploymentManifest,
|
||||
DeploymentGenerator: backup.App.DeploymentGenerator,
|
||||
RequiredOutboundConnections: backup.App.RequiredOutboundConnections,
|
||||
// Explicitly omit read-only fields like CreatedAt, UpdatedAt, Fields, etc.
|
||||
},
|
||||
}
|
||||
|
||||
if err := rm.client.CreateApp(ctx, appInput); err != nil {
|
||||
return fmt.Errorf("failed to restore app: %w", err)
|
||||
}
|
||||
|
||||
rm.logf("Successfully restored app: %s", backup.App.Key.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreInstance recreates an instance that was deleted during deployment
|
||||
func (rm *EdgeConnectResourceManager) restoreInstance(ctx context.Context, backup *InstanceBackup) error {
|
||||
rm.logf("Restoring instance: %s on %s:%s",
|
||||
backup.Instance.Key.Name,
|
||||
backup.Instance.Key.CloudletKey.Organization,
|
||||
backup.Instance.Key.CloudletKey.Name)
|
||||
|
||||
// Build a clean instance input with only creation-safe fields
|
||||
// We must exclude read-only fields like CloudletLoc, CreatedAt, etc.
|
||||
instanceInput := &v2.NewAppInstanceInput{
|
||||
Region: backup.Region,
|
||||
AppInst: v2.AppInstance{
|
||||
Key: backup.Instance.Key,
|
||||
AppKey: backup.Instance.AppKey,
|
||||
Flavor: backup.Instance.Flavor,
|
||||
// Explicitly omit read-only fields like CloudletLoc, State, PowerState, CreatedAt, etc.
|
||||
},
|
||||
}
|
||||
|
||||
// Retry logic to handle namespace termination race conditions
|
||||
maxRetries := 5
|
||||
retryDelay := 10 * time.Second
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
rm.logf("Retrying instance restore %s (attempt %d/%d)", backup.Instance.Key.Name, attempt, maxRetries)
|
||||
select {
|
||||
case <-time.After(retryDelay):
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
err := rm.client.CreateAppInstance(ctx, instanceInput)
|
||||
if err == nil {
|
||||
rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// Check if error is retryable
|
||||
if !rm.isRetryableError(err) {
|
||||
rm.logf("Failed to restore instance %s: %v (non-retryable error, giving up)", backup.Instance.Key.Name, err)
|
||||
return fmt.Errorf("failed to restore instance: %w", err)
|
||||
}
|
||||
|
||||
if attempt < maxRetries {
|
||||
rm.logf("Failed to restore instance %s: %v (will retry)", backup.Instance.Key.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to restore instance after %d attempts: %w", maxRetries+1, lastErr)
|
||||
}
|
||||
|
||||
// isRetryableError determines if an error should be retried
|
||||
func (rm *EdgeConnectResourceManager) isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
// Special case: Kubernetes namespace termination race condition
|
||||
// This is a transient 400 error that should be retried
|
||||
if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's an APIError with a status code
|
||||
var apiErr *v2.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
// Don't retry client errors (4xx)
|
||||
if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 {
|
||||
return false
|
||||
}
|
||||
// Retry server errors (5xx)
|
||||
if apiErr.StatusCode >= 500 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Retry all other errors (network issues, timeouts, etc.)
|
||||
return true
|
||||
}
|
||||
|
||||
// logf logs a message if a logger is configured
|
||||
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
||||
if rm.logger != nil {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -464,6 +465,111 @@ func TestRollbackDeploymentFailure(t *testing.T) {
|
|||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestRollbackDeploymentWithRestore(t *testing.T) {
|
||||
mockClient := &MockResourceClient{}
|
||||
logger := &TestLogger{}
|
||||
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
|
||||
|
||||
plan := createTestDeploymentPlan()
|
||||
|
||||
// Simulate a RecreateStrategy scenario:
|
||||
// 1. Old app and instance were deleted and backed up
|
||||
// 2. New app was created successfully
|
||||
// 3. New instance creation failed
|
||||
// 4. Rollback should: delete new app, restore old app, restore old instance
|
||||
oldApp := v2.App{
|
||||
Key: v2.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
DeploymentManifest: "old-manifest-content",
|
||||
}
|
||||
|
||||
oldInstance := v2.AppInstance{
|
||||
Key: v2.AppInstanceKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app-1.0.0-instance",
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: "test-cloudlet-org",
|
||||
Name: "test-cloudlet",
|
||||
},
|
||||
},
|
||||
AppKey: v2.AppKey{
|
||||
Organization: "test-org",
|
||||
Name: "test-app",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Flavor: v2.Flavor{Name: "small"},
|
||||
}
|
||||
|
||||
result := &ExecutionResult{
|
||||
Plan: plan,
|
||||
// Completed actions: new app was created before failure
|
||||
CompletedActions: []ActionResult{
|
||||
{
|
||||
Type: ActionCreate,
|
||||
Target: "test-app",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
// Failed action: new instance creation failed
|
||||
FailedActions: []ActionResult{
|
||||
{
|
||||
Type: ActionCreate,
|
||||
Target: "test-app-1.0.0-instance",
|
||||
Success: false,
|
||||
},
|
||||
},
|
||||
// Backup of deleted resources
|
||||
DeletedAppBackup: &AppBackup{
|
||||
App: oldApp,
|
||||
Region: "US",
|
||||
ManifestContent: "old-manifest-content",
|
||||
},
|
||||
DeletedInstancesBackup: []InstanceBackup{
|
||||
{
|
||||
Instance: oldInstance,
|
||||
Region: "US",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock rollback operations in order:
|
||||
// 1. Delete newly created app (rollback create)
|
||||
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||
Return(nil).Once()
|
||||
|
||||
// 2. Restore old app (from backup)
|
||||
mockClient.On("CreateApp", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInput) bool {
|
||||
return input.App.Key.Name == "test-app" && input.App.DeploymentManifest == "old-manifest-content"
|
||||
})).Return(nil).Once()
|
||||
|
||||
// 3. Restore old instance (from backup)
|
||||
mockClient.On("CreateAppInstance", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInstanceInput) bool {
|
||||
return input.AppInst.Key.Name == "test-app-1.0.0-instance"
|
||||
})).Return(nil).Once()
|
||||
|
||||
ctx := context.Background()
|
||||
err := manager.RollbackDeployment(ctx, result)
|
||||
|
||||
require.NoError(t, err)
|
||||
mockClient.AssertExpectations(t)
|
||||
|
||||
// Verify rollback was logged
|
||||
assert.Greater(t, len(logger.messages), 0)
|
||||
// Should have messages about rolling back created resources and restoring deleted resources
|
||||
hasRestoreLog := false
|
||||
for _, msg := range logger.messages {
|
||||
if strings.Contains(msg, "Restoring deleted resources") {
|
||||
hasRestoreLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasRestoreLog, "Should log restoration of deleted resources")
|
||||
}
|
||||
|
||||
func TestConvertNetworkRules(t *testing.T) {
|
||||
network := &config.NetworkConfig{
|
||||
OutboundConnections: []config.OutboundConnection{
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
||||
|
|
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
|
|||
CreateApp(ctx context.Context, input *v2.NewAppInput) error
|
||||
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
|
||||
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
||||
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error)
|
||||
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error)
|
||||
CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
|
||||
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
|
||||
|
|
@ -323,12 +323,7 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
|
|||
// Extract outbound connections from the app
|
||||
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
||||
for i, conn := range app.RequiredOutboundConnections {
|
||||
current.OutboundConnections[i] = SecurityRule{
|
||||
Protocol: conn.Protocol,
|
||||
PortRangeMin: conn.PortRangeMin,
|
||||
PortRangeMax: conn.PortRangeMax,
|
||||
RemoteCIDR: conn.RemoteCIDR,
|
||||
}
|
||||
current.OutboundConnections[i] = SecurityRule(conn)
|
||||
}
|
||||
|
||||
return current, nil
|
||||
|
|
@ -348,7 +343,9 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
|
|||
},
|
||||
}
|
||||
|
||||
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
|
||||
appKey := v2.AppKey{Name: desired.AppName}
|
||||
|
||||
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -392,7 +389,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str
|
|||
// Compare outbound connections
|
||||
outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
|
||||
if len(outboundChanges) > 0 {
|
||||
sb:= strings.Builder{}
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("Outbound connections changed:\n")
|
||||
for _, change := range outboundChanges {
|
||||
sb.WriteString(change)
|
||||
|
|
@ -470,7 +467,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
|
|
@ -505,18 +504,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
|
|||
var duration time.Duration
|
||||
|
||||
// App operations
|
||||
if plan.AppAction.Type == ActionCreate {
|
||||
switch plan.AppAction.Type {
|
||||
case ActionCreate:
|
||||
duration += 30 * time.Second
|
||||
} else if plan.AppAction.Type == ActionUpdate {
|
||||
case ActionUpdate:
|
||||
duration += 15 * time.Second
|
||||
}
|
||||
|
||||
// Instance operations (can be done in parallel)
|
||||
instanceDuration := time.Duration(0)
|
||||
for _, action := range plan.InstanceActions {
|
||||
if action.Type == ActionCreate {
|
||||
switch action.Type {
|
||||
case ActionCreate:
|
||||
instanceDuration = max(instanceDuration, 2*time.Minute)
|
||||
} else if action.Type == ActionUpdate {
|
||||
case ActionUpdate:
|
||||
instanceDuration = max(instanceDuration, 1*time.Minute)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
|
|||
return args.Get(0).(v2.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error) {
|
||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return v2.AppInstance{}, args.Error(1)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
)
|
||||
|
||||
// DeploymentStrategy represents the type of deployment strategy
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// RecreateStrategy implements the recreate deployment strategy
|
||||
|
|
@ -159,6 +159,19 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo
|
|||
return nil
|
||||
}
|
||||
|
||||
// Backup instances before deleting them (for rollback restoration)
|
||||
r.logf("Backing up %d existing instances before deletion", len(instancesToDelete))
|
||||
for _, action := range instancesToDelete {
|
||||
backup, err := r.backupInstance(ctx, action, config)
|
||||
if err != nil {
|
||||
r.logf("Warning: failed to backup instance %s before deletion: %v", action.InstanceName, err)
|
||||
// Continue with deletion even if backup fails - this is best effort
|
||||
} else {
|
||||
result.DeletedInstancesBackup = append(result.DeletedInstancesBackup, *backup)
|
||||
r.logf("Backed up instance: %s", action.InstanceName)
|
||||
}
|
||||
}
|
||||
|
||||
deleteResults := r.executeInstanceActionsWithRetry(ctx, instancesToDelete, "delete", config)
|
||||
|
||||
for _, deleteResult := range deleteResults {
|
||||
|
|
@ -172,6 +185,19 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo
|
|||
}
|
||||
|
||||
r.logf("Phase 1 complete: deleted %d instances", len(deleteResults))
|
||||
|
||||
// Wait for Kubernetes namespace termination to complete
|
||||
// This prevents "namespace is being terminated" errors when recreating instances
|
||||
if len(deleteResults) > 0 {
|
||||
waitTime := 5 * time.Second
|
||||
r.logf("Waiting %v for namespace termination to complete...", waitTime)
|
||||
select {
|
||||
case <-time.After(waitTime):
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +210,17 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP
|
|||
|
||||
r.logf("Phase 2: Deleting existing application")
|
||||
|
||||
// Backup app before deleting it (for rollback restoration)
|
||||
r.logf("Backing up existing app before deletion")
|
||||
backup, err := r.backupApp(ctx, plan, config)
|
||||
if err != nil {
|
||||
r.logf("Warning: failed to backup app before deletion: %v", err)
|
||||
// Continue with deletion even if backup fails - this is best effort
|
||||
} else {
|
||||
result.DeletedAppBackup = backup
|
||||
r.logf("Backed up app: %s", plan.AppAction.Desired.Name)
|
||||
}
|
||||
|
||||
appKey := v2.AppKey{
|
||||
Organization: plan.AppAction.Desired.Organization,
|
||||
Name: plan.AppAction.Desired.Name,
|
||||
|
|
@ -516,6 +553,54 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// backupApp fetches and stores the current app state before deletion
|
||||
func (r *RecreateStrategy) backupApp(ctx context.Context, plan *DeploymentPlan, config *config.EdgeConnectConfig) (*AppBackup, error) {
|
||||
appKey := v2.AppKey{
|
||||
Organization: plan.AppAction.Desired.Organization,
|
||||
Name: plan.AppAction.Desired.Name,
|
||||
Version: plan.AppAction.Desired.Version,
|
||||
}
|
||||
|
||||
app, err := r.client.ShowApp(ctx, appKey, plan.AppAction.Desired.Region)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch app for backup: %w", err)
|
||||
}
|
||||
|
||||
backup := &AppBackup{
|
||||
App: app,
|
||||
Region: plan.AppAction.Desired.Region,
|
||||
ManifestContent: app.DeploymentManifest,
|
||||
}
|
||||
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
// backupInstance fetches and stores the current instance state before deletion
|
||||
func (r *RecreateStrategy) backupInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (*InstanceBackup, error) {
|
||||
instanceKey := v2.AppInstanceKey{
|
||||
Organization: action.Desired.Organization,
|
||||
Name: action.InstanceName,
|
||||
CloudletKey: v2.CloudletKey{
|
||||
Organization: action.Target.CloudletOrg,
|
||||
Name: action.Target.CloudletName,
|
||||
},
|
||||
}
|
||||
|
||||
appKey := v2.AppKey{Name: action.Desired.AppName}
|
||||
|
||||
instance, err := r.client.ShowAppInstance(ctx, instanceKey, appKey, action.Target.Region)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch instance for backup: %w", err)
|
||||
}
|
||||
|
||||
backup := &InstanceBackup{
|
||||
Instance: instance,
|
||||
Region: action.Target.Region,
|
||||
}
|
||||
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
// logf logs a message if a logger is configured
|
||||
func (r *RecreateStrategy) logf(format string, v ...interface{}) {
|
||||
if r.logger != nil {
|
||||
|
|
@ -530,6 +615,14 @@ func isRetryableError(err error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
// Special case: Kubernetes namespace termination race condition
|
||||
// This is a transient 400 error that should be retried
|
||||
if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's an APIError with a status code
|
||||
var apiErr *v2.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// SecurityRule defines network access rules (alias to SDK type for consistency)
|
||||
|
|
@ -271,6 +271,12 @@ type ExecutionResult struct {
|
|||
|
||||
// RollbackSuccess indicates if rollback was successful
|
||||
RollbackSuccess bool
|
||||
|
||||
// DeletedAppBackup stores the app that was deleted (for rollback restoration)
|
||||
DeletedAppBackup *AppBackup
|
||||
|
||||
// DeletedInstancesBackup stores instances that were deleted (for rollback restoration)
|
||||
DeletedInstancesBackup []InstanceBackup
|
||||
}
|
||||
|
||||
// ActionResult represents the result of executing a single action
|
||||
|
|
@ -294,6 +300,27 @@ type ActionResult struct {
|
|||
Details string
|
||||
}
|
||||
|
||||
// AppBackup stores a deleted app's complete state for rollback restoration
|
||||
type AppBackup struct {
|
||||
// App is the full app object that was deleted
|
||||
App v2.App
|
||||
|
||||
// Region where the app was deployed
|
||||
Region string
|
||||
|
||||
// ManifestContent is the deployment manifest content
|
||||
ManifestContent string
|
||||
}
|
||||
|
||||
// InstanceBackup stores a deleted instance's complete state for rollback restoration
|
||||
type InstanceBackup struct {
|
||||
// Instance is the full instance object that was deleted
|
||||
Instance v2.AppInstance
|
||||
|
||||
// Region where the instance was deployed
|
||||
Region string
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the deployment plan has no actions to perform
|
||||
func (dp *DeploymentPlan) IsEmpty() bool {
|
||||
if dp.AppAction.Type != ActionNone {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import (
|
|||
"fmt"
|
||||
"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/v2/internal/config"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||
type EdgeConnectClientInterface interface {
|
||||
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) ([]edgeconnect.AppInstance, error)
|
||||
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
|
||||
}
|
||||
|
|
@ -154,8 +154,9 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *
|
|||
Name: infra.CloudletName,
|
||||
},
|
||||
}
|
||||
appKey := edgeconnect.AppKey{Name: config.Metadata.Name}
|
||||
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region)
|
||||
if err != nil {
|
||||
// If it's a not found error, just continue
|
||||
if isNotFoundError(err) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// ResourceManagerInterface defines the interface for resource management
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -27,7 +27,7 @@ func (m *MockResourceClient) ShowApp(ctx context.Context, appKey v2.AppKey, regi
|
|||
return args.Get(0).(v2.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
||||
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||
type EdgeConnectClientInterface interface {
|
||||
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error)
|
||||
ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error)
|
||||
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
||||
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
|
||||
}
|
||||
|
|
@ -154,8 +154,9 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *
|
|||
Name: infra.CloudletName,
|
||||
},
|
||||
}
|
||||
appKey := v2.AppKey{Name: config.Metadata.Name}
|
||||
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
|
||||
instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region)
|
||||
if err != nil {
|
||||
// If it's a not found error, just continue
|
||||
if isNotFoundError(err) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -28,7 +28,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
|
|||
return args.Get(0).(v2.App), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
||||
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) {
|
||||
args := m.Called(ctx, instanceKey, region)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -1,6 +1,6 @@
|
|||
package main
|
||||
|
||||
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/cmd"
|
||||
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
|
|
|
|||
BIN
public.gpg
Normal file
BIN
public.gpg
Normal file
Binary file not shown.
|
|
@ -16,7 +16,7 @@ A comprehensive Go SDK for the EdgeXR Master Controller API, providing typed int
|
|||
### Installation
|
||||
|
||||
```go
|
||||
import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
||||
)
|
||||
|
||||
// CreateAppInstance creates a new application instance in the specified region
|
||||
|
|
@ -23,7 +23,9 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateAppInstance")
|
||||
|
|
@ -43,12 +45,12 @@ 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, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{AppKey: appKey, Key: appInstKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
|||
if err != nil {
|
||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
||||
|
|
@ -83,12 +87,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
|||
|
||||
// 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, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +100,9 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
||||
|
|
@ -125,7 +131,9 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
|
|||
if err != nil {
|
||||
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
||||
|
|
@ -152,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
|||
if err != nil {
|
||||
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
||||
|
|
@ -179,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -201,6 +213,10 @@ func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result i
|
|||
var errorMessage string
|
||||
|
||||
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
||||
// On permission denied, Edge API returns just an empty array []!
|
||||
if len(line) == 0 || line[0] == '[' {
|
||||
return fmt.Errorf("%w", ErrFaultyResponsePerhaps403)
|
||||
}
|
||||
// Try parsing as ResultResponse first (error format)
|
||||
var resultResp ResultResponse
|
||||
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -156,6 +156,7 @@ func TestCreateAppInstance(t *testing.T) {
|
|||
func TestShowAppInstance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
appKey AppKey
|
||||
appInstKey AppInstanceKey
|
||||
region string
|
||||
mockStatusCode int
|
||||
|
|
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
Name: "testcloudlet",
|
||||
},
|
||||
},
|
||||
appKey: AppKey{Name: "test-app-id"},
|
||||
region: "us-west",
|
||||
mockStatusCode: 200,
|
||||
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
|
||||
|
|
@ -190,6 +192,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
Name: "testcloudlet",
|
||||
},
|
||||
},
|
||||
appKey: AppKey{Name: "test-app-id"},
|
||||
region: "us-west",
|
||||
mockStatusCode: 404,
|
||||
mockResponse: "",
|
||||
|
|
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
|
|||
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
|
||||
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, appInstances, 2)
|
||||
|
|
@ -361,7 +364,7 @@ func TestUpdateAppInstance(t *testing.T) {
|
|||
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrResourceNotFound indicates the requested resource was not found
|
||||
ErrResourceNotFound = fmt.Errorf("resource not found")
|
||||
ErrFaultyResponsePerhaps403 = fmt.Errorf("faulty response from API, may indicate permission denied")
|
||||
)
|
||||
|
||||
// CreateApp creates a new application in the specified region
|
||||
|
|
@ -28,7 +29,9 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateApp")
|
||||
|
|
@ -55,7 +58,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
|
|||
if err != nil {
|
||||
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
|
|
@ -95,7 +100,9 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowApps failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowApps")
|
||||
|
|
@ -124,7 +131,9 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "UpdateApp")
|
||||
|
|
@ -151,7 +160,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -169,6 +180,10 @@ func (c *Client) parseStreamingResponse(resp *http.Response, result interface{})
|
|||
var responses []Response[App]
|
||||
|
||||
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
||||
// On permission denied, Edge API returns just an empty array []!
|
||||
if len(line) == 0 || line[0] == '[' {
|
||||
return fmt.Errorf("%w", ErrFaultyResponsePerhaps403)
|
||||
}
|
||||
var response Response[App]
|
||||
if err := json.Unmarshal(line, &response); err != nil {
|
||||
return err
|
||||
|
|
@ -238,7 +253,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
|
|||
bodyBytes := []byte{}
|
||||
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||
messages = append(messages, string(bodyBytes))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
|
|||
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
|
|||
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
|
|||
assert.Equal(t, 400, err.StatusCode)
|
||||
assert.Len(t, err.Messages, 2)
|
||||
}
|
||||
|
||||
// Helper function to create a test server that handles streaming JSON responses
|
||||
func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(statusCode)
|
||||
for _, response := range responses {
|
||||
w.Write([]byte(response + "\n"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,9 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// Read response body - same as existing implementation
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) {
|
|||
// Return token
|
||||
response := map[string]string{"token": "dynamic-token-456"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) {
|
|||
// Mock login server that returns error
|
||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Invalid credentials"))
|
||||
_, _ = w.Write([]byte("Invalid credentials"))
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "cached-token-789"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "refreshed-token-999"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "new-token-after-invalidation"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) {
|
|||
// Mock server returning invalid JSON
|
||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("invalid json response"))
|
||||
_, _ = w.Write([]byte("invalid json response"))
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
||||
)
|
||||
|
||||
// CreateCloudlet creates a new cloudlet in the specified region
|
||||
|
|
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateCloudlet")
|
||||
|
|
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
|
|||
if err != nil {
|
||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
|
|
@ -89,7 +93,9 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
||||
|
|
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
|
||||
|
|
@ -189,7 +199,9 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func TestCreateCloudlet(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) {
|
|||
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateAppInstance creates a new application instance in the specified region
|
||||
|
|
@ -25,15 +24,16 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateAppInstance")
|
||||
}
|
||||
|
||||
// Parse streaming JSON response
|
||||
var appInstances []AppInstance
|
||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||
if _, err = parseStreamingResponse[AppInstance](resp); err != nil {
|
||||
return fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ 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, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
|
|
@ -58,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
|||
if err != nil {
|
||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
||||
|
|
@ -71,7 +73,7 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
|||
|
||||
// Parse streaming JSON response
|
||||
var appInstances []AppInstance
|
||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||
if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil {
|
||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -85,12 +87,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
|||
|
||||
// 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, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) {
|
||||
transport := c.getTransport()
|
||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||
|
||||
filter := AppInstanceFilter{
|
||||
AppInstance: AppInstance{Key: appInstKey},
|
||||
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
|
||||
Region: region,
|
||||
}
|
||||
|
||||
|
|
@ -98,18 +100,20 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
||||
}
|
||||
|
||||
var appInstances []AppInstance
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return appInstances, nil // Return empty slice for not found
|
||||
return []AppInstance{}, nil // Return empty slice for not found
|
||||
}
|
||||
|
||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||
var appInstances []AppInstance
|
||||
if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil {
|
||||
return nil, fmt.Errorf("ShowAppInstances failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +131,9 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
|
|||
if err != nil {
|
||||
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
||||
|
|
@ -154,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
|||
if err != nil {
|
||||
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
||||
|
|
@ -181,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -195,88 +205,89 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
|||
}
|
||||
|
||||
// parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances
|
||||
func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result interface{}) error {
|
||||
func parseStreamingResponse[T Message](resp *http.Response) ([]T, error) {
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
return []T{}, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Try parsing as a direct JSON array first (v2 API format)
|
||||
switch v := result.(type) {
|
||||
case *[]AppInstance:
|
||||
var appInstances []AppInstance
|
||||
if err := json.Unmarshal(bodyBytes, &appInstances); err == nil {
|
||||
*v = appInstances
|
||||
return nil
|
||||
}
|
||||
// todo finish check the responses, test them, and make a unify result, probably need
|
||||
// to update the response parameter to the message type e.g. App or AppInst
|
||||
isV2, err := isV2Response(bodyBytes)
|
||||
if err != nil {
|
||||
return []T{}, fmt.Errorf("failed to parse streaming response: %w", err)
|
||||
}
|
||||
|
||||
// Fall back to streaming format (v1 API format)
|
||||
var appInstances []AppInstance
|
||||
var messages []string
|
||||
var hasError bool
|
||||
var errorCode int
|
||||
var errorMessage string
|
||||
|
||||
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
|
||||
// Try parsing as ResultResponse first (error format)
|
||||
var resultResp ResultResponse
|
||||
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
|
||||
if resultResp.IsError() {
|
||||
hasError = true
|
||||
errorCode = resultResp.GetCode()
|
||||
errorMessage = resultResp.GetMessage()
|
||||
if isV2 {
|
||||
resultV2, err := parseStreamingResponseV2[T](resp.StatusCode, bodyBytes)
|
||||
if err != nil {
|
||||
return []T{}, err
|
||||
}
|
||||
return nil
|
||||
return resultV2, nil
|
||||
}
|
||||
|
||||
// Try parsing as Response[AppInstance]
|
||||
var response Response[AppInstance]
|
||||
if err := json.Unmarshal(line, &response); err != nil {
|
||||
return err
|
||||
resultV1, err := parseStreamingResponseV1[T](resp.StatusCode, bodyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.HasData() {
|
||||
appInstances = append(appInstances, response.Data)
|
||||
}
|
||||
if response.IsMessage() {
|
||||
msg := response.Data.GetMessage()
|
||||
messages = append(messages, msg)
|
||||
// Check for error indicators in messages
|
||||
if msg == "CreateError" || msg == "UpdateError" || msg == "DeleteError" {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
if !resultV1.IsSuccessful() {
|
||||
return []T{}, resultV1.Error()
|
||||
}
|
||||
|
||||
// If we detected an error, return it
|
||||
if hasError {
|
||||
apiErr := &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Messages: messages,
|
||||
}
|
||||
if errorCode > 0 {
|
||||
apiErr.StatusCode = errorCode
|
||||
apiErr.Code = fmt.Sprintf("%d", errorCode)
|
||||
}
|
||||
if errorMessage != "" {
|
||||
apiErr.Messages = append([]string{errorMessage}, apiErr.Messages...)
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// Set result based on type
|
||||
switch v := result.(type) {
|
||||
case *[]AppInstance:
|
||||
*v = appInstances
|
||||
default:
|
||||
return fmt.Errorf("unsupported result type: %T", result)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resultV1.GetData(), nil
|
||||
}
|
||||
|
||||
func parseStreamingResponseV1[T Message](statusCode int, bodyBytes []byte) (Responses[T], error) {
|
||||
// Fall back to streaming format (v1 API format)
|
||||
var responses Responses[T]
|
||||
responses.StatusCode = statusCode
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(bodyBytes))
|
||||
for {
|
||||
var d Response[T]
|
||||
if err := decoder.Decode(&d); err != nil {
|
||||
if err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
return Responses[T]{}, fmt.Errorf("error in parsing json object into Message: %w", err)
|
||||
}
|
||||
|
||||
if d.Result.Message != "" && d.Result.Code != 0 {
|
||||
responses.StatusCode = d.Result.Code
|
||||
}
|
||||
|
||||
if strings.Contains(d.Data.GetMessage(), "CreateError") {
|
||||
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "CreateError"))
|
||||
}
|
||||
|
||||
if strings.Contains(d.Data.GetMessage(), "UpdateError") {
|
||||
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "UpdateError"))
|
||||
}
|
||||
|
||||
if strings.Contains(d.Data.GetMessage(), "DeleteError") {
|
||||
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "DeleteError"))
|
||||
}
|
||||
|
||||
responses.Responses = append(responses.Responses, d)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func isV2Response(bodyBytes []byte) (bool, error) {
|
||||
if len(bodyBytes) == 0 {
|
||||
return false, fmt.Errorf("malformatted response body")
|
||||
}
|
||||
|
||||
return bodyBytes[0] == '[', nil
|
||||
}
|
||||
|
||||
func parseStreamingResponseV2[T Message](statusCode int, bodyBytes []byte) ([]T, error) {
|
||||
var result []T
|
||||
if err := json.Unmarshal(bodyBytes, &result); err != nil {
|
||||
return result, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -157,6 +157,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
appInstKey AppInstanceKey
|
||||
appKey AppKey
|
||||
region string
|
||||
mockStatusCode int
|
||||
mockResponse string
|
||||
|
|
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
Name: "testcloudlet",
|
||||
},
|
||||
},
|
||||
appKey: AppKey{Name: "testapp"},
|
||||
region: "us-west",
|
||||
mockStatusCode: 200,
|
||||
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
|
||||
|
|
@ -190,6 +192,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
Name: "testcloudlet",
|
||||
},
|
||||
},
|
||||
appKey: AppKey{Name: "testapp"},
|
||||
region: "us-west",
|
||||
mockStatusCode: 404,
|
||||
mockResponse: "",
|
||||
|
|
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
|
|||
|
||||
// Execute test
|
||||
ctx := context.Background()
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
|
||||
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region)
|
||||
|
||||
// Verify results
|
||||
if tt.expectError {
|
||||
|
|
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
|
|||
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL)
|
||||
ctx := context.Background()
|
||||
|
||||
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
|
||||
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, appInstances, 2)
|
||||
|
|
@ -361,7 +364,7 @@ func TestUpdateAppInstance(t *testing.T) {
|
|||
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -29,7 +27,9 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateApp")
|
||||
|
|
@ -56,7 +56,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
|
|||
if err != nil {
|
||||
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||
|
|
@ -69,7 +71,7 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
|
|||
|
||||
// Parse streaming JSON response
|
||||
var apps []App
|
||||
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||
if apps, err = parseStreamingResponse[App](resp); err != nil {
|
||||
return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -96,18 +98,20 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowApps failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowApps")
|
||||
}
|
||||
|
||||
var apps []App
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return apps, nil // Return empty slice for not found
|
||||
return []App{}, nil // Return empty slice for not found
|
||||
}
|
||||
|
||||
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||
var apps []App
|
||||
if apps, err = parseStreamingResponse[App](resp); err != nil {
|
||||
return nil, fmt.Errorf("ShowApps failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +129,9 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "UpdateApp")
|
||||
|
|
@ -152,7 +158,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -165,70 +173,6 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseStreamingResponse parses the EdgeXR streaming JSON response format
|
||||
func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) error {
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Try parsing as a direct JSON array first (v2 API format)
|
||||
switch v := result.(type) {
|
||||
case *[]App:
|
||||
var apps []App
|
||||
if err := json.Unmarshal(bodyBytes, &apps); err == nil {
|
||||
*v = apps
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to streaming format (v1 API format)
|
||||
var responses []Response[App]
|
||||
var apps []App
|
||||
var messages []string
|
||||
|
||||
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
|
||||
var response Response[App]
|
||||
if err := json.Unmarshal(line, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
responses = append(responses, response)
|
||||
return nil
|
||||
})
|
||||
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
|
||||
// Extract data from responses
|
||||
for _, response := range responses {
|
||||
if response.HasData() {
|
||||
apps = append(apps, response.Data)
|
||||
}
|
||||
if response.IsMessage() {
|
||||
messages = append(messages, response.Data.GetMessage())
|
||||
}
|
||||
}
|
||||
|
||||
// If we have error messages, return them
|
||||
if len(messages) > 0 {
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Messages: messages,
|
||||
}
|
||||
}
|
||||
|
||||
// Set result based on type
|
||||
switch v := result.(type) {
|
||||
case *[]App:
|
||||
*v = apps
|
||||
default:
|
||||
return fmt.Errorf("unsupported result type: %T", result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTransport creates an HTTP transport with current client settings
|
||||
func (c *Client) getTransport() *sdkhttp.Transport {
|
||||
return sdkhttp.NewTransport(
|
||||
|
|
@ -254,7 +198,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
|
|||
bodyBytes := []byte{}
|
||||
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||
messages = append(messages, string(bodyBytes))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
|
|||
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
|
|||
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
|
|||
assert.Equal(t, 400, err.StatusCode)
|
||||
assert.Len(t, err.Messages, 2)
|
||||
}
|
||||
|
||||
// Helper function to create a test server that handles streaming JSON responses
|
||||
func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(statusCode)
|
||||
for _, response := range responses {
|
||||
w.Write([]byte(response + "\n"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,9 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// Read response body - same as existing implementation
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) {
|
|||
// Return token
|
||||
response := map[string]string{"token": "dynamic-token-456"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) {
|
|||
// Mock login server that returns error
|
||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Invalid credentials"))
|
||||
_, _ = w.Write([]byte("Invalid credentials"))
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "cached-token-789"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "refreshed-token-999"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) {
|
|||
callCount++
|
||||
response := map[string]string{"token": "new-token-after-invalidation"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) {
|
|||
// Mock server returning invalid JSON
|
||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("invalid json response"))
|
||||
_, _ = w.Write([]byte("invalid json response"))
|
||||
}))
|
||||
defer loginServer.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
||||
)
|
||||
|
||||
// CreateCloudlet creates a new cloudlet in the specified region
|
||||
|
|
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
|
|||
if err != nil {
|
||||
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return c.handleErrorResponse(resp, "CreateCloudlet")
|
||||
|
|
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
|
|||
if err != nil {
|
||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||
|
|
@ -89,7 +93,9 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
||||
|
|
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
|
|||
if err != nil {
|
||||
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// 404 is acceptable for delete operations (already deleted)
|
||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||
|
|
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
|
||||
|
|
@ -189,7 +199,9 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func TestCreateCloudlet(t *testing.T) {
|
|||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) {
|
|||
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
||||
`
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(response))
|
||||
_, _ = w.Write([]byte(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) {
|
|||
|
||||
w.WriteHeader(tt.mockStatusCode)
|
||||
if tt.mockResponse != "" {
|
||||
w.Write([]byte(tt.mockResponse))
|
||||
_, _ = w.Write([]byte(tt.mockResponse))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ type DeleteAppInstanceInput struct {
|
|||
|
||||
// Response wraps a single API response
|
||||
type Response[T Message] struct {
|
||||
ResultResponse `json:",inline"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
|
|
@ -326,6 +327,7 @@ func (r *ResultResponse) GetCode() int {
|
|||
type Responses[T Message] struct {
|
||||
Responses []Response[T] `json:"responses,omitempty"`
|
||||
StatusCode int `json:"-"`
|
||||
Errors []error `json:"-"`
|
||||
}
|
||||
|
||||
func (r *Responses[T]) GetData() []T {
|
||||
|
|
@ -344,12 +346,15 @@ func (r *Responses[T]) GetMessages() []string {
|
|||
if v.IsMessage() {
|
||||
messages = append(messages, v.Data.GetMessage())
|
||||
}
|
||||
if v.Result.Message != "" {
|
||||
messages = append(messages, v.Result.Message)
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func (r *Responses[T]) IsSuccessful() bool {
|
||||
return r.StatusCode >= 200 && r.StatusCode < 400
|
||||
return len(r.Errors) == 0 && (r.StatusCode >= 200 && r.StatusCode < 400)
|
||||
}
|
||||
|
||||
func (r *Responses[T]) Error() error {
|
||||
|
|
@ -410,3 +415,7 @@ type CloudletResourceUsage struct {
|
|||
Region string `json:"region"`
|
||||
Usage map[string]interface{} `json:"usage"`
|
||||
}
|
||||
|
||||
type ErrorMessage struct {
|
||||
Message string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
name: edgeconnect-coder-deployment
|
||||
#namespace: gitea
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -193,7 +193,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow
|
|||
},
|
||||
}
|
||||
|
||||
instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, config.Region, 5*time.Minute)
|
||||
instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, v2.AppKey{}, config.Region, 5*time.Minute)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for instance ready: %w", err)
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow
|
|||
|
||||
// 6. List Application Instances
|
||||
fmt.Println("\n6️⃣ Listing application instances...")
|
||||
instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, config.Region)
|
||||
instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, v2.AppKey{}, config.Region)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list app instances: %w", err)
|
||||
}
|
||||
|
|
@ -306,7 +306,7 @@ func getEnvOrDefault(key, defaultValue string) string {
|
|||
}
|
||||
|
||||
// waitForInstanceReady polls the instance status until it's no longer "Creating" or timeout
|
||||
func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, region string, timeout time.Duration) (v2.AppInstance, error) {
|
||||
func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string, timeout time.Duration) (v2.AppInstance, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppI
|
|||
return v2.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
|
||||
|
||||
case <-ticker.C:
|
||||
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
|
||||
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, appKey, region)
|
||||
if err != nil {
|
||||
// Log error but continue polling
|
||||
fmt.Printf(" ⚠️ Error checking instance state: %v\n", err)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
29
sdk/examples/forgejo-runner/EdgeConnectConfig_v2.yaml
Normal file
29
sdk/examples/forgejo-runner/EdgeConnectConfig_v2.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Is there a swagger file for the new EdgeConnect API?
|
||||
# How does it differ from the EdgeXR API?
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "forgejo-runner-orca" # name could be used for appName
|
||||
appVersion: "1"
|
||||
organization: "edp2-orca"
|
||||
spec:
|
||||
# dockerApp: # Docker is OBSOLETE
|
||||
# appVersion: "1.0.0"
|
||||
# manifestFile: "./docker-compose.yaml"
|
||||
# image: "https://registry-1.docker.io/library/nginx:latest"
|
||||
k8sApp:
|
||||
manifestFile: "./forgejo-runner-deployment.yaml"
|
||||
infraTemplate:
|
||||
- region: "US"
|
||||
cloudletOrg: "TelekomOp"
|
||||
cloudletName: "gardener-shepherd-test"
|
||||
flavorName: "defualt"
|
||||
network:
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 443
|
||||
portRangeMax: 443
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
29
sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v1.yaml
Normal file
29
sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v1.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Is there a swagger file for the new EdgeConnect API?
|
||||
# How does it differ from the EdgeXR API?
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "edge-ubuntu-buildkit" # name could be used for appName
|
||||
appVersion: "1.0.0"
|
||||
organization: "edp2"
|
||||
spec:
|
||||
# dockerApp: # Docker is OBSOLETE
|
||||
# appVersion: "1.0.0"
|
||||
# manifestFile: "./docker-compose.yaml"
|
||||
# image: "https://registry-1.docker.io/library/nginx:latest"
|
||||
k8sApp:
|
||||
manifestFile: "./k8s-deployment.yaml"
|
||||
infraTemplate:
|
||||
- region: "EU"
|
||||
cloudletOrg: "TelekomOP"
|
||||
cloudletName: "Munich"
|
||||
flavorName: "EU.small"
|
||||
network:
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 443
|
||||
portRangeMax: 443
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
29
sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v2.yaml
Normal file
29
sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v2.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Is there a swagger file for the new EdgeConnect API?
|
||||
# How does it differ from the EdgeXR API?
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "edge-ubuntu-buildkit" # name could be used for appName
|
||||
appVersion: "1"
|
||||
organization: "edp2-orca"
|
||||
spec:
|
||||
# dockerApp: # Docker is OBSOLETE
|
||||
# appVersion: "1.0.0"
|
||||
# manifestFile: "./docker-compose.yaml"
|
||||
# image: "https://registry-1.docker.io/library/nginx:latest"
|
||||
k8sApp:
|
||||
manifestFile: "./k8s-deployment.yaml"
|
||||
infraTemplate:
|
||||
- region: "US"
|
||||
cloudletOrg: "TelekomOp"
|
||||
cloudletName: "gardener-shepherd-test"
|
||||
flavorName: "defualt"
|
||||
network:
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 443
|
||||
portRangeMax: 443
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
57
sdk/examples/ubuntu-buildkit/k8s-deployment.yaml
Normal file
57
sdk/examples/ubuntu-buildkit/k8s-deployment.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Add remote buildx builder:
|
||||
# docker buildx create --use --name sidecar tcp://127.0.0.1:1234
|
||||
|
||||
# Run build:
|
||||
# docker buildx build .
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ubuntu-runner
|
||||
labels:
|
||||
run: ubuntu-runner
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: tcp80
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
selector:
|
||||
run: ubuntu-runner
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ubuntu-runner
|
||||
name: ubuntu-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ubuntu-runner
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: ubuntu-runner
|
||||
annotations:
|
||||
container.apparmor.security.beta.kubernetes.io/buildkitd: unconfined
|
||||
spec:
|
||||
containers:
|
||||
- name: ubuntu
|
||||
image: edp.buildth.ing/devfw-cicd/catthehacker/ubuntu:act-22.04-amd64
|
||||
command:
|
||||
- sleep
|
||||
- 7d
|
||||
- args:
|
||||
- --allow-insecure-entitlement=network.host
|
||||
- --oci-worker-no-process-sandbox
|
||||
- --addr
|
||||
- tcp://127.0.0.1:1234
|
||||
image: moby/buildkit:v0.25.1-rootless
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: buildkitd
|
||||
|
|
@ -162,7 +162,9 @@ func (t *Transport) CallJSON(ctx context.Context, method, url string, body inter
|
|||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// Read response body
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue