Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02856be541 | |||
| e38d7e84d5 | |||
| 2909e0d1b4 | |||
| ece2955a2a | |||
| a51e2ae454 | |||
| ece3dddfe6 | |||
| 9772a072e8 | |||
| f3cbfa3723 | |||
| 26ba07200e |
47 changed files with 1055 additions and 490 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ dist/
|
||||||
### direnv ###
|
### direnv ###
|
||||||
.direnv
|
.direnv
|
||||||
.envrc
|
.envrc
|
||||||
|
|
||||||
|
edge-connect-client
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -28,7 +28,7 @@ clean:
|
||||||
|
|
||||||
# Lint the code
|
# Lint the code
|
||||||
lint:
|
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)
|
# Run all checks (generate, test, lint)
|
||||||
check: test lint
|
check: test lint
|
||||||
|
|
|
||||||
14
cmd/app.go
14
cmd/app.go
|
|
@ -37,7 +37,7 @@ func validateBaseURL(baseURL string) error {
|
||||||
return fmt.Errorf("user and or password should not be set")
|
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)
|
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(&appName, "name", "n", "", "application name")
|
||||||
cmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
cmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
||||||
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
||||||
cmd.MarkFlagRequired("org")
|
if err := cmd.MarkFlagRequired("org"); err != nil {
|
||||||
cmd.MarkFlagRequired("region")
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := cmd.MarkFlagRequired("region"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add required name flag for specific commands
|
// Add required name flag for specific commands
|
||||||
for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} {
|
for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} {
|
||||||
cmd.MarkFlagRequired("name")
|
if err := cmd.MarkFlagRequired("name"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
cmd/apply.go
22
cmd/apply.go
|
|
@ -31,7 +31,7 @@ the necessary changes to deploy your applications across multiple cloudlets.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if configFile == "" {
|
if configFile == "" {
|
||||||
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
||||||
cmd.Usage()
|
_ = cmd.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,20 +208,6 @@ func runApplyV2(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun
|
||||||
return displayDeploymentResults(deployResult)
|
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 {
|
func displayDeploymentResults(result interface{}) error {
|
||||||
// Use reflection or type assertion to handle both v1 and v2 result types
|
// 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
|
// 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 {
|
func confirmDeployment() bool {
|
||||||
fmt.Print("Do you want to proceed? (yes/no): ")
|
fmt.Print("Do you want to proceed? (yes/no): ")
|
||||||
var response string
|
var response string
|
||||||
fmt.Scanln(&response)
|
_, _ = fmt.Scanln(&response)
|
||||||
|
|
||||||
switch response {
|
switch response {
|
||||||
case "yes", "y", "YES", "Y":
|
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(&dryRun, "dry-run", false, "preview changes without applying them")
|
||||||
applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan")
|
applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan")
|
||||||
|
|
||||||
applyCmd.MarkFlagRequired("file")
|
if err := applyCmd.MarkFlagRequired("file"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Instances are always deleted before the application.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if deleteConfigFile == "" {
|
if deleteConfigFile == "" {
|
||||||
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
|
||||||
cmd.Usage()
|
_ = cmd.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,7 +273,7 @@ func displayDeletionResultsV2(deleteResult *deletev2.DeletionResult) error {
|
||||||
func confirmDeletion() bool {
|
func confirmDeletion() bool {
|
||||||
fmt.Print("Do you want to proceed with deletion? (yes/no): ")
|
fmt.Print("Do you want to proceed with deletion? (yes/no): ")
|
||||||
var response string
|
var response string
|
||||||
fmt.Scanln(&response)
|
_, _ = fmt.Scanln(&response)
|
||||||
|
|
||||||
switch response {
|
switch response {
|
||||||
case "yes", "y", "YES", "Y":
|
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(&deleteDryRun, "dry-run", false, "preview deletion without actually deleting resources")
|
||||||
deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan")
|
deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan")
|
||||||
|
|
||||||
deleteCmd.MarkFlagRequired("file")
|
if err := deleteCmd.MarkFlagRequired("file"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ var (
|
||||||
cloudletOrg string
|
cloudletOrg string
|
||||||
instanceName string
|
instanceName string
|
||||||
flavorName string
|
flavorName string
|
||||||
|
appId string
|
||||||
)
|
)
|
||||||
|
|
||||||
var appInstanceCmd = &cobra.Command{
|
var appInstanceCmd = &cobra.Command{
|
||||||
|
|
@ -104,7 +105,8 @@ var showInstanceCmd = &cobra.Command{
|
||||||
Name: cloudletName,
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error showing app instance: %v\n", err)
|
fmt.Printf("Error showing app instance: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -120,7 +122,8 @@ var showInstanceCmd = &cobra.Command{
|
||||||
Name: cloudletName,
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error showing app instance: %v\n", err)
|
fmt.Printf("Error showing app instance: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -146,7 +149,8 @@ var listInstancesCmd = &cobra.Command{
|
||||||
Name: cloudletName,
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error listing app instances: %v\n", err)
|
fmt.Printf("Error listing app instances: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -165,7 +169,8 @@ var listInstancesCmd = &cobra.Command{
|
||||||
Name: cloudletName,
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error listing app instances: %v\n", err)
|
fmt.Printf("Error listing app instances: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -229,18 +234,33 @@ func init() {
|
||||||
cmd.Flags().StringVarP(&cloudletName, "cloudlet", "c", "", "cloudlet name (required)")
|
cmd.Flags().StringVarP(&cloudletName, "cloudlet", "c", "", "cloudlet name (required)")
|
||||||
cmd.Flags().StringVarP(&cloudletOrg, "cloudlet-org", "", "", "cloudlet organization (required)")
|
cmd.Flags().StringVarP(&cloudletOrg, "cloudlet-org", "", "", "cloudlet organization (required)")
|
||||||
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)")
|
||||||
|
cmd.Flags().StringVarP(&appId, "app-id", "i", "", "application id")
|
||||||
|
|
||||||
cmd.MarkFlagRequired("org")
|
if err := cmd.MarkFlagRequired("org"); err != nil {
|
||||||
cmd.MarkFlagRequired("name")
|
panic(err)
|
||||||
cmd.MarkFlagRequired("cloudlet")
|
}
|
||||||
cmd.MarkFlagRequired("cloudlet-org")
|
if err := cmd.MarkFlagRequired("name"); err != nil {
|
||||||
cmd.MarkFlagRequired("region")
|
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
|
// Add additional flags for create command
|
||||||
createInstanceCmd.Flags().StringVarP(&appName, "app", "a", "", "application name (required)")
|
createInstanceCmd.Flags().StringVarP(&appName, "app", "a", "", "application name (required)")
|
||||||
createInstanceCmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
createInstanceCmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
|
||||||
createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)")
|
createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)")
|
||||||
createInstanceCmd.MarkFlagRequired("app")
|
if err := createInstanceCmd.MarkFlagRequired("app"); err != nil {
|
||||||
createInstanceCmd.MarkFlagRequired("flavor")
|
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().StringVar(&apiVersion, "api-version", "v2", "API version to use (v1 or v2)")
|
||||||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
|
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
|
||||||
|
|
||||||
viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
|
if err := viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")); err != nil {
|
||||||
viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username"))
|
panic(err)
|
||||||
viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password"))
|
}
|
||||||
viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version"))
|
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() {
|
func initConfig() {
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
viper.SetEnvPrefix("EDGE_CONNECT")
|
viper.SetEnvPrefix("EDGE_CONNECT")
|
||||||
viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL")
|
if err := viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL"); err != nil {
|
||||||
viper.BindEnv("username", "EDGE_CONNECT_USERNAME")
|
panic(err)
|
||||||
viper.BindEnv("password", "EDGE_CONNECT_PASSWORD")
|
}
|
||||||
viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION")
|
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 != "" {
|
if cfgFile != "" {
|
||||||
viper.SetConfigFile(cfgFile)
|
viper.SetConfigFile(cfgFile)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
|
||||||
CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error
|
CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error
|
||||||
UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error
|
UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error
|
||||||
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) 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
|
CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error
|
||||||
UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error
|
UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error
|
||||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) 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
|
// Extract outbound connections from the app
|
||||||
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
||||||
for i, conn := range app.RequiredOutboundConnections {
|
for i, conn := range app.RequiredOutboundConnections {
|
||||||
current.OutboundConnections[i] = SecurityRule{
|
current.OutboundConnections[i] = SecurityRule(conn)
|
||||||
Protocol: conn.Protocol,
|
|
||||||
PortRangeMin: conn.PortRangeMin,
|
|
||||||
PortRangeMax: conn.PortRangeMax,
|
|
||||||
RemoteCIDR: conn.RemoteCIDR,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return current, nil
|
return current, nil
|
||||||
|
|
@ -347,8 +342,11 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
|
||||||
Name: desired.CloudletName,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +468,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
if _, err := io.Copy(hasher, file); err != nil {
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
|
@ -505,18 +505,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
|
||||||
var duration time.Duration
|
var duration time.Duration
|
||||||
|
|
||||||
// App operations
|
// App operations
|
||||||
if plan.AppAction.Type == ActionCreate {
|
switch plan.AppAction.Type {
|
||||||
|
case ActionCreate:
|
||||||
duration += 30 * time.Second
|
duration += 30 * time.Second
|
||||||
} else if plan.AppAction.Type == ActionUpdate {
|
case ActionUpdate:
|
||||||
duration += 15 * time.Second
|
duration += 15 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance operations (can be done in parallel)
|
// Instance operations (can be done in parallel)
|
||||||
instanceDuration := time.Duration(0)
|
instanceDuration := time.Duration(0)
|
||||||
for _, action := range plan.InstanceActions {
|
for _, action := range plan.InstanceActions {
|
||||||
if action.Type == ActionCreate {
|
switch action.Type {
|
||||||
|
case ActionCreate:
|
||||||
instanceDuration = max(instanceDuration, 2*time.Minute)
|
instanceDuration = max(instanceDuration, 2*time.Minute)
|
||||||
} else if action.Type == ActionUpdate {
|
case ActionUpdate:
|
||||||
instanceDuration = max(instanceDuration, 1*time.Minute)
|
instanceDuration = max(instanceDuration, 1*time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect.
|
||||||
return args.Get(0).(edgeconnect.App), args.Error(1)
|
return args.Get(0).(edgeconnect.App), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) {
|
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error) {
|
||||||
args := m.Called(ctx, instanceKey, region)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return edgeconnect.AppInstance{}, args.Error(1)
|
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)
|
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) {
|
func TestNewPlanner(t *testing.T) {
|
||||||
mockClient := &MockEdgeConnectClient{}
|
mockClient := &MockEdgeConnectClient{}
|
||||||
planner := NewPlanner(mockClient)
|
planner := NewPlanner(mockClient)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
|
||||||
|
|
@ -204,7 +206,8 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re
|
||||||
|
|
||||||
rollbackErrors := []error{}
|
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-- {
|
for i := len(result.CompletedActions) - 1; i >= 0; i-- {
|
||||||
action := result.CompletedActions[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 {
|
if len(rollbackErrors) > 0 {
|
||||||
return fmt.Errorf("rollback encountered %d errors: %v", len(rollbackErrors), rollbackErrors)
|
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)
|
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
|
// logf logs a message if a logger is configured
|
||||||
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
|
||||||
if rm.logger != nil {
|
if rm.logger != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -464,6 +465,111 @@ func TestRollbackDeploymentFailure(t *testing.T) {
|
||||||
mockClient.AssertExpectations(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) {
|
func TestConvertNetworkRules(t *testing.T) {
|
||||||
network := &config.NetworkConfig{
|
network := &config.NetworkConfig{
|
||||||
OutboundConnections: []config.OutboundConnection{
|
OutboundConnections: []config.OutboundConnection{
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
|
||||||
CreateApp(ctx context.Context, input *v2.NewAppInput) error
|
CreateApp(ctx context.Context, input *v2.NewAppInput) error
|
||||||
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
|
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
|
||||||
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) 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
|
CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
|
||||||
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
|
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
|
||||||
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) 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
|
// Extract outbound connections from the app
|
||||||
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
|
||||||
for i, conn := range app.RequiredOutboundConnections {
|
for i, conn := range app.RequiredOutboundConnections {
|
||||||
current.OutboundConnections[i] = SecurityRule{
|
current.OutboundConnections[i] = SecurityRule(conn)
|
||||||
Protocol: conn.Protocol,
|
|
||||||
PortRangeMin: conn.PortRangeMin,
|
|
||||||
PortRangeMax: conn.PortRangeMax,
|
|
||||||
RemoteCIDR: conn.RemoteCIDR,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return current, nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -470,7 +467,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
return "", fmt.Errorf("failed to open manifest file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
if _, err := io.Copy(hasher, file); err != nil {
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
|
@ -505,18 +504,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
|
||||||
var duration time.Duration
|
var duration time.Duration
|
||||||
|
|
||||||
// App operations
|
// App operations
|
||||||
if plan.AppAction.Type == ActionCreate {
|
switch plan.AppAction.Type {
|
||||||
|
case ActionCreate:
|
||||||
duration += 30 * time.Second
|
duration += 30 * time.Second
|
||||||
} else if plan.AppAction.Type == ActionUpdate {
|
case ActionUpdate:
|
||||||
duration += 15 * time.Second
|
duration += 15 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance operations (can be done in parallel)
|
// Instance operations (can be done in parallel)
|
||||||
instanceDuration := time.Duration(0)
|
instanceDuration := time.Duration(0)
|
||||||
for _, action := range plan.InstanceActions {
|
for _, action := range plan.InstanceActions {
|
||||||
if action.Type == ActionCreate {
|
switch action.Type {
|
||||||
|
case ActionCreate:
|
||||||
instanceDuration = max(instanceDuration, 2*time.Minute)
|
instanceDuration = max(instanceDuration, 2*time.Minute)
|
||||||
} else if action.Type == ActionUpdate {
|
case ActionUpdate:
|
||||||
instanceDuration = max(instanceDuration, 1*time.Minute)
|
instanceDuration = max(instanceDuration, 1*time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
|
||||||
return args.Get(0).(v2.App), args.Error(1)
|
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)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return v2.AppInstance{}, args.Error(1)
|
return v2.AppInstance{}, args.Error(1)
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,19 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo
|
||||||
return nil
|
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)
|
deleteResults := r.executeInstanceActionsWithRetry(ctx, instancesToDelete, "delete", config)
|
||||||
|
|
||||||
for _, deleteResult := range deleteResults {
|
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))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +210,17 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP
|
||||||
|
|
||||||
r.logf("Phase 2: Deleting existing application")
|
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{
|
appKey := v2.AppKey{
|
||||||
Organization: plan.AppAction.Desired.Organization,
|
Organization: plan.AppAction.Desired.Organization,
|
||||||
Name: plan.AppAction.Desired.Name,
|
Name: plan.AppAction.Desired.Name,
|
||||||
|
|
@ -516,6 +553,54 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi
|
||||||
return true, nil
|
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
|
// logf logs a message if a logger is configured
|
||||||
func (r *RecreateStrategy) logf(format string, v ...interface{}) {
|
func (r *RecreateStrategy) logf(format string, v ...interface{}) {
|
||||||
if r.logger != nil {
|
if r.logger != nil {
|
||||||
|
|
@ -530,6 +615,14 @@ func isRetryableError(err error) bool {
|
||||||
return false
|
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
|
// Check if it's an APIError with a status code
|
||||||
var apiErr *v2.APIError
|
var apiErr *v2.APIError
|
||||||
if errors.As(err, &apiErr) {
|
if errors.As(err, &apiErr) {
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,12 @@ type ExecutionResult struct {
|
||||||
|
|
||||||
// RollbackSuccess indicates if rollback was successful
|
// RollbackSuccess indicates if rollback was successful
|
||||||
RollbackSuccess bool
|
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
|
// ActionResult represents the result of executing a single action
|
||||||
|
|
@ -294,6 +300,27 @@ type ActionResult struct {
|
||||||
Details string
|
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
|
// IsEmpty returns true if the deployment plan has no actions to perform
|
||||||
func (dp *DeploymentPlan) IsEmpty() bool {
|
func (dp *DeploymentPlan) IsEmpty() bool {
|
||||||
if dp.AppAction.Type != ActionNone {
|
if dp.AppAction.Type != ActionNone {
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,13 @@ func TestValidateExampleStructure(t *testing.T) {
|
||||||
config := &EdgeConnectConfig{
|
config := &EdgeConnectConfig{
|
||||||
Kind: "edgeconnect-deployment",
|
Kind: "edgeconnect-deployment",
|
||||||
Metadata: Metadata{
|
Metadata: Metadata{
|
||||||
Name: "edge-app-demo",
|
Name: "edge-app-demo",
|
||||||
AppVersion: "1.0.0",
|
AppVersion: "1.0.0",
|
||||||
Organization: "edp2",
|
Organization: "edp2",
|
||||||
},
|
},
|
||||||
Spec: Spec{
|
Spec: Spec{
|
||||||
DockerApp: &DockerApp{ // Use DockerApp to avoid manifest file validation
|
DockerApp: &DockerApp{ // Use DockerApp to avoid manifest file validation
|
||||||
Image: "nginx:latest",
|
Image: "nginx:latest",
|
||||||
},
|
},
|
||||||
InfraTemplate: []InfraTemplate{
|
InfraTemplate: []InfraTemplate{
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||||
type EdgeConnectClientInterface interface {
|
type EdgeConnectClientInterface interface {
|
||||||
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
|
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
|
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
|
||||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, 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,
|
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 err != nil {
|
||||||
// If it's a not found error, just continue
|
// If it's a not found error, just continue
|
||||||
if isNotFoundError(err) {
|
if isNotFoundError(err) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func (m *MockResourceClient) ShowApp(ctx context.Context, appKey v2.AppKey, regi
|
||||||
return args.Get(0).(v2.App), args.Error(1)
|
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)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
// EdgeConnectClientInterface defines the methods needed for deletion planning
|
||||||
type EdgeConnectClientInterface interface {
|
type EdgeConnectClientInterface interface {
|
||||||
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
|
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
|
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
||||||
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, 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,
|
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 err != nil {
|
||||||
// If it's a not found error, just continue
|
// If it's a not found error, just continue
|
||||||
if isNotFoundError(err) {
|
if isNotFoundError(err) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
|
||||||
return args.Get(0).(v2.App), args.Error(1)
|
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)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ func TestDeletionPlan_IsEmpty(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty plan with no resources",
|
name: "empty plan with no resources",
|
||||||
plan: &DeletionPlan{
|
plan: &DeletionPlan{
|
||||||
ConfigName: "test-config",
|
ConfigName: "test-config",
|
||||||
AppToDelete: nil,
|
AppToDelete: nil,
|
||||||
InstancesToDelete: []InstanceDeletion{},
|
InstancesToDelete: []InstanceDeletion{},
|
||||||
},
|
},
|
||||||
expected: true,
|
expected: true,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateAppInstance")
|
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
|
// ShowAppInstance retrieves a single application instance by key and region
|
||||||
// Maps to POST /auth/ctrl/ShowAppInst
|
// 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()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
filter := AppInstanceFilter{
|
filter := AppInstanceFilter{
|
||||||
AppInstance: AppInstance{Key: appInstKey},
|
AppInstance: AppInstance{AppKey: appKey, Key: appInstKey},
|
||||||
Region: region,
|
Region: region,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
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
|
// ShowAppInstances retrieves all application instances matching the filter criteria
|
||||||
// Maps to POST /auth/ctrl/ShowAppInst
|
// 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()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
filter := AppInstanceFilter{
|
filter := AppInstanceFilter{
|
||||||
AppInstance: AppInstance{Key: appInstKey},
|
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
|
||||||
Region: region,
|
Region: region,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +100,9 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
||||||
|
|
@ -125,7 +131,9 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
||||||
|
|
@ -152,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
||||||
|
|
@ -179,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
|
@ -201,6 +213,10 @@ func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result i
|
||||||
var errorMessage string
|
var errorMessage string
|
||||||
|
|
||||||
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
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)
|
// Try parsing as ResultResponse first (error format)
|
||||||
var resultResp ResultResponse
|
var resultResp ResultResponse
|
||||||
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
|
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"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -156,6 +156,7 @@ func TestCreateAppInstance(t *testing.T) {
|
||||||
func TestShowAppInstance(t *testing.T) {
|
func TestShowAppInstance(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
appKey AppKey
|
||||||
appInstKey AppInstanceKey
|
appInstKey AppInstanceKey
|
||||||
region string
|
region string
|
||||||
mockStatusCode int
|
mockStatusCode int
|
||||||
|
|
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
Name: "testcloudlet",
|
Name: "testcloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appKey: AppKey{Name: "test-app-id"},
|
||||||
region: "us-west",
|
region: "us-west",
|
||||||
mockStatusCode: 200,
|
mockStatusCode: 200,
|
||||||
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
|
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",
|
Name: "testcloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appKey: AppKey{Name: "test-app-id"},
|
||||||
region: "us-west",
|
region: "us-west",
|
||||||
mockStatusCode: 404,
|
mockStatusCode: 404,
|
||||||
mockResponse: "",
|
mockResponse: "",
|
||||||
|
|
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
|
|
||||||
// Execute test
|
// Execute test
|
||||||
ctx := context.Background()
|
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
|
// Verify results
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
|
|
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient(server.URL)
|
client := NewClient(server.URL)
|
||||||
ctx := context.Background()
|
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)
|
require.NoError(t, err)
|
||||||
assert.Len(t, appInstances, 2)
|
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)
|
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrResourceNotFound indicates the requested resource was not found
|
// ErrResourceNotFound indicates the requested resource was not found
|
||||||
ErrResourceNotFound = fmt.Errorf("resource 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
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateApp failed: %w", err)
|
return fmt.Errorf("CreateApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateApp")
|
return c.handleErrorResponse(resp, "CreateApp")
|
||||||
|
|
@ -55,7 +58,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowApps failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowApps")
|
return nil, c.handleErrorResponse(resp, "ShowApps")
|
||||||
|
|
@ -124,7 +131,9 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("UpdateApp failed: %w", err)
|
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "UpdateApp")
|
return c.handleErrorResponse(resp, "UpdateApp")
|
||||||
|
|
@ -151,7 +160,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteApp failed: %w", err)
|
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
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]
|
var responses []Response[App]
|
||||||
|
|
||||||
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
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]
|
var response Response[App]
|
||||||
if err := json.Unmarshal(line, &response); err != nil {
|
if err := json.Unmarshal(line, &response); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -238,7 +253,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
|
||||||
bodyBytes := []byte{}
|
bodyBytes := []byte{}
|
||||||
|
|
||||||
if resp.Body != nil {
|
if resp.Body != nil {
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
bodyBytes, _ = io.ReadAll(resp.Body)
|
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||||
messages = append(messages, string(bodyBytes))
|
messages = append(messages, string(bodyBytes))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
|
||||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
|
||||||
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
|
||||||
assert.Equal(t, 400, err.StatusCode)
|
assert.Equal(t, 400, err.StatusCode)
|
||||||
assert.Len(t, err.Messages, 2)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// Read response body - same as existing implementation
|
// Read response body - same as existing implementation
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) {
|
||||||
// Return token
|
// Return token
|
||||||
response := map[string]string{"token": "dynamic-token-456"}
|
response := map[string]string{"token": "dynamic-token-456"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) {
|
||||||
// Mock login server that returns error
|
// Mock login server that returns error
|
||||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte("Invalid credentials"))
|
_, _ = w.Write([]byte("Invalid credentials"))
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "cached-token-789"}
|
response := map[string]string{"token": "cached-token-789"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "refreshed-token-999"}
|
response := map[string]string{"token": "refreshed-token-999"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "new-token-after-invalidation"}
|
response := map[string]string{"token": "new-token-after-invalidation"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) {
|
||||||
// Mock server returning invalid JSON
|
// Mock server returning invalid JSON
|
||||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([]byte("invalid json response"))
|
_, _ = w.Write([]byte("invalid json response"))
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateCloudlet")
|
return c.handleErrorResponse(resp, "CreateCloudlet")
|
||||||
|
|
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
||||||
|
|
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
|
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",
|
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"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
|
||||||
|
|
@ -60,74 +60,74 @@ const (
|
||||||
|
|
||||||
// AppInstance field constants for partial updates (based on EdgeXR API specification)
|
// AppInstance field constants for partial updates (based on EdgeXR API specification)
|
||||||
const (
|
const (
|
||||||
AppInstFieldKey = "2"
|
AppInstFieldKey = "2"
|
||||||
AppInstFieldKeyAppKey = "2.1"
|
AppInstFieldKeyAppKey = "2.1"
|
||||||
AppInstFieldKeyAppKeyOrganization = "2.1.1"
|
AppInstFieldKeyAppKeyOrganization = "2.1.1"
|
||||||
AppInstFieldKeyAppKeyName = "2.1.2"
|
AppInstFieldKeyAppKeyName = "2.1.2"
|
||||||
AppInstFieldKeyAppKeyVersion = "2.1.3"
|
AppInstFieldKeyAppKeyVersion = "2.1.3"
|
||||||
AppInstFieldKeyClusterInstKey = "2.4"
|
AppInstFieldKeyClusterInstKey = "2.4"
|
||||||
AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1"
|
AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1"
|
||||||
AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1"
|
AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2"
|
AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1"
|
AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2"
|
AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3"
|
AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3"
|
||||||
AppInstFieldKeyClusterInstKeyOrganization = "2.4.3"
|
AppInstFieldKeyClusterInstKeyOrganization = "2.4.3"
|
||||||
AppInstFieldCloudletLoc = "3"
|
AppInstFieldCloudletLoc = "3"
|
||||||
AppInstFieldCloudletLocLatitude = "3.1"
|
AppInstFieldCloudletLocLatitude = "3.1"
|
||||||
AppInstFieldCloudletLocLongitude = "3.2"
|
AppInstFieldCloudletLocLongitude = "3.2"
|
||||||
AppInstFieldCloudletLocHorizontalAccuracy = "3.3"
|
AppInstFieldCloudletLocHorizontalAccuracy = "3.3"
|
||||||
AppInstFieldCloudletLocVerticalAccuracy = "3.4"
|
AppInstFieldCloudletLocVerticalAccuracy = "3.4"
|
||||||
AppInstFieldCloudletLocAltitude = "3.5"
|
AppInstFieldCloudletLocAltitude = "3.5"
|
||||||
AppInstFieldCloudletLocCourse = "3.6"
|
AppInstFieldCloudletLocCourse = "3.6"
|
||||||
AppInstFieldCloudletLocSpeed = "3.7"
|
AppInstFieldCloudletLocSpeed = "3.7"
|
||||||
AppInstFieldCloudletLocTimestamp = "3.8"
|
AppInstFieldCloudletLocTimestamp = "3.8"
|
||||||
AppInstFieldCloudletLocTimestampSeconds = "3.8.1"
|
AppInstFieldCloudletLocTimestampSeconds = "3.8.1"
|
||||||
AppInstFieldCloudletLocTimestampNanos = "3.8.2"
|
AppInstFieldCloudletLocTimestampNanos = "3.8.2"
|
||||||
AppInstFieldUri = "4"
|
AppInstFieldUri = "4"
|
||||||
AppInstFieldLiveness = "6"
|
AppInstFieldLiveness = "6"
|
||||||
AppInstFieldMappedPorts = "9"
|
AppInstFieldMappedPorts = "9"
|
||||||
AppInstFieldMappedPortsProto = "9.1"
|
AppInstFieldMappedPortsProto = "9.1"
|
||||||
AppInstFieldMappedPortsInternalPort = "9.2"
|
AppInstFieldMappedPortsInternalPort = "9.2"
|
||||||
AppInstFieldMappedPortsPublicPort = "9.3"
|
AppInstFieldMappedPortsPublicPort = "9.3"
|
||||||
AppInstFieldMappedPortsFqdnPrefix = "9.5"
|
AppInstFieldMappedPortsFqdnPrefix = "9.5"
|
||||||
AppInstFieldMappedPortsEndPort = "9.6"
|
AppInstFieldMappedPortsEndPort = "9.6"
|
||||||
AppInstFieldMappedPortsTls = "9.7"
|
AppInstFieldMappedPortsTls = "9.7"
|
||||||
AppInstFieldMappedPortsNginx = "9.8"
|
AppInstFieldMappedPortsNginx = "9.8"
|
||||||
AppInstFieldMappedPortsMaxPktSize = "9.9"
|
AppInstFieldMappedPortsMaxPktSize = "9.9"
|
||||||
AppInstFieldFlavor = "12"
|
AppInstFieldFlavor = "12"
|
||||||
AppInstFieldFlavorName = "12.1"
|
AppInstFieldFlavorName = "12.1"
|
||||||
AppInstFieldState = "14"
|
AppInstFieldState = "14"
|
||||||
AppInstFieldErrors = "15"
|
AppInstFieldErrors = "15"
|
||||||
AppInstFieldCrmOverride = "16"
|
AppInstFieldCrmOverride = "16"
|
||||||
AppInstFieldRuntimeInfo = "17"
|
AppInstFieldRuntimeInfo = "17"
|
||||||
AppInstFieldRuntimeInfoContainerIds = "17.1"
|
AppInstFieldRuntimeInfoContainerIds = "17.1"
|
||||||
AppInstFieldCreatedAt = "21"
|
AppInstFieldCreatedAt = "21"
|
||||||
AppInstFieldCreatedAtSeconds = "21.1"
|
AppInstFieldCreatedAtSeconds = "21.1"
|
||||||
AppInstFieldCreatedAtNanos = "21.2"
|
AppInstFieldCreatedAtNanos = "21.2"
|
||||||
AppInstFieldAutoClusterIpAccess = "22"
|
AppInstFieldAutoClusterIpAccess = "22"
|
||||||
AppInstFieldRevision = "24"
|
AppInstFieldRevision = "24"
|
||||||
AppInstFieldForceUpdate = "25"
|
AppInstFieldForceUpdate = "25"
|
||||||
AppInstFieldUpdateMultiple = "26"
|
AppInstFieldUpdateMultiple = "26"
|
||||||
AppInstFieldConfigs = "27"
|
AppInstFieldConfigs = "27"
|
||||||
AppInstFieldConfigsKind = "27.1"
|
AppInstFieldConfigsKind = "27.1"
|
||||||
AppInstFieldConfigsConfig = "27.2"
|
AppInstFieldConfigsConfig = "27.2"
|
||||||
AppInstFieldHealthCheck = "29"
|
AppInstFieldHealthCheck = "29"
|
||||||
AppInstFieldPowerState = "31"
|
AppInstFieldPowerState = "31"
|
||||||
AppInstFieldExternalVolumeSize = "32"
|
AppInstFieldExternalVolumeSize = "32"
|
||||||
AppInstFieldAvailabilityZone = "33"
|
AppInstFieldAvailabilityZone = "33"
|
||||||
AppInstFieldVmFlavor = "34"
|
AppInstFieldVmFlavor = "34"
|
||||||
AppInstFieldOptRes = "35"
|
AppInstFieldOptRes = "35"
|
||||||
AppInstFieldUpdatedAt = "36"
|
AppInstFieldUpdatedAt = "36"
|
||||||
AppInstFieldUpdatedAtSeconds = "36.1"
|
AppInstFieldUpdatedAtSeconds = "36.1"
|
||||||
AppInstFieldUpdatedAtNanos = "36.2"
|
AppInstFieldUpdatedAtNanos = "36.2"
|
||||||
AppInstFieldRealClusterName = "37"
|
AppInstFieldRealClusterName = "37"
|
||||||
AppInstFieldInternalPortToLbIp = "38"
|
AppInstFieldInternalPortToLbIp = "38"
|
||||||
AppInstFieldInternalPortToLbIpKey = "38.1"
|
AppInstFieldInternalPortToLbIpKey = "38.1"
|
||||||
AppInstFieldInternalPortToLbIpValue = "38.2"
|
AppInstFieldInternalPortToLbIpValue = "38.2"
|
||||||
AppInstFieldDedicatedIp = "39"
|
AppInstFieldDedicatedIp = "39"
|
||||||
AppInstFieldUniqueId = "40"
|
AppInstFieldUniqueId = "40"
|
||||||
AppInstFieldDnsLabel = "41"
|
AppInstFieldDnsLabel = "41"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message interface for types that can provide error messages
|
// Message interface for types that can provide error messages
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateAppInstance creates a new application instance in the specified region
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateAppInstance")
|
return c.handleErrorResponse(resp, "CreateAppInstance")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse streaming JSON response
|
// Parse streaming JSON response
|
||||||
var appInstances []AppInstance
|
if _, err = parseStreamingResponse[AppInstance](resp); err != nil {
|
||||||
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
|
||||||
return fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
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
|
// ShowAppInstance retrieves a single application instance by key and region
|
||||||
// Maps to POST /auth/ctrl/ShowAppInst
|
// 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()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
|
|
@ -58,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
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
|
// Parse streaming JSON response
|
||||||
var appInstances []AppInstance
|
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)
|
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
|
// ShowAppInstances retrieves all application instances matching the filter criteria
|
||||||
// Maps to POST /auth/ctrl/ShowAppInst
|
// 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()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
filter := AppInstanceFilter{
|
filter := AppInstanceFilter{
|
||||||
AppInstance: AppInstance{Key: appInstKey},
|
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
|
||||||
Region: region,
|
Region: region,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,18 +100,20 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
||||||
}
|
}
|
||||||
|
|
||||||
var appInstances []AppInstance
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
||||||
|
|
@ -154,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
||||||
|
|
@ -181,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
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
|
// 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)
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
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)
|
// todo finish check the responses, test them, and make a unify result, probably need
|
||||||
switch v := result.(type) {
|
// to update the response parameter to the message type e.g. App or AppInst
|
||||||
case *[]AppInstance:
|
isV2, err := isV2Response(bodyBytes)
|
||||||
var appInstances []AppInstance
|
if err != nil {
|
||||||
if err := json.Unmarshal(bodyBytes, &appInstances); err == nil {
|
return []T{}, fmt.Errorf("failed to parse streaming response: %w", err)
|
||||||
*v = appInstances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to streaming format (v1 API format)
|
if isV2 {
|
||||||
var appInstances []AppInstance
|
resultV2, err := parseStreamingResponseV2[T](resp.StatusCode, bodyBytes)
|
||||||
var messages []string
|
if err != nil {
|
||||||
var hasError bool
|
return []T{}, err
|
||||||
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()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return resultV2, nil
|
||||||
// Try parsing as Response[AppInstance]
|
|
||||||
var response Response[AppInstance]
|
|
||||||
if err := json.Unmarshal(line, &response); err != nil {
|
|
||||||
return 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 we detected an error, return it
|
resultV1, err := parseStreamingResponseV1[T](resp.StatusCode, bodyBytes)
|
||||||
if hasError {
|
if err != nil {
|
||||||
apiErr := &APIError{
|
return nil, err
|
||||||
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
|
if !resultV1.IsSuccessful() {
|
||||||
switch v := result.(type) {
|
return []T{}, resultV1.Error()
|
||||||
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"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -157,6 +157,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
appInstKey AppInstanceKey
|
appInstKey AppInstanceKey
|
||||||
|
appKey AppKey
|
||||||
region string
|
region string
|
||||||
mockStatusCode int
|
mockStatusCode int
|
||||||
mockResponse string
|
mockResponse string
|
||||||
|
|
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
Name: "testcloudlet",
|
Name: "testcloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appKey: AppKey{Name: "testapp"},
|
||||||
region: "us-west",
|
region: "us-west",
|
||||||
mockStatusCode: 200,
|
mockStatusCode: 200,
|
||||||
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
|
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",
|
Name: "testcloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appKey: AppKey{Name: "testapp"},
|
||||||
region: "us-west",
|
region: "us-west",
|
||||||
mockStatusCode: 404,
|
mockStatusCode: 404,
|
||||||
mockResponse: "",
|
mockResponse: "",
|
||||||
|
|
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
|
||||||
|
|
||||||
// Execute test
|
// Execute test
|
||||||
ctx := context.Background()
|
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
|
// Verify results
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
|
|
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient(server.URL)
|
client := NewClient(server.URL)
|
||||||
ctx := context.Background()
|
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)
|
require.NoError(t, err)
|
||||||
assert.Len(t, appInstances, 2)
|
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)
|
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@
|
||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -29,7 +27,9 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateApp failed: %w", err)
|
return fmt.Errorf("CreateApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateApp")
|
return c.handleErrorResponse(resp, "CreateApp")
|
||||||
|
|
@ -56,7 +56,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
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
|
// Parse streaming JSON response
|
||||||
var apps []App
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowApps failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowApps")
|
return nil, c.handleErrorResponse(resp, "ShowApps")
|
||||||
}
|
}
|
||||||
|
|
||||||
var apps []App
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("UpdateApp failed: %w", err)
|
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "UpdateApp")
|
return c.handleErrorResponse(resp, "UpdateApp")
|
||||||
|
|
@ -152,7 +158,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteApp failed: %w", err)
|
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
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
|
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
|
// getTransport creates an HTTP transport with current client settings
|
||||||
func (c *Client) getTransport() *sdkhttp.Transport {
|
func (c *Client) getTransport() *sdkhttp.Transport {
|
||||||
return sdkhttp.NewTransport(
|
return sdkhttp.NewTransport(
|
||||||
|
|
@ -254,7 +198,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
|
||||||
bodyBytes := []byte{}
|
bodyBytes := []byte{}
|
||||||
|
|
||||||
if resp.Body != nil {
|
if resp.Body != nil {
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
bodyBytes, _ = io.ReadAll(resp.Body)
|
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||||
messages = append(messages, string(bodyBytes))
|
messages = append(messages, string(bodyBytes))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
|
||||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
|
||||||
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
|
||||||
assert.Equal(t, 400, err.StatusCode)
|
assert.Equal(t, 400, err.StatusCode)
|
||||||
assert.Len(t, err.Messages, 2)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// Read response body - same as existing implementation
|
// Read response body - same as existing implementation
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) {
|
||||||
// Return token
|
// Return token
|
||||||
response := map[string]string{"token": "dynamic-token-456"}
|
response := map[string]string{"token": "dynamic-token-456"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) {
|
||||||
// Mock login server that returns error
|
// Mock login server that returns error
|
||||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte("Invalid credentials"))
|
_, _ = w.Write([]byte("Invalid credentials"))
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "cached-token-789"}
|
response := map[string]string{"token": "cached-token-789"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "refreshed-token-999"}
|
response := map[string]string{"token": "refreshed-token-999"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) {
|
||||||
callCount++
|
callCount++
|
||||||
response := map[string]string{"token": "new-token-after-invalidation"}
|
response := map[string]string{"token": "new-token-after-invalidation"}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
_ = json.NewEncoder(w).Encode(response)
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) {
|
||||||
// Mock server returning invalid JSON
|
// Mock server returning invalid JSON
|
||||||
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([]byte("invalid json response"))
|
_, _ = w.Write([]byte("invalid json response"))
|
||||||
}))
|
}))
|
||||||
defer loginServer.Close()
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return c.handleErrorResponse(resp, "CreateCloudlet")
|
return c.handleErrorResponse(resp, "CreateCloudlet")
|
||||||
|
|
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
|
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 {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
||||||
|
|
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// 404 is acceptable for delete operations (already deleted)
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
|
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",
|
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"))
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) {
|
||||||
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
||||||
`
|
`
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
w.Write([]byte(response))
|
_, _ = w.Write([]byte(response))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
|
|
@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) {
|
||||||
|
|
||||||
w.WriteHeader(tt.mockStatusCode)
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
if tt.mockResponse != "" {
|
if tt.mockResponse != "" {
|
||||||
w.Write([]byte(tt.mockResponse))
|
_, _ = w.Write([]byte(tt.mockResponse))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
|
||||||
|
|
@ -60,74 +60,74 @@ const (
|
||||||
|
|
||||||
// AppInstance field constants for partial updates (based on EdgeXR API specification)
|
// AppInstance field constants for partial updates (based on EdgeXR API specification)
|
||||||
const (
|
const (
|
||||||
AppInstFieldKey = "2"
|
AppInstFieldKey = "2"
|
||||||
AppInstFieldKeyAppKey = "2.1"
|
AppInstFieldKeyAppKey = "2.1"
|
||||||
AppInstFieldKeyAppKeyOrganization = "2.1.1"
|
AppInstFieldKeyAppKeyOrganization = "2.1.1"
|
||||||
AppInstFieldKeyAppKeyName = "2.1.2"
|
AppInstFieldKeyAppKeyName = "2.1.2"
|
||||||
AppInstFieldKeyAppKeyVersion = "2.1.3"
|
AppInstFieldKeyAppKeyVersion = "2.1.3"
|
||||||
AppInstFieldKeyClusterInstKey = "2.4"
|
AppInstFieldKeyClusterInstKey = "2.4"
|
||||||
AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1"
|
AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1"
|
||||||
AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1"
|
AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2"
|
AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1"
|
AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2"
|
AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2"
|
||||||
AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3"
|
AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3"
|
||||||
AppInstFieldKeyClusterInstKeyOrganization = "2.4.3"
|
AppInstFieldKeyClusterInstKeyOrganization = "2.4.3"
|
||||||
AppInstFieldCloudletLoc = "3"
|
AppInstFieldCloudletLoc = "3"
|
||||||
AppInstFieldCloudletLocLatitude = "3.1"
|
AppInstFieldCloudletLocLatitude = "3.1"
|
||||||
AppInstFieldCloudletLocLongitude = "3.2"
|
AppInstFieldCloudletLocLongitude = "3.2"
|
||||||
AppInstFieldCloudletLocHorizontalAccuracy = "3.3"
|
AppInstFieldCloudletLocHorizontalAccuracy = "3.3"
|
||||||
AppInstFieldCloudletLocVerticalAccuracy = "3.4"
|
AppInstFieldCloudletLocVerticalAccuracy = "3.4"
|
||||||
AppInstFieldCloudletLocAltitude = "3.5"
|
AppInstFieldCloudletLocAltitude = "3.5"
|
||||||
AppInstFieldCloudletLocCourse = "3.6"
|
AppInstFieldCloudletLocCourse = "3.6"
|
||||||
AppInstFieldCloudletLocSpeed = "3.7"
|
AppInstFieldCloudletLocSpeed = "3.7"
|
||||||
AppInstFieldCloudletLocTimestamp = "3.8"
|
AppInstFieldCloudletLocTimestamp = "3.8"
|
||||||
AppInstFieldCloudletLocTimestampSeconds = "3.8.1"
|
AppInstFieldCloudletLocTimestampSeconds = "3.8.1"
|
||||||
AppInstFieldCloudletLocTimestampNanos = "3.8.2"
|
AppInstFieldCloudletLocTimestampNanos = "3.8.2"
|
||||||
AppInstFieldUri = "4"
|
AppInstFieldUri = "4"
|
||||||
AppInstFieldLiveness = "6"
|
AppInstFieldLiveness = "6"
|
||||||
AppInstFieldMappedPorts = "9"
|
AppInstFieldMappedPorts = "9"
|
||||||
AppInstFieldMappedPortsProto = "9.1"
|
AppInstFieldMappedPortsProto = "9.1"
|
||||||
AppInstFieldMappedPortsInternalPort = "9.2"
|
AppInstFieldMappedPortsInternalPort = "9.2"
|
||||||
AppInstFieldMappedPortsPublicPort = "9.3"
|
AppInstFieldMappedPortsPublicPort = "9.3"
|
||||||
AppInstFieldMappedPortsFqdnPrefix = "9.5"
|
AppInstFieldMappedPortsFqdnPrefix = "9.5"
|
||||||
AppInstFieldMappedPortsEndPort = "9.6"
|
AppInstFieldMappedPortsEndPort = "9.6"
|
||||||
AppInstFieldMappedPortsTls = "9.7"
|
AppInstFieldMappedPortsTls = "9.7"
|
||||||
AppInstFieldMappedPortsNginx = "9.8"
|
AppInstFieldMappedPortsNginx = "9.8"
|
||||||
AppInstFieldMappedPortsMaxPktSize = "9.9"
|
AppInstFieldMappedPortsMaxPktSize = "9.9"
|
||||||
AppInstFieldFlavor = "12"
|
AppInstFieldFlavor = "12"
|
||||||
AppInstFieldFlavorName = "12.1"
|
AppInstFieldFlavorName = "12.1"
|
||||||
AppInstFieldState = "14"
|
AppInstFieldState = "14"
|
||||||
AppInstFieldErrors = "15"
|
AppInstFieldErrors = "15"
|
||||||
AppInstFieldCrmOverride = "16"
|
AppInstFieldCrmOverride = "16"
|
||||||
AppInstFieldRuntimeInfo = "17"
|
AppInstFieldRuntimeInfo = "17"
|
||||||
AppInstFieldRuntimeInfoContainerIds = "17.1"
|
AppInstFieldRuntimeInfoContainerIds = "17.1"
|
||||||
AppInstFieldCreatedAt = "21"
|
AppInstFieldCreatedAt = "21"
|
||||||
AppInstFieldCreatedAtSeconds = "21.1"
|
AppInstFieldCreatedAtSeconds = "21.1"
|
||||||
AppInstFieldCreatedAtNanos = "21.2"
|
AppInstFieldCreatedAtNanos = "21.2"
|
||||||
AppInstFieldAutoClusterIpAccess = "22"
|
AppInstFieldAutoClusterIpAccess = "22"
|
||||||
AppInstFieldRevision = "24"
|
AppInstFieldRevision = "24"
|
||||||
AppInstFieldForceUpdate = "25"
|
AppInstFieldForceUpdate = "25"
|
||||||
AppInstFieldUpdateMultiple = "26"
|
AppInstFieldUpdateMultiple = "26"
|
||||||
AppInstFieldConfigs = "27"
|
AppInstFieldConfigs = "27"
|
||||||
AppInstFieldConfigsKind = "27.1"
|
AppInstFieldConfigsKind = "27.1"
|
||||||
AppInstFieldConfigsConfig = "27.2"
|
AppInstFieldConfigsConfig = "27.2"
|
||||||
AppInstFieldHealthCheck = "29"
|
AppInstFieldHealthCheck = "29"
|
||||||
AppInstFieldPowerState = "31"
|
AppInstFieldPowerState = "31"
|
||||||
AppInstFieldExternalVolumeSize = "32"
|
AppInstFieldExternalVolumeSize = "32"
|
||||||
AppInstFieldAvailabilityZone = "33"
|
AppInstFieldAvailabilityZone = "33"
|
||||||
AppInstFieldVmFlavor = "34"
|
AppInstFieldVmFlavor = "34"
|
||||||
AppInstFieldOptRes = "35"
|
AppInstFieldOptRes = "35"
|
||||||
AppInstFieldUpdatedAt = "36"
|
AppInstFieldUpdatedAt = "36"
|
||||||
AppInstFieldUpdatedAtSeconds = "36.1"
|
AppInstFieldUpdatedAtSeconds = "36.1"
|
||||||
AppInstFieldUpdatedAtNanos = "36.2"
|
AppInstFieldUpdatedAtNanos = "36.2"
|
||||||
AppInstFieldRealClusterName = "37"
|
AppInstFieldRealClusterName = "37"
|
||||||
AppInstFieldInternalPortToLbIp = "38"
|
AppInstFieldInternalPortToLbIp = "38"
|
||||||
AppInstFieldInternalPortToLbIpKey = "38.1"
|
AppInstFieldInternalPortToLbIpKey = "38.1"
|
||||||
AppInstFieldInternalPortToLbIpValue = "38.2"
|
AppInstFieldInternalPortToLbIpValue = "38.2"
|
||||||
AppInstFieldDedicatedIp = "39"
|
AppInstFieldDedicatedIp = "39"
|
||||||
AppInstFieldUniqueId = "40"
|
AppInstFieldUniqueId = "40"
|
||||||
AppInstFieldDnsLabel = "41"
|
AppInstFieldDnsLabel = "41"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message interface for types that can provide error messages
|
// Message interface for types that can provide error messages
|
||||||
|
|
@ -291,7 +291,8 @@ type DeleteAppInstanceInput struct {
|
||||||
|
|
||||||
// Response wraps a single API response
|
// Response wraps a single API response
|
||||||
type Response[T Message] struct {
|
type Response[T Message] struct {
|
||||||
Data T `json:"data"`
|
ResultResponse `json:",inline"`
|
||||||
|
Data T `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Response[T]) HasData() bool {
|
func (res *Response[T]) HasData() bool {
|
||||||
|
|
@ -326,6 +327,7 @@ func (r *ResultResponse) GetCode() int {
|
||||||
type Responses[T Message] struct {
|
type Responses[T Message] struct {
|
||||||
Responses []Response[T] `json:"responses,omitempty"`
|
Responses []Response[T] `json:"responses,omitempty"`
|
||||||
StatusCode int `json:"-"`
|
StatusCode int `json:"-"`
|
||||||
|
Errors []error `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Responses[T]) GetData() []T {
|
func (r *Responses[T]) GetData() []T {
|
||||||
|
|
@ -344,12 +346,15 @@ func (r *Responses[T]) GetMessages() []string {
|
||||||
if v.IsMessage() {
|
if v.IsMessage() {
|
||||||
messages = append(messages, v.Data.GetMessage())
|
messages = append(messages, v.Data.GetMessage())
|
||||||
}
|
}
|
||||||
|
if v.Result.Message != "" {
|
||||||
|
messages = append(messages, v.Result.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Responses[T]) IsSuccessful() bool {
|
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 {
|
func (r *Responses[T]) Error() error {
|
||||||
|
|
@ -410,3 +415,7 @@ type CloudletResourceUsage struct {
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Usage map[string]interface{} `json:"usage"`
|
Usage map[string]interface{} `json:"usage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrorMessage struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: edgeconnect-coder-deployment
|
name: edgeconnect-coder-deployment
|
||||||
|
#namespace: gitea
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to wait for instance ready: %w", err)
|
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
|
// 6. List Application Instances
|
||||||
fmt.Println("\n6️⃣ Listing 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list app instances: %w", err)
|
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
|
// 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)
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
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)
|
return v2.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
|
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, appKey, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log error but continue polling
|
// Log error but continue polling
|
||||||
fmt.Printf(" ⚠️ Error checking instance state: %v\n", err)
|
fmt.Printf(" ⚠️ Error checking instance state: %v\n", err)
|
||||||
|
|
|
||||||
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 {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// Read response body
|
// Read response body
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue