diff --git a/.gitignore b/.gitignore index dec973c..c08c1df 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,3 @@ dist/ ### direnv ### .direnv .envrc - -edge-connect-client diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 248c94f..4731016 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -10,11 +10,11 @@ builds: - CGO_ENABLED=0 goos: - linux - - darwin - - windows + #- darwin + #- windows goarch: - amd64 - - arm64 + #- arm64 archives: - formats: [tar.gz] @@ -43,6 +43,9 @@ signs: - "--detach-sign" - "${artifact}" +#binary_signs: +# - {} + changelog: abbrev: 10 filters: diff --git a/Makefile b/Makefile index a8695c5..496876e 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ clean: # Lint the code lint: - go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2 run + golangci-lint run # Run all checks (generate, test, lint) check: test lint diff --git a/cmd/app.go b/cmd/app.go index 37218bf..79fc2c5 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -37,7 +37,7 @@ func validateBaseURL(baseURL string) error { return fmt.Errorf("user and or password should not be set") } - if url.Path != "" && url.Path != "/" { + if !(url.Path == "" || url.Path == "/") { return fmt.Errorf("should not contain any path '%s'", url.Path) } @@ -291,18 +291,12 @@ func init() { cmd.Flags().StringVarP(&appName, "name", "n", "", "application name") cmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version") cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)") - if err := cmd.MarkFlagRequired("org"); err != nil { - panic(err) - } - if err := cmd.MarkFlagRequired("region"); err != nil { - panic(err) - } + cmd.MarkFlagRequired("org") + cmd.MarkFlagRequired("region") } // Add required name flag for specific commands for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} { - if err := cmd.MarkFlagRequired("name"); err != nil { - panic(err) - } + cmd.MarkFlagRequired("name") } } diff --git a/cmd/apply.go b/cmd/apply.go index cf2b37f..1493841 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -10,9 +10,9 @@ import ( "path/filepath" "strings" - applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v1" - applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v2" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" + applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v1" + applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" "github.com/spf13/cobra" ) @@ -31,7 +31,7 @@ the necessary changes to deploy your applications across multiple cloudlets.`, Run: func(cmd *cobra.Command, args []string) { if configFile == "" { fmt.Fprintf(os.Stderr, "Error: configuration file is required\n") - _ = cmd.Usage() + cmd.Usage() os.Exit(1) } @@ -208,6 +208,20 @@ func runApplyV2(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun return displayDeploymentResults(deployResult) } +type deploymentResult interface { + IsSuccess() bool + GetDuration() string + GetCompletedActions() []actionResult + GetFailedActions() []actionResult + GetError() error +} + +type actionResult interface { + GetType() string + GetTarget() string + GetError() error +} + func displayDeploymentResults(result interface{}) error { // Use reflection or type assertion to handle both v1 and v2 result types // For now, we'll use a simple approach that works with both @@ -274,7 +288,7 @@ func displayDeploymentResultsV2(deployResult *applyv2.ExecutionResult) error { func confirmDeployment() bool { fmt.Print("Do you want to proceed? (yes/no): ") var response string - _, _ = fmt.Scanln(&response) + fmt.Scanln(&response) switch response { case "yes", "y", "YES", "Y": @@ -291,7 +305,5 @@ func init() { applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without applying them") applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan") - if err := applyCmd.MarkFlagRequired("file"); err != nil { - panic(err) - } + applyCmd.MarkFlagRequired("file") } diff --git a/cmd/delete.go b/cmd/delete.go index dcc1614..912741b 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -10,9 +10,9 @@ import ( "path/filepath" "strings" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v1" - deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v2" + deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v1" + deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" "github.com/spf13/cobra" ) @@ -31,7 +31,7 @@ Instances are always deleted before the application.`, Run: func(cmd *cobra.Command, args []string) { if deleteConfigFile == "" { fmt.Fprintf(os.Stderr, "Error: configuration file is required\n") - _ = cmd.Usage() + cmd.Usage() os.Exit(1) } @@ -273,7 +273,7 @@ func displayDeletionResultsV2(deleteResult *deletev2.DeletionResult) error { func confirmDeletion() bool { fmt.Print("Do you want to proceed with deletion? (yes/no): ") var response string - _, _ = fmt.Scanln(&response) + fmt.Scanln(&response) switch response { case "yes", "y", "YES", "Y": @@ -290,7 +290,5 @@ func init() { deleteCmd.Flags().BoolVar(&deleteDryRun, "dry-run", false, "preview deletion without actually deleting resources") deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan") - if err := deleteCmd.MarkFlagRequired("file"); err != nil { - panic(err) - } + deleteCmd.MarkFlagRequired("file") } diff --git a/cmd/instance.go b/cmd/instance.go index d856dea..1eb6cb6 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -5,8 +5,8 @@ import ( "fmt" "os" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/spf13/cobra" ) @@ -15,7 +15,6 @@ var ( cloudletOrg string instanceName string flavorName string - appId string ) var appInstanceCmd = &cobra.Command{ @@ -105,8 +104,7 @@ var showInstanceCmd = &cobra.Command{ Name: cloudletName, }, } - appkey := edgeconnect.AppKey{Name: appId} - instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region) + instance, err := c.ShowAppInstance(context.Background(), instanceKey, region) if err != nil { fmt.Printf("Error showing app instance: %v\n", err) os.Exit(1) @@ -122,8 +120,7 @@ var showInstanceCmd = &cobra.Command{ Name: cloudletName, }, } - appkey := v2.AppKey{Name: appId} - instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region) + instance, err := c.ShowAppInstance(context.Background(), instanceKey, region) if err != nil { fmt.Printf("Error showing app instance: %v\n", err) os.Exit(1) @@ -149,8 +146,7 @@ var listInstancesCmd = &cobra.Command{ Name: cloudletName, }, } - appKey := edgeconnect.AppKey{Name: appId} - instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region) + instances, err := c.ShowAppInstances(context.Background(), instanceKey, region) if err != nil { fmt.Printf("Error listing app instances: %v\n", err) os.Exit(1) @@ -169,8 +165,7 @@ var listInstancesCmd = &cobra.Command{ Name: cloudletName, }, } - appKey := v2.AppKey{Name: appId} - instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region) + instances, err := c.ShowAppInstances(context.Background(), instanceKey, region) if err != nil { fmt.Printf("Error listing app instances: %v\n", err) os.Exit(1) @@ -234,33 +229,18 @@ func init() { cmd.Flags().StringVarP(&cloudletName, "cloudlet", "c", "", "cloudlet name (required)") cmd.Flags().StringVarP(&cloudletOrg, "cloudlet-org", "", "", "cloudlet organization (required)") cmd.Flags().StringVarP(®ion, "region", "r", "", "region (required)") - cmd.Flags().StringVarP(&appId, "app-id", "i", "", "application id") - if err := cmd.MarkFlagRequired("org"); err != nil { - panic(err) - } - if err := cmd.MarkFlagRequired("name"); err != nil { - panic(err) - } - if err := cmd.MarkFlagRequired("cloudlet"); err != nil { - panic(err) - } - if err := cmd.MarkFlagRequired("cloudlet-org"); err != nil { - panic(err) - } - if err := cmd.MarkFlagRequired("region"); err != nil { - panic(err) - } + cmd.MarkFlagRequired("org") + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("cloudlet") + cmd.MarkFlagRequired("cloudlet-org") + cmd.MarkFlagRequired("region") } // Add additional flags for create command createInstanceCmd.Flags().StringVarP(&appName, "app", "a", "", "application name (required)") createInstanceCmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version") createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)") - if err := createInstanceCmd.MarkFlagRequired("app"); err != nil { - panic(err) - } - if err := createInstanceCmd.MarkFlagRequired("flavor"); err != nil { - panic(err) - } + createInstanceCmd.MarkFlagRequired("app") + createInstanceCmd.MarkFlagRequired("flavor") } diff --git a/cmd/root.go b/cmd/root.go index 52ae3ca..dd22f72 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,35 +44,19 @@ func init() { rootCmd.PersistentFlags().StringVar(&apiVersion, "api-version", "v2", "API version to use (v1 or v2)") rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging") - if err := viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")); err != nil { - panic(err) - } - if err := viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")); err != nil { - panic(err) - } - if err := viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")); err != nil { - panic(err) - } - if err := viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version")); err != nil { - panic(err) - } + viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")) + viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")) + viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")) + viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version")) } func initConfig() { viper.AutomaticEnv() viper.SetEnvPrefix("EDGE_CONNECT") - if err := viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL"); err != nil { - panic(err) - } - if err := viper.BindEnv("username", "EDGE_CONNECT_USERNAME"); err != nil { - panic(err) - } - if err := viper.BindEnv("password", "EDGE_CONNECT_PASSWORD"); err != nil { - panic(err) - } - if err := viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION"); err != nil { - panic(err) - } + viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL") + viper.BindEnv("username", "EDGE_CONNECT_USERNAME") + viper.BindEnv("password", "EDGE_CONNECT_PASSWORD") + viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION") if cfgFile != "" { viper.SetConfigFile(cfgFile) diff --git a/go.mod b/go.mod index e88a974..dd77621 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module edp.buildth.ing/DevFW-CICD/edge-connect-client/v2 +module edp.buildth.ing/DevFW-CICD/edge-connect-client go 1.25.1 diff --git a/internal/apply/v1/manager.go b/internal/apply/v1/manager.go index 048e85e..a0668e8 100644 --- a/internal/apply/v1/manager.go +++ b/internal/apply/v1/manager.go @@ -7,8 +7,8 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // ResourceManagerInterface defines the interface for resource management diff --git a/internal/apply/v1/manager_test.go b/internal/apply/v1/manager_test.go index d4b4744..9ed3cac 100644 --- a/internal/apply/v1/manager_test.go +++ b/internal/apply/v1/manager_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/internal/apply/v1/planner.go b/internal/apply/v1/planner.go index e1a1449..33b8d9c 100644 --- a/internal/apply/v1/planner.go +++ b/internal/apply/v1/planner.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // EdgeConnectClientInterface defines the methods needed for deployment planning @@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface { CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error - ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error) + ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error @@ -135,9 +135,9 @@ func (p *EdgeConnectPlanner) planAppAction(ctx context.Context, config *config.E desired := &AppState{ Name: config.Metadata.Name, Version: config.Metadata.AppVersion, - Organization: config.Metadata.Organization, // Use first infra template for org - Region: config.Spec.InfraTemplate[0].Region, // Use first infra template for region - Exists: false, // Will be set based on current state + Organization: config.Metadata.Organization, // Use first infra template for org + Region: config.Spec.InfraTemplate[0].Region, // Use first infra template for region + Exists: false, // Will be set based on current state } if config.Spec.IsK8sApp() { @@ -323,7 +323,12 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap // Extract outbound connections from the app current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections)) for i, conn := range app.RequiredOutboundConnections { - current.OutboundConnections[i] = SecurityRule(conn) + current.OutboundConnections[i] = SecurityRule{ + Protocol: conn.Protocol, + PortRangeMin: conn.PortRangeMin, + PortRangeMax: conn.PortRangeMax, + RemoteCIDR: conn.RemoteCIDR, + } } return current, nil @@ -342,11 +347,8 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire Name: desired.CloudletName, }, } - appKey := edgeconnect.AppKey{ - Name: desired.AppName, - } - instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region) + instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region) if err != nil { return nil, err } @@ -390,7 +392,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str // Compare outbound connections outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections) if len(outboundChanges) > 0 { - sb := strings.Builder{} + sb:= strings.Builder{} sb.WriteString("Outbound connections changed:\n") for _, change := range outboundChanges { sb.WriteString(change) @@ -468,9 +470,7 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string, if err != nil { return "", fmt.Errorf("failed to open manifest file: %w", err) } - defer func() { - _ = file.Close() - }() + defer file.Close() hasher := sha256.New() if _, err := io.Copy(hasher, file); err != nil { @@ -505,20 +505,18 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti var duration time.Duration // App operations - switch plan.AppAction.Type { - case ActionCreate: + if plan.AppAction.Type == ActionCreate { duration += 30 * time.Second - case ActionUpdate: + } else if plan.AppAction.Type == ActionUpdate { duration += 15 * time.Second } // Instance operations (can be done in parallel) instanceDuration := time.Duration(0) for _, action := range plan.InstanceActions { - switch action.Type { - case ActionCreate: + if action.Type == ActionCreate { instanceDuration = max(instanceDuration, 2*time.Minute) - case ActionUpdate: + } else if action.Type == ActionUpdate { instanceDuration = max(instanceDuration, 1*time.Minute) } } diff --git a/internal/apply/v1/planner_test.go b/internal/apply/v1/planner_test.go index 6530d8e..8c1e48a 100644 --- a/internal/apply/v1/planner_test.go +++ b/internal/apply/v1/planner_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect. return args.Get(0).(edgeconnect.App), args.Error(1) } -func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error) { +func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) { args := m.Called(ctx, instanceKey, region) if args.Get(0) == nil { return edgeconnect.AppInstance{}, args.Error(1) @@ -75,6 +75,14 @@ func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey edgeconnect return args.Get(0).([]edgeconnect.App), args.Error(1) } +func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error) { + args := m.Called(ctx, instanceKey, region) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]edgeconnect.AppInstance), args.Error(1) +} + func TestNewPlanner(t *testing.T) { mockClient := &MockEdgeConnectClient{} planner := NewPlanner(mockClient) diff --git a/internal/apply/v1/strategy.go b/internal/apply/v1/strategy.go index db2f90f..44f2471 100644 --- a/internal/apply/v1/strategy.go +++ b/internal/apply/v1/strategy.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" ) // DeploymentStrategy represents the type of deployment strategy diff --git a/internal/apply/v1/strategy_recreate.go b/internal/apply/v1/strategy_recreate.go index b8cc736..1f6f121 100644 --- a/internal/apply/v1/strategy_recreate.go +++ b/internal/apply/v1/strategy_recreate.go @@ -10,8 +10,8 @@ import ( "sync" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // RecreateStrategy implements the recreate deployment strategy diff --git a/internal/apply/v1/types.go b/internal/apply/v1/types.go index 4863716..223fa74 100644 --- a/internal/apply/v1/types.go +++ b/internal/apply/v1/types.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // SecurityRule defines network access rules (alias to SDK type for consistency) diff --git a/internal/apply/v2/manager.go b/internal/apply/v2/manager.go index 9bce91f..fc1b483 100644 --- a/internal/apply/v2/manager.go +++ b/internal/apply/v2/manager.go @@ -4,13 +4,11 @@ package v2 import ( "context" - "errors" "fmt" - "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // ResourceManagerInterface defines the interface for resource management @@ -206,8 +204,7 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re rollbackErrors := []error{} - // Phase 1: Delete resources that were created in this deployment attempt (in reverse order) - rm.logf("Phase 1: Rolling back created resources") + // Rollback completed instances (in reverse order) for i := len(result.CompletedActions) - 1; i >= 0; i-- { action := result.CompletedActions[i] @@ -221,32 +218,6 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re } } - // Phase 2: Restore resources that were deleted before the failed deployment - // This is critical for RecreateStrategy which deletes everything before recreating - if result.DeletedAppBackup != nil || len(result.DeletedInstancesBackup) > 0 { - rm.logf("Phase 2: Restoring deleted resources") - - // Restore app first (must exist before instances can be created) - if result.DeletedAppBackup != nil { - if err := rm.restoreApp(ctx, result.DeletedAppBackup); err != nil { - rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore app: %w", err)) - rm.logf("Failed to restore app: %v", err) - } else { - rm.logf("Successfully restored app: %s", result.DeletedAppBackup.App.Key.Name) - } - } - - // Restore instances - for _, backup := range result.DeletedInstancesBackup { - if err := rm.restoreInstance(ctx, &backup); err != nil { - rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore instance %s: %w", backup.Instance.Key.Name, err)) - rm.logf("Failed to restore instance %s: %v", backup.Instance.Key.Name, err) - } else { - rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name) - } - } - } - if len(rollbackErrors) > 0 { return fmt.Errorf("rollback encountered %d errors: %v", len(rollbackErrors), rollbackErrors) } @@ -307,125 +278,6 @@ func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, acti return fmt.Errorf("instance action not found for rollback: %s", action.Target) } -// restoreApp recreates an app that was deleted during deployment -func (rm *EdgeConnectResourceManager) restoreApp(ctx context.Context, backup *AppBackup) error { - rm.logf("Restoring app: %s/%s version %s", - backup.App.Key.Organization, backup.App.Key.Name, backup.App.Key.Version) - - // Build a clean app input with only creation-safe fields - // We must exclude read-only fields like CreatedAt, UpdatedAt, etc. - appInput := &v2.NewAppInput{ - Region: backup.Region, - App: v2.App{ - Key: backup.App.Key, - Deployment: backup.App.Deployment, - ImageType: backup.App.ImageType, - ImagePath: backup.App.ImagePath, - AllowServerless: backup.App.AllowServerless, - DefaultFlavor: backup.App.DefaultFlavor, - ServerlessConfig: backup.App.ServerlessConfig, - DeploymentManifest: backup.App.DeploymentManifest, - DeploymentGenerator: backup.App.DeploymentGenerator, - RequiredOutboundConnections: backup.App.RequiredOutboundConnections, - // Explicitly omit read-only fields like CreatedAt, UpdatedAt, Fields, etc. - }, - } - - if err := rm.client.CreateApp(ctx, appInput); err != nil { - return fmt.Errorf("failed to restore app: %w", err) - } - - rm.logf("Successfully restored app: %s", backup.App.Key.Name) - return nil -} - -// restoreInstance recreates an instance that was deleted during deployment -func (rm *EdgeConnectResourceManager) restoreInstance(ctx context.Context, backup *InstanceBackup) error { - rm.logf("Restoring instance: %s on %s:%s", - backup.Instance.Key.Name, - backup.Instance.Key.CloudletKey.Organization, - backup.Instance.Key.CloudletKey.Name) - - // Build a clean instance input with only creation-safe fields - // We must exclude read-only fields like CloudletLoc, CreatedAt, etc. - instanceInput := &v2.NewAppInstanceInput{ - Region: backup.Region, - AppInst: v2.AppInstance{ - Key: backup.Instance.Key, - AppKey: backup.Instance.AppKey, - Flavor: backup.Instance.Flavor, - // Explicitly omit read-only fields like CloudletLoc, State, PowerState, CreatedAt, etc. - }, - } - - // Retry logic to handle namespace termination race conditions - maxRetries := 5 - retryDelay := 10 * time.Second - - var lastErr error - for attempt := 0; attempt <= maxRetries; attempt++ { - if attempt > 0 { - rm.logf("Retrying instance restore %s (attempt %d/%d)", backup.Instance.Key.Name, attempt, maxRetries) - select { - case <-time.After(retryDelay): - case <-ctx.Done(): - return ctx.Err() - } - } - - err := rm.client.CreateAppInstance(ctx, instanceInput) - if err == nil { - rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name) - return nil - } - - lastErr = err - - // Check if error is retryable - if !rm.isRetryableError(err) { - rm.logf("Failed to restore instance %s: %v (non-retryable error, giving up)", backup.Instance.Key.Name, err) - return fmt.Errorf("failed to restore instance: %w", err) - } - - if attempt < maxRetries { - rm.logf("Failed to restore instance %s: %v (will retry)", backup.Instance.Key.Name, err) - } - } - - return fmt.Errorf("failed to restore instance after %d attempts: %w", maxRetries+1, lastErr) -} - -// isRetryableError determines if an error should be retried -func (rm *EdgeConnectResourceManager) isRetryableError(err error) bool { - if err == nil { - return false - } - - errStr := strings.ToLower(err.Error()) - - // Special case: Kubernetes namespace termination race condition - // This is a transient 400 error that should be retried - if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") { - return true - } - - // Check if it's an APIError with a status code - var apiErr *v2.APIError - if errors.As(err, &apiErr) { - // Don't retry client errors (4xx) - if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 { - return false - } - // Retry server errors (5xx) - if apiErr.StatusCode >= 500 { - return true - } - } - - // Retry all other errors (network issues, timeouts, etc.) - return true -} - // logf logs a message if a logger is configured func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) { if rm.logger != nil { diff --git a/internal/apply/v2/manager_test.go b/internal/apply/v2/manager_test.go index 6d5ef18..68c60fd 100644 --- a/internal/apply/v2/manager_test.go +++ b/internal/apply/v2/manager_test.go @@ -7,12 +7,11 @@ import ( "fmt" "os" "path/filepath" - "strings" "testing" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -465,111 +464,6 @@ func TestRollbackDeploymentFailure(t *testing.T) { mockClient.AssertExpectations(t) } -func TestRollbackDeploymentWithRestore(t *testing.T) { - mockClient := &MockResourceClient{} - logger := &TestLogger{} - manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig())) - - plan := createTestDeploymentPlan() - - // Simulate a RecreateStrategy scenario: - // 1. Old app and instance were deleted and backed up - // 2. New app was created successfully - // 3. New instance creation failed - // 4. Rollback should: delete new app, restore old app, restore old instance - oldApp := v2.App{ - Key: v2.AppKey{ - Organization: "test-org", - Name: "test-app", - Version: "1.0.0", - }, - Deployment: "kubernetes", - DeploymentManifest: "old-manifest-content", - } - - oldInstance := v2.AppInstance{ - Key: v2.AppInstanceKey{ - Organization: "test-org", - Name: "test-app-1.0.0-instance", - CloudletKey: v2.CloudletKey{ - Organization: "test-cloudlet-org", - Name: "test-cloudlet", - }, - }, - AppKey: v2.AppKey{ - Organization: "test-org", - Name: "test-app", - Version: "1.0.0", - }, - Flavor: v2.Flavor{Name: "small"}, - } - - result := &ExecutionResult{ - Plan: plan, - // Completed actions: new app was created before failure - CompletedActions: []ActionResult{ - { - Type: ActionCreate, - Target: "test-app", - Success: true, - }, - }, - // Failed action: new instance creation failed - FailedActions: []ActionResult{ - { - Type: ActionCreate, - Target: "test-app-1.0.0-instance", - Success: false, - }, - }, - // Backup of deleted resources - DeletedAppBackup: &AppBackup{ - App: oldApp, - Region: "US", - ManifestContent: "old-manifest-content", - }, - DeletedInstancesBackup: []InstanceBackup{ - { - Instance: oldInstance, - Region: "US", - }, - }, - } - - // Mock rollback operations in order: - // 1. Delete newly created app (rollback create) - mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US"). - Return(nil).Once() - - // 2. Restore old app (from backup) - mockClient.On("CreateApp", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInput) bool { - return input.App.Key.Name == "test-app" && input.App.DeploymentManifest == "old-manifest-content" - })).Return(nil).Once() - - // 3. Restore old instance (from backup) - mockClient.On("CreateAppInstance", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInstanceInput) bool { - return input.AppInst.Key.Name == "test-app-1.0.0-instance" - })).Return(nil).Once() - - ctx := context.Background() - err := manager.RollbackDeployment(ctx, result) - - require.NoError(t, err) - mockClient.AssertExpectations(t) - - // Verify rollback was logged - assert.Greater(t, len(logger.messages), 0) - // Should have messages about rolling back created resources and restoring deleted resources - hasRestoreLog := false - for _, msg := range logger.messages { - if strings.Contains(msg, "Restoring deleted resources") { - hasRestoreLog = true - break - } - } - assert.True(t, hasRestoreLog, "Should log restoration of deleted resources") -} - func TestConvertNetworkRules(t *testing.T) { network := &config.NetworkConfig{ OutboundConnections: []config.OutboundConnection{ diff --git a/internal/apply/v2/planner.go b/internal/apply/v2/planner.go index 797a411..52de1ee 100644 --- a/internal/apply/v2/planner.go +++ b/internal/apply/v2/planner.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // EdgeConnectClientInterface defines the methods needed for deployment planning @@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface { CreateApp(ctx context.Context, input *v2.NewAppInput) error UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error - ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error) + ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error @@ -135,9 +135,9 @@ func (p *EdgeConnectPlanner) planAppAction(ctx context.Context, config *config.E desired := &AppState{ Name: config.Metadata.Name, Version: config.Metadata.AppVersion, - Organization: config.Metadata.Organization, // Use first infra template for org - Region: config.Spec.InfraTemplate[0].Region, // Use first infra template for region - Exists: false, // Will be set based on current state + Organization: config.Metadata.Organization, // Use first infra template for org + Region: config.Spec.InfraTemplate[0].Region, // Use first infra template for region + Exists: false, // Will be set based on current state } if config.Spec.IsK8sApp() { @@ -323,7 +323,12 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap // Extract outbound connections from the app current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections)) for i, conn := range app.RequiredOutboundConnections { - current.OutboundConnections[i] = SecurityRule(conn) + current.OutboundConnections[i] = SecurityRule{ + Protocol: conn.Protocol, + PortRangeMin: conn.PortRangeMin, + PortRangeMax: conn.PortRangeMax, + RemoteCIDR: conn.RemoteCIDR, + } } return current, nil @@ -343,9 +348,7 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire }, } - appKey := v2.AppKey{Name: desired.AppName} - - instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region) + instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region) if err != nil { return nil, err } @@ -389,7 +392,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str // Compare outbound connections outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections) if len(outboundChanges) > 0 { - sb := strings.Builder{} + sb:= strings.Builder{} sb.WriteString("Outbound connections changed:\n") for _, change := range outboundChanges { sb.WriteString(change) @@ -467,9 +470,7 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string, if err != nil { return "", fmt.Errorf("failed to open manifest file: %w", err) } - defer func() { - _ = file.Close() - }() + defer file.Close() hasher := sha256.New() if _, err := io.Copy(hasher, file); err != nil { @@ -504,20 +505,18 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti var duration time.Duration // App operations - switch plan.AppAction.Type { - case ActionCreate: + if plan.AppAction.Type == ActionCreate { duration += 30 * time.Second - case ActionUpdate: + } else if plan.AppAction.Type == ActionUpdate { duration += 15 * time.Second } // Instance operations (can be done in parallel) instanceDuration := time.Duration(0) for _, action := range plan.InstanceActions { - switch action.Type { - case ActionCreate: + if action.Type == ActionCreate { instanceDuration = max(instanceDuration, 2*time.Minute) - case ActionUpdate: + } else if action.Type == ActionUpdate { instanceDuration = max(instanceDuration, 1*time.Minute) } } diff --git a/internal/apply/v2/planner_test.go b/internal/apply/v2/planner_test.go index 3fbdbc3..fe56871 100644 --- a/internal/apply/v2/planner_test.go +++ b/internal/apply/v2/planner_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r return args.Get(0).(v2.App), args.Error(1) } -func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error) { +func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error) { args := m.Called(ctx, instanceKey, region) if args.Get(0) == nil { return v2.AppInstance{}, args.Error(1) diff --git a/internal/apply/v2/strategy.go b/internal/apply/v2/strategy.go index 78e3df4..6a1661a 100644 --- a/internal/apply/v2/strategy.go +++ b/internal/apply/v2/strategy.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" ) // DeploymentStrategy represents the type of deployment strategy diff --git a/internal/apply/v2/strategy_recreate.go b/internal/apply/v2/strategy_recreate.go index 6af0a68..739a454 100644 --- a/internal/apply/v2/strategy_recreate.go +++ b/internal/apply/v2/strategy_recreate.go @@ -10,8 +10,8 @@ import ( "sync" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // RecreateStrategy implements the recreate deployment strategy @@ -159,19 +159,6 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo return nil } - // Backup instances before deleting them (for rollback restoration) - r.logf("Backing up %d existing instances before deletion", len(instancesToDelete)) - for _, action := range instancesToDelete { - backup, err := r.backupInstance(ctx, action, config) - if err != nil { - r.logf("Warning: failed to backup instance %s before deletion: %v", action.InstanceName, err) - // Continue with deletion even if backup fails - this is best effort - } else { - result.DeletedInstancesBackup = append(result.DeletedInstancesBackup, *backup) - r.logf("Backed up instance: %s", action.InstanceName) - } - } - deleteResults := r.executeInstanceActionsWithRetry(ctx, instancesToDelete, "delete", config) for _, deleteResult := range deleteResults { @@ -185,19 +172,6 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo } r.logf("Phase 1 complete: deleted %d instances", len(deleteResults)) - - // Wait for Kubernetes namespace termination to complete - // This prevents "namespace is being terminated" errors when recreating instances - if len(deleteResults) > 0 { - waitTime := 5 * time.Second - r.logf("Waiting %v for namespace termination to complete...", waitTime) - select { - case <-time.After(waitTime): - case <-ctx.Done(): - return ctx.Err() - } - } - return nil } @@ -210,17 +184,6 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP r.logf("Phase 2: Deleting existing application") - // Backup app before deleting it (for rollback restoration) - r.logf("Backing up existing app before deletion") - backup, err := r.backupApp(ctx, plan, config) - if err != nil { - r.logf("Warning: failed to backup app before deletion: %v", err) - // Continue with deletion even if backup fails - this is best effort - } else { - result.DeletedAppBackup = backup - r.logf("Backed up app: %s", plan.AppAction.Desired.Name) - } - appKey := v2.AppKey{ Organization: plan.AppAction.Desired.Organization, Name: plan.AppAction.Desired.Name, @@ -553,54 +516,6 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi return true, nil } -// backupApp fetches and stores the current app state before deletion -func (r *RecreateStrategy) backupApp(ctx context.Context, plan *DeploymentPlan, config *config.EdgeConnectConfig) (*AppBackup, error) { - appKey := v2.AppKey{ - Organization: plan.AppAction.Desired.Organization, - Name: plan.AppAction.Desired.Name, - Version: plan.AppAction.Desired.Version, - } - - app, err := r.client.ShowApp(ctx, appKey, plan.AppAction.Desired.Region) - if err != nil { - return nil, fmt.Errorf("failed to fetch app for backup: %w", err) - } - - backup := &AppBackup{ - App: app, - Region: plan.AppAction.Desired.Region, - ManifestContent: app.DeploymentManifest, - } - - return backup, nil -} - -// backupInstance fetches and stores the current instance state before deletion -func (r *RecreateStrategy) backupInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (*InstanceBackup, error) { - instanceKey := v2.AppInstanceKey{ - Organization: action.Desired.Organization, - Name: action.InstanceName, - CloudletKey: v2.CloudletKey{ - Organization: action.Target.CloudletOrg, - Name: action.Target.CloudletName, - }, - } - - appKey := v2.AppKey{Name: action.Desired.AppName} - - instance, err := r.client.ShowAppInstance(ctx, instanceKey, appKey, action.Target.Region) - if err != nil { - return nil, fmt.Errorf("failed to fetch instance for backup: %w", err) - } - - backup := &InstanceBackup{ - Instance: instance, - Region: action.Target.Region, - } - - return backup, nil -} - // logf logs a message if a logger is configured func (r *RecreateStrategy) logf(format string, v ...interface{}) { if r.logger != nil { @@ -615,14 +530,6 @@ func isRetryableError(err error) bool { return false } - errStr := strings.ToLower(err.Error()) - - // Special case: Kubernetes namespace termination race condition - // This is a transient 400 error that should be retried - if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") { - return true - } - // Check if it's an APIError with a status code var apiErr *v2.APIError if errors.As(err, &apiErr) { diff --git a/internal/apply/v2/types.go b/internal/apply/v2/types.go index 26d998e..90b7956 100644 --- a/internal/apply/v2/types.go +++ b/internal/apply/v2/types.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // SecurityRule defines network access rules (alias to SDK type for consistency) @@ -271,12 +271,6 @@ type ExecutionResult struct { // RollbackSuccess indicates if rollback was successful RollbackSuccess bool - - // DeletedAppBackup stores the app that was deleted (for rollback restoration) - DeletedAppBackup *AppBackup - - // DeletedInstancesBackup stores instances that were deleted (for rollback restoration) - DeletedInstancesBackup []InstanceBackup } // ActionResult represents the result of executing a single action @@ -300,27 +294,6 @@ type ActionResult struct { Details string } -// AppBackup stores a deleted app's complete state for rollback restoration -type AppBackup struct { - // App is the full app object that was deleted - App v2.App - - // Region where the app was deployed - Region string - - // ManifestContent is the deployment manifest content - ManifestContent string -} - -// InstanceBackup stores a deleted instance's complete state for rollback restoration -type InstanceBackup struct { - // Instance is the full instance object that was deleted - Instance v2.AppInstance - - // Region where the instance was deployed - Region string -} - // IsEmpty returns true if the deployment plan has no actions to perform func (dp *DeploymentPlan) IsEmpty() bool { if dp.AppAction.Type != ActionNone { diff --git a/internal/config/example_test.go b/internal/config/example_test.go index f7299c2..536399f 100644 --- a/internal/config/example_test.go +++ b/internal/config/example_test.go @@ -70,13 +70,13 @@ func TestValidateExampleStructure(t *testing.T) { config := &EdgeConnectConfig{ Kind: "edgeconnect-deployment", Metadata: Metadata{ - Name: "edge-app-demo", - AppVersion: "1.0.0", + Name: "edge-app-demo", + AppVersion: "1.0.0", Organization: "edp2", }, Spec: Spec{ DockerApp: &DockerApp{ // Use DockerApp to avoid manifest file validation - Image: "nginx:latest", + Image: "nginx:latest", }, InfraTemplate: []InfraTemplate{ { diff --git a/internal/delete/v1/manager.go b/internal/delete/v1/manager.go index e20eba9..470ac37 100644 --- a/internal/delete/v1/manager.go +++ b/internal/delete/v1/manager.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // ResourceManagerInterface defines the interface for resource management diff --git a/internal/delete/v1/planner.go b/internal/delete/v1/planner.go index ca97b84..d436057 100644 --- a/internal/delete/v1/planner.go +++ b/internal/delete/v1/planner.go @@ -7,14 +7,14 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ) // EdgeConnectClientInterface defines the methods needed for deletion planning type EdgeConnectClientInterface interface { ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error) - ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) ([]edgeconnect.AppInstance, error) + ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error } @@ -154,9 +154,8 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config * Name: infra.CloudletName, }, } - appKey := edgeconnect.AppKey{Name: config.Metadata.Name} - instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region) + instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region) if err != nil { // If it's a not found error, just continue if isNotFoundError(err) { diff --git a/internal/delete/v2/manager.go b/internal/delete/v2/manager.go index 35518a2..a644f32 100644 --- a/internal/delete/v2/manager.go +++ b/internal/delete/v2/manager.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // ResourceManagerInterface defines the interface for resource management diff --git a/internal/delete/v2/manager_test.go b/internal/delete/v2/manager_test.go index d021f20..fd098af 100644 --- a/internal/delete/v2/manager_test.go +++ b/internal/delete/v2/manager_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -27,7 +27,7 @@ func (m *MockResourceClient) ShowApp(ctx context.Context, appKey v2.AppKey, regi return args.Get(0).(v2.App), args.Error(1) } -func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) { +func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) { args := m.Called(ctx, instanceKey, region) if args.Get(0) == nil { return nil, args.Error(1) diff --git a/internal/delete/v2/planner.go b/internal/delete/v2/planner.go index 76ec1c6..e77cd9e 100644 --- a/internal/delete/v2/planner.go +++ b/internal/delete/v2/planner.go @@ -7,14 +7,14 @@ import ( "fmt" "time" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) // EdgeConnectClientInterface defines the methods needed for deletion planning type EdgeConnectClientInterface interface { ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error) - ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) + ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error } @@ -154,9 +154,8 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config * Name: infra.CloudletName, }, } - appKey := v2.AppKey{Name: config.Metadata.Name} - instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region) + instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region) if err != nil { // If it's a not found error, just continue if isNotFoundError(err) { diff --git a/internal/delete/v2/planner_test.go b/internal/delete/v2/planner_test.go index 292cecc..c37a318 100644 --- a/internal/delete/v2/planner_test.go +++ b/internal/delete/v2/planner_test.go @@ -8,8 +8,8 @@ import ( "path/filepath" "testing" - "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -28,7 +28,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r return args.Get(0).(v2.App), args.Error(1) } -func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) { +func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) { args := m.Called(ctx, instanceKey, region) if args.Get(0) == nil { return nil, args.Error(1) diff --git a/internal/delete/v2/types_test.go b/internal/delete/v2/types_test.go index 225c5ef..8dfa6b0 100644 --- a/internal/delete/v2/types_test.go +++ b/internal/delete/v2/types_test.go @@ -16,8 +16,8 @@ func TestDeletionPlan_IsEmpty(t *testing.T) { { name: "empty plan with no resources", plan: &DeletionPlan{ - ConfigName: "test-config", - AppToDelete: nil, + ConfigName: "test-config", + AppToDelete: nil, InstancesToDelete: []InstanceDeletion{}, }, expected: true, diff --git a/main.go b/main.go index 2d198e9..9bc902d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/cmd" +import "edp.buildth.ing/DevFW-CICD/edge-connect-client/cmd" func main() { cmd.Execute() diff --git a/public.gpg b/public.gpg deleted file mode 100644 index 32d15f1..0000000 Binary files a/public.gpg and /dev/null differ diff --git a/sdk/README.md b/sdk/README.md index be2374f..89dc673 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -16,7 +16,7 @@ A comprehensive Go SDK for the EdgeXR Master Controller API, providing typed int ### Installation ```go -import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" +import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ``` ### Authentication diff --git a/sdk/edgeconnect/appinstance.go b/sdk/edgeconnect/appinstance.go index 34e3486..a26f45c 100644 --- a/sdk/edgeconnect/appinstance.go +++ b/sdk/edgeconnect/appinstance.go @@ -9,7 +9,7 @@ import ( "fmt" "net/http" - sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http" + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) // CreateAppInstance creates a new application instance in the specified region @@ -23,9 +23,7 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp if err != nil { return fmt.Errorf("CreateAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateAppInstance") @@ -45,12 +43,12 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp // ShowAppInstance retrieves a single application instance by key and region // Maps to POST /auth/ctrl/ShowAppInst -func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) { +func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) (AppInstance, error) { transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst" filter := AppInstanceFilter{ - AppInstance: AppInstance{AppKey: appKey, Key: appInstKey}, + AppInstance: AppInstance{Key: appInstKey}, Region: region, } @@ -58,9 +56,7 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, if err != nil { return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return AppInstance{}, fmt.Errorf("app instance %s/%s: %w", @@ -87,12 +83,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, // ShowAppInstances retrieves all application instances matching the filter criteria // Maps to POST /auth/ctrl/ShowAppInst -func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) { +func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) { transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst" filter := AppInstanceFilter{ - AppInstance: AppInstance{Key: appInstKey, AppKey: appKey}, + AppInstance: AppInstance{Key: appInstKey}, Region: region, } @@ -100,9 +96,7 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey if err != nil { return nil, fmt.Errorf("ShowAppInstances failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowAppInstances") @@ -131,9 +125,7 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance if err != nil { return fmt.Errorf("UpdateAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "UpdateAppInstance") @@ -160,9 +152,7 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK if err != nil { return fmt.Errorf("RefreshAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "RefreshAppInstance") @@ -189,9 +179,7 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe if err != nil { return fmt.Errorf("DeleteAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -213,10 +201,6 @@ func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result i var errorMessage string parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error { - // On permission denied, Edge API returns just an empty array []! - if len(line) == 0 || line[0] == '[' { - return fmt.Errorf("%w", ErrFaultyResponsePerhaps403) - } // Try parsing as ResultResponse first (error format) var resultResp ResultResponse if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" { diff --git a/sdk/edgeconnect/appinstance_test.go b/sdk/edgeconnect/appinstance_test.go index 3545904..ac9c1eb 100644 --- a/sdk/edgeconnect/appinstance_test.go +++ b/sdk/edgeconnect/appinstance_test.go @@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -156,7 +156,6 @@ func TestCreateAppInstance(t *testing.T) { func TestShowAppInstance(t *testing.T) { tests := []struct { name string - appKey AppKey appInstKey AppInstanceKey region string mockStatusCode int @@ -174,7 +173,6 @@ func TestShowAppInstance(t *testing.T) { Name: "testcloudlet", }, }, - appKey: AppKey{Name: "test-app-id"}, region: "us-west", mockStatusCode: 200, mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}} @@ -192,7 +190,6 @@ func TestShowAppInstance(t *testing.T) { Name: "testcloudlet", }, }, - appKey: AppKey{Name: "test-app-id"}, region: "us-west", mockStatusCode: 404, mockResponse: "", @@ -210,7 +207,7 @@ func TestShowAppInstance(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -222,7 +219,7 @@ func TestShowAppInstance(t *testing.T) { // Execute test ctx := context.Background() - appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region) + appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region) // Verify results if tt.expectError { @@ -257,14 +254,14 @@ func TestShowAppInstances(t *testing.T) { {"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() client := NewClient(server.URL) ctx := context.Background() - appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west") + appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west") require.NoError(t, err) assert.Len(t, appInstances, 2) @@ -364,7 +361,7 @@ func TestUpdateAppInstance(t *testing.T) { assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() diff --git a/sdk/edgeconnect/apps.go b/sdk/edgeconnect/apps.go index a086475..70f5dea 100644 --- a/sdk/edgeconnect/apps.go +++ b/sdk/edgeconnect/apps.go @@ -10,13 +10,12 @@ import ( "io" "net/http" - sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http" + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) var ( // ErrResourceNotFound indicates the requested resource was not found - ErrResourceNotFound = fmt.Errorf("resource not found") - ErrFaultyResponsePerhaps403 = fmt.Errorf("faulty response from API, may indicate permission denied") + ErrResourceNotFound = fmt.Errorf("resource not found") ) // CreateApp creates a new application in the specified region @@ -29,9 +28,7 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error { if err != nil { return fmt.Errorf("CreateApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateApp") @@ -58,9 +55,7 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App if err != nil { return App{}, fmt.Errorf("ShowApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w", @@ -100,9 +95,7 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([] if err != nil { return nil, fmt.Errorf("ShowApps failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowApps") @@ -131,9 +124,7 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error { if err != nil { return fmt.Errorf("UpdateApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "UpdateApp") @@ -160,9 +151,7 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er if err != nil { return fmt.Errorf("DeleteApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -180,10 +169,6 @@ func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) var responses []Response[App] parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error { - // On permission denied, Edge API returns just an empty array []! - if len(line) == 0 || line[0] == '[' { - return fmt.Errorf("%w", ErrFaultyResponsePerhaps403) - } var response Response[App] if err := json.Unmarshal(line, &response); err != nil { return err @@ -253,9 +238,7 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro bodyBytes := []byte{} if resp.Body != nil { - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() bodyBytes, _ = io.ReadAll(resp.Body) messages = append(messages, string(bodyBytes)) } diff --git a/sdk/edgeconnect/apps_test.go b/sdk/edgeconnect/apps_test.go index 88437ca..30531f6 100644 --- a/sdk/edgeconnect/apps_test.go +++ b/sdk/edgeconnect/apps_test.go @@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) { {"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() @@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) { assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -407,3 +407,13 @@ func TestAPIError(t *testing.T) { assert.Equal(t, 400, err.StatusCode) assert.Len(t, err.Messages, 2) } + +// Helper function to create a test server that handles streaming JSON responses +func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + for _, response := range responses { + w.Write([]byte(response + "\n")) + } + })) +} diff --git a/sdk/edgeconnect/auth.go b/sdk/edgeconnect/auth.go index cf6067b..eab24b9 100644 --- a/sdk/edgeconnect/auth.go +++ b/sdk/edgeconnect/auth.go @@ -138,9 +138,7 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e if err != nil { return "", err } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // Read response body - same as existing implementation body, err := io.ReadAll(resp.Body) diff --git a/sdk/edgeconnect/auth_test.go b/sdk/edgeconnect/auth_test.go index 8e68dc4..8ea3176 100644 --- a/sdk/edgeconnect/auth_test.go +++ b/sdk/edgeconnect/auth_test.go @@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) { // Return token response := map[string]string{"token": "dynamic-token-456"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) { // Mock login server that returns error loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("Invalid credentials")) + w.Write([]byte("Invalid credentials")) })) defer loginServer.Close() @@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) { callCount++ response := map[string]string{"token": "cached-token-789"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) { callCount++ response := map[string]string{"token": "refreshed-token-999"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) { callCount++ response := map[string]string{"token": "new-token-after-invalidation"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) { // Mock server returning invalid JSON loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte("invalid json response")) + w.Write([]byte("invalid json response")) })) defer loginServer.Close() diff --git a/sdk/edgeconnect/cloudlet.go b/sdk/edgeconnect/cloudlet.go index 142b9d6..e3f4b7d 100644 --- a/sdk/edgeconnect/cloudlet.go +++ b/sdk/edgeconnect/cloudlet.go @@ -9,7 +9,7 @@ import ( "fmt" "net/http" - sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http" + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) // CreateCloudlet creates a new cloudlet in the specified region @@ -22,9 +22,7 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er if err != nil { return fmt.Errorf("CreateCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateCloudlet") @@ -51,9 +49,7 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi if err != nil { return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w", @@ -93,9 +89,7 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg if err != nil { return nil, fmt.Errorf("ShowCloudlets failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowCloudlets") @@ -129,9 +123,7 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re if err != nil { return fmt.Errorf("DeleteCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -159,9 +151,7 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe if err != nil { return nil, fmt.Errorf("GetCloudletManifest failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w", @@ -199,9 +189,7 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud if err != nil { return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w", diff --git a/sdk/edgeconnect/cloudlet_test.go b/sdk/edgeconnect/cloudlet_test.go index b029f17..7d129bb 100644 --- a/sdk/edgeconnect/cloudlet_test.go +++ b/sdk/edgeconnect/cloudlet_test.go @@ -70,7 +70,7 @@ func TestCreateCloudlet(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) { {"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() @@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() diff --git a/sdk/edgeconnect/types.go b/sdk/edgeconnect/types.go index 307ed52..7fd39fc 100644 --- a/sdk/edgeconnect/types.go +++ b/sdk/edgeconnect/types.go @@ -60,74 +60,74 @@ const ( // AppInstance field constants for partial updates (based on EdgeXR API specification) const ( - AppInstFieldKey = "2" - AppInstFieldKeyAppKey = "2.1" - AppInstFieldKeyAppKeyOrganization = "2.1.1" - AppInstFieldKeyAppKeyName = "2.1.2" - AppInstFieldKeyAppKeyVersion = "2.1.3" - AppInstFieldKeyClusterInstKey = "2.4" - AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1" - AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1" - AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2" - AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1" - AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2" + AppInstFieldKey = "2" + AppInstFieldKeyAppKey = "2.1" + AppInstFieldKeyAppKeyOrganization = "2.1.1" + AppInstFieldKeyAppKeyName = "2.1.2" + AppInstFieldKeyAppKeyVersion = "2.1.3" + AppInstFieldKeyClusterInstKey = "2.4" + AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1" + AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1" + AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2" + AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1" + AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2" AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3" - AppInstFieldKeyClusterInstKeyOrganization = "2.4.3" - AppInstFieldCloudletLoc = "3" - AppInstFieldCloudletLocLatitude = "3.1" - AppInstFieldCloudletLocLongitude = "3.2" - AppInstFieldCloudletLocHorizontalAccuracy = "3.3" - AppInstFieldCloudletLocVerticalAccuracy = "3.4" - AppInstFieldCloudletLocAltitude = "3.5" - AppInstFieldCloudletLocCourse = "3.6" - AppInstFieldCloudletLocSpeed = "3.7" - AppInstFieldCloudletLocTimestamp = "3.8" - AppInstFieldCloudletLocTimestampSeconds = "3.8.1" - AppInstFieldCloudletLocTimestampNanos = "3.8.2" - AppInstFieldUri = "4" - AppInstFieldLiveness = "6" - AppInstFieldMappedPorts = "9" - AppInstFieldMappedPortsProto = "9.1" - AppInstFieldMappedPortsInternalPort = "9.2" - AppInstFieldMappedPortsPublicPort = "9.3" - AppInstFieldMappedPortsFqdnPrefix = "9.5" - AppInstFieldMappedPortsEndPort = "9.6" - AppInstFieldMappedPortsTls = "9.7" - AppInstFieldMappedPortsNginx = "9.8" - AppInstFieldMappedPortsMaxPktSize = "9.9" - AppInstFieldFlavor = "12" - AppInstFieldFlavorName = "12.1" - AppInstFieldState = "14" - AppInstFieldErrors = "15" - AppInstFieldCrmOverride = "16" - AppInstFieldRuntimeInfo = "17" - AppInstFieldRuntimeInfoContainerIds = "17.1" - AppInstFieldCreatedAt = "21" - AppInstFieldCreatedAtSeconds = "21.1" - AppInstFieldCreatedAtNanos = "21.2" - AppInstFieldAutoClusterIpAccess = "22" - AppInstFieldRevision = "24" - AppInstFieldForceUpdate = "25" - AppInstFieldUpdateMultiple = "26" - AppInstFieldConfigs = "27" - AppInstFieldConfigsKind = "27.1" - AppInstFieldConfigsConfig = "27.2" - AppInstFieldHealthCheck = "29" - AppInstFieldPowerState = "31" - AppInstFieldExternalVolumeSize = "32" - AppInstFieldAvailabilityZone = "33" - AppInstFieldVmFlavor = "34" - AppInstFieldOptRes = "35" - AppInstFieldUpdatedAt = "36" - AppInstFieldUpdatedAtSeconds = "36.1" - AppInstFieldUpdatedAtNanos = "36.2" - AppInstFieldRealClusterName = "37" - AppInstFieldInternalPortToLbIp = "38" - AppInstFieldInternalPortToLbIpKey = "38.1" - AppInstFieldInternalPortToLbIpValue = "38.2" - AppInstFieldDedicatedIp = "39" - AppInstFieldUniqueId = "40" - AppInstFieldDnsLabel = "41" + AppInstFieldKeyClusterInstKeyOrganization = "2.4.3" + AppInstFieldCloudletLoc = "3" + AppInstFieldCloudletLocLatitude = "3.1" + AppInstFieldCloudletLocLongitude = "3.2" + AppInstFieldCloudletLocHorizontalAccuracy = "3.3" + AppInstFieldCloudletLocVerticalAccuracy = "3.4" + AppInstFieldCloudletLocAltitude = "3.5" + AppInstFieldCloudletLocCourse = "3.6" + AppInstFieldCloudletLocSpeed = "3.7" + AppInstFieldCloudletLocTimestamp = "3.8" + AppInstFieldCloudletLocTimestampSeconds = "3.8.1" + AppInstFieldCloudletLocTimestampNanos = "3.8.2" + AppInstFieldUri = "4" + AppInstFieldLiveness = "6" + AppInstFieldMappedPorts = "9" + AppInstFieldMappedPortsProto = "9.1" + AppInstFieldMappedPortsInternalPort = "9.2" + AppInstFieldMappedPortsPublicPort = "9.3" + AppInstFieldMappedPortsFqdnPrefix = "9.5" + AppInstFieldMappedPortsEndPort = "9.6" + AppInstFieldMappedPortsTls = "9.7" + AppInstFieldMappedPortsNginx = "9.8" + AppInstFieldMappedPortsMaxPktSize = "9.9" + AppInstFieldFlavor = "12" + AppInstFieldFlavorName = "12.1" + AppInstFieldState = "14" + AppInstFieldErrors = "15" + AppInstFieldCrmOverride = "16" + AppInstFieldRuntimeInfo = "17" + AppInstFieldRuntimeInfoContainerIds = "17.1" + AppInstFieldCreatedAt = "21" + AppInstFieldCreatedAtSeconds = "21.1" + AppInstFieldCreatedAtNanos = "21.2" + AppInstFieldAutoClusterIpAccess = "22" + AppInstFieldRevision = "24" + AppInstFieldForceUpdate = "25" + AppInstFieldUpdateMultiple = "26" + AppInstFieldConfigs = "27" + AppInstFieldConfigsKind = "27.1" + AppInstFieldConfigsConfig = "27.2" + AppInstFieldHealthCheck = "29" + AppInstFieldPowerState = "31" + AppInstFieldExternalVolumeSize = "32" + AppInstFieldAvailabilityZone = "33" + AppInstFieldVmFlavor = "34" + AppInstFieldOptRes = "35" + AppInstFieldUpdatedAt = "36" + AppInstFieldUpdatedAtSeconds = "36.1" + AppInstFieldUpdatedAtNanos = "36.2" + AppInstFieldRealClusterName = "37" + AppInstFieldInternalPortToLbIp = "38" + AppInstFieldInternalPortToLbIpKey = "38.1" + AppInstFieldInternalPortToLbIpValue = "38.2" + AppInstFieldDedicatedIp = "39" + AppInstFieldUniqueId = "40" + AppInstFieldDnsLabel = "41" ) // Message interface for types that can provide error messages diff --git a/sdk/edgeconnect/v2/appinstance.go b/sdk/edgeconnect/v2/appinstance.go index 52dcf1f..4fb7204 100644 --- a/sdk/edgeconnect/v2/appinstance.go +++ b/sdk/edgeconnect/v2/appinstance.go @@ -10,7 +10,8 @@ import ( "fmt" "io" "net/http" - "strings" + + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) // CreateAppInstance creates a new application instance in the specified region @@ -24,16 +25,15 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp if err != nil { return fmt.Errorf("CreateAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateAppInstance") } // Parse streaming JSON response - if _, err = parseStreamingResponse[AppInstance](resp); err != nil { + var appInstances []AppInstance + if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil { return fmt.Errorf("ShowAppInstance failed to parse response: %w", err) } @@ -45,7 +45,7 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp // ShowAppInstance retrieves a single application instance by key and region // Maps to POST /auth/ctrl/ShowAppInst -func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) { +func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) (AppInstance, error) { transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst" @@ -58,9 +58,7 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, if err != nil { return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return AppInstance{}, fmt.Errorf("app instance %s/%s: %w", @@ -73,7 +71,7 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, // Parse streaming JSON response var appInstances []AppInstance - if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil { + if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil { return AppInstance{}, fmt.Errorf("ShowAppInstance failed to parse response: %w", err) } @@ -87,12 +85,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, // ShowAppInstances retrieves all application instances matching the filter criteria // Maps to POST /auth/ctrl/ShowAppInst -func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) { +func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) { transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst" filter := AppInstanceFilter{ - AppInstance: AppInstance{Key: appInstKey, AppKey: appKey}, + AppInstance: AppInstance{Key: appInstKey}, Region: region, } @@ -100,20 +98,18 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey if err != nil { return nil, fmt.Errorf("ShowAppInstances failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowAppInstances") } + var appInstances []AppInstance if resp.StatusCode == http.StatusNotFound { - return []AppInstance{}, nil // Return empty slice for not found + return appInstances, nil // Return empty slice for not found } - var appInstances []AppInstance - if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil { + if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil { return nil, fmt.Errorf("ShowAppInstances failed to parse response: %w", err) } @@ -131,9 +127,7 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance if err != nil { return fmt.Errorf("UpdateAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "UpdateAppInstance") @@ -160,9 +154,7 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK if err != nil { return fmt.Errorf("RefreshAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "RefreshAppInstance") @@ -189,9 +181,7 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe if err != nil { return fmt.Errorf("DeleteAppInstance failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -205,89 +195,88 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe } // parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances -func parseStreamingResponse[T Message](resp *http.Response) ([]T, error) { +func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result interface{}) error { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return []T{}, fmt.Errorf("failed to read response body: %w", err) + return fmt.Errorf("failed to read response body: %w", err) } - // todo finish check the responses, test them, and make a unify result, probably need - // to update the response parameter to the message type e.g. App or AppInst - isV2, err := isV2Response(bodyBytes) - if err != nil { - return []T{}, fmt.Errorf("failed to parse streaming response: %w", err) - } - - if isV2 { - resultV2, err := parseStreamingResponseV2[T](resp.StatusCode, bodyBytes) - if err != nil { - return []T{}, err + // Try parsing as a direct JSON array first (v2 API format) + switch v := result.(type) { + case *[]AppInstance: + var appInstances []AppInstance + if err := json.Unmarshal(bodyBytes, &appInstances); err == nil { + *v = appInstances + return nil } - return resultV2, nil } - resultV1, err := parseStreamingResponseV1[T](resp.StatusCode, bodyBytes) - if err != nil { - return nil, err - } - - if !resultV1.IsSuccessful() { - return []T{}, resultV1.Error() - } - - 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 + var appInstances []AppInstance + var messages []string + var hasError bool + var errorCode int + var errorMessage string - decoder := json.NewDecoder(bytes.NewReader(bodyBytes)) - for { - var d Response[T] - if err := decoder.Decode(&d); err != nil { - if err.Error() == "EOF" { - break + 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 Responses[T]{}, fmt.Errorf("error in parsing json object into Message: %w", err) + return nil } - if d.Result.Message != "" && d.Result.Code != 0 { - responses.StatusCode = d.Result.Code + // Try parsing as Response[AppInstance] + var response Response[AppInstance] + if err := json.Unmarshal(line, &response); err != nil { + return err } - if strings.Contains(d.Data.GetMessage(), "CreateError") { - responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "CreateError")) + if response.HasData() { + appInstances = append(appInstances, response.Data) } - - if strings.Contains(d.Data.GetMessage(), "UpdateError") { - responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "UpdateError")) + 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 strings.Contains(d.Data.GetMessage(), "DeleteError") { - responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "DeleteError")) - } - - responses.Responses = append(responses.Responses, d) + if parseErr != nil { + return parseErr } - return responses, nil -} - -func isV2Response(bodyBytes []byte) (bool, error) { - if len(bodyBytes) == 0 { - return false, fmt.Errorf("malformatted response body") + // If we detected an error, return it + if hasError { + apiErr := &APIError{ + StatusCode: resp.StatusCode, + Messages: messages, + } + if errorCode > 0 { + apiErr.StatusCode = errorCode + apiErr.Code = fmt.Sprintf("%d", errorCode) + } + if errorMessage != "" { + apiErr.Messages = append([]string{errorMessage}, apiErr.Messages...) + } + return apiErr } - 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) + // Set result based on type + switch v := result.(type) { + case *[]AppInstance: + *v = appInstances + default: + return fmt.Errorf("unsupported result type: %T", result) } - return result, nil + return nil } diff --git a/sdk/edgeconnect/v2/appinstance_test.go b/sdk/edgeconnect/v2/appinstance_test.go index 04df669..e1c3d5e 100644 --- a/sdk/edgeconnect/v2/appinstance_test.go +++ b/sdk/edgeconnect/v2/appinstance_test.go @@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -157,7 +157,6 @@ func TestShowAppInstance(t *testing.T) { tests := []struct { name string appInstKey AppInstanceKey - appKey AppKey region string mockStatusCode int mockResponse string @@ -174,7 +173,6 @@ func TestShowAppInstance(t *testing.T) { Name: "testcloudlet", }, }, - appKey: AppKey{Name: "testapp"}, region: "us-west", mockStatusCode: 200, mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}} @@ -192,7 +190,6 @@ func TestShowAppInstance(t *testing.T) { Name: "testcloudlet", }, }, - appKey: AppKey{Name: "testapp"}, region: "us-west", mockStatusCode: 404, mockResponse: "", @@ -210,7 +207,7 @@ func TestShowAppInstance(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -222,7 +219,7 @@ func TestShowAppInstance(t *testing.T) { // Execute test ctx := context.Background() - appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region) + appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region) // Verify results if tt.expectError { @@ -257,14 +254,14 @@ func TestShowAppInstances(t *testing.T) { {"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() client := NewClient(server.URL) ctx := context.Background() - appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west") + appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west") require.NoError(t, err) assert.Len(t, appInstances, 2) @@ -364,7 +361,7 @@ func TestUpdateAppInstance(t *testing.T) { assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() diff --git a/sdk/edgeconnect/v2/apps.go b/sdk/edgeconnect/v2/apps.go index 61c1f4c..06d529f 100644 --- a/sdk/edgeconnect/v2/apps.go +++ b/sdk/edgeconnect/v2/apps.go @@ -4,12 +4,14 @@ package v2 import ( + "bytes" "context" + "encoding/json" "fmt" "io" "net/http" - sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http" + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) var ( @@ -27,9 +29,7 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error { if err != nil { return fmt.Errorf("CreateApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateApp") @@ -56,9 +56,7 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App if err != nil { return App{}, fmt.Errorf("ShowApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w", @@ -71,7 +69,7 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App // Parse streaming JSON response var apps []App - if apps, err = parseStreamingResponse[App](resp); err != nil { + if err := c.parseStreamingResponse(resp, &apps); err != nil { return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err) } @@ -98,20 +96,18 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([] if err != nil { return nil, fmt.Errorf("ShowApps failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowApps") } + var apps []App if resp.StatusCode == http.StatusNotFound { - return []App{}, nil // Return empty slice for not found + return apps, nil // Return empty slice for not found } - var apps []App - if apps, err = parseStreamingResponse[App](resp); err != nil { + if err := c.parseStreamingResponse(resp, &apps); err != nil { return nil, fmt.Errorf("ShowApps failed to parse response: %w", err) } @@ -129,9 +125,7 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error { if err != nil { return fmt.Errorf("UpdateApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "UpdateApp") @@ -158,9 +152,7 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er if err != nil { return fmt.Errorf("DeleteApp failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -173,6 +165,70 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er return nil } +// parseStreamingResponse parses the EdgeXR streaming JSON response format +func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) error { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + // Try parsing as a direct JSON array first (v2 API format) + switch v := result.(type) { + case *[]App: + var apps []App + if err := json.Unmarshal(bodyBytes, &apps); err == nil { + *v = apps + return nil + } + } + + // Fall back to streaming format (v1 API format) + var responses []Response[App] + var apps []App + var messages []string + + parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error { + var response Response[App] + if err := json.Unmarshal(line, &response); err != nil { + return err + } + responses = append(responses, response) + return nil + }) + + if parseErr != nil { + return parseErr + } + + // Extract data from responses + for _, response := range responses { + if response.HasData() { + apps = append(apps, response.Data) + } + if response.IsMessage() { + messages = append(messages, response.Data.GetMessage()) + } + } + + // If we have error messages, return them + if len(messages) > 0 { + return &APIError{ + StatusCode: resp.StatusCode, + Messages: messages, + } + } + + // Set result based on type + switch v := result.(type) { + case *[]App: + *v = apps + default: + return fmt.Errorf("unsupported result type: %T", result) + } + + return nil +} + // getTransport creates an HTTP transport with current client settings func (c *Client) getTransport() *sdkhttp.Transport { return sdkhttp.NewTransport( @@ -198,9 +254,7 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro bodyBytes := []byte{} if resp.Body != nil { - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() bodyBytes, _ = io.ReadAll(resp.Body) messages = append(messages, string(bodyBytes)) } diff --git a/sdk/edgeconnect/v2/apps_test.go b/sdk/edgeconnect/v2/apps_test.go index a4c202f..4ea757c 100644 --- a/sdk/edgeconnect/v2/apps_test.go +++ b/sdk/edgeconnect/v2/apps_test.go @@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) { {"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() @@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) { assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -407,3 +407,13 @@ func TestAPIError(t *testing.T) { assert.Equal(t, 400, err.StatusCode) assert.Len(t, err.Messages, 2) } + +// Helper function to create a test server that handles streaming JSON responses +func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + for _, response := range responses { + w.Write([]byte(response + "\n")) + } + })) +} diff --git a/sdk/edgeconnect/v2/auth.go b/sdk/edgeconnect/v2/auth.go index f428f64..a1f33a2 100644 --- a/sdk/edgeconnect/v2/auth.go +++ b/sdk/edgeconnect/v2/auth.go @@ -138,9 +138,7 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e if err != nil { return "", err } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // Read response body - same as existing implementation body, err := io.ReadAll(resp.Body) diff --git a/sdk/edgeconnect/v2/auth_test.go b/sdk/edgeconnect/v2/auth_test.go index 34ebcaf..0fc5b24 100644 --- a/sdk/edgeconnect/v2/auth_test.go +++ b/sdk/edgeconnect/v2/auth_test.go @@ -56,7 +56,7 @@ func TestUsernamePasswordProvider_Success(t *testing.T) { // Return token response := map[string]string{"token": "dynamic-token-456"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -75,7 +75,7 @@ func TestUsernamePasswordProvider_LoginFailure(t *testing.T) { // Mock login server that returns error loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("Invalid credentials")) + w.Write([]byte("Invalid credentials")) })) defer loginServer.Close() @@ -99,7 +99,7 @@ func TestUsernamePasswordProvider_TokenCaching(t *testing.T) { callCount++ response := map[string]string{"token": "cached-token-789"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -128,7 +128,7 @@ func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) { callCount++ response := map[string]string{"token": "refreshed-token-999"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -157,7 +157,7 @@ func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) { callCount++ response := map[string]string{"token": "new-token-after-invalidation"} w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) + json.NewEncoder(w).Encode(response) })) defer loginServer.Close() @@ -185,7 +185,7 @@ func TestUsernamePasswordProvider_BadJSONResponse(t *testing.T) { // Mock server returning invalid JSON loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte("invalid json response")) + w.Write([]byte("invalid json response")) })) defer loginServer.Close() diff --git a/sdk/edgeconnect/v2/cloudlet.go b/sdk/edgeconnect/v2/cloudlet.go index c877486..85ef522 100644 --- a/sdk/edgeconnect/v2/cloudlet.go +++ b/sdk/edgeconnect/v2/cloudlet.go @@ -9,7 +9,7 @@ import ( "fmt" "net/http" - sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http" + sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http" ) // CreateCloudlet creates a new cloudlet in the specified region @@ -22,9 +22,7 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er if err != nil { return fmt.Errorf("CreateCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 { return c.handleErrorResponse(resp, "CreateCloudlet") @@ -51,9 +49,7 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi if err != nil { return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w", @@ -93,9 +89,7 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg if err != nil { return nil, fmt.Errorf("ShowCloudlets failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { return nil, c.handleErrorResponse(resp, "ShowCloudlets") @@ -129,9 +123,7 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re if err != nil { return fmt.Errorf("DeleteCloudlet failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // 404 is acceptable for delete operations (already deleted) if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound { @@ -159,9 +151,7 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe if err != nil { return nil, fmt.Errorf("GetCloudletManifest failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w", @@ -199,9 +189,7 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud if err != nil { return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err) } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w", diff --git a/sdk/edgeconnect/v2/cloudlet_test.go b/sdk/edgeconnect/v2/cloudlet_test.go index d8ffb75..8f2cc06 100644 --- a/sdk/edgeconnect/v2/cloudlet_test.go +++ b/sdk/edgeconnect/v2/cloudlet_test.go @@ -70,7 +70,7 @@ func TestCreateCloudlet(t *testing.T) { assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(tt.mockStatusCode) - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) })) defer server.Close() @@ -140,7 +140,7 @@ func TestShowCloudlet(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -187,7 +187,7 @@ func TestShowCloudlets(t *testing.T) { {"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}} ` w.WriteHeader(200) - _, _ = w.Write([]byte(response)) + w.Write([]byte(response)) })) defer server.Close() @@ -312,7 +312,7 @@ func TestGetCloudletManifest(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() @@ -380,7 +380,7 @@ func TestGetCloudletResourceUsage(t *testing.T) { w.WriteHeader(tt.mockStatusCode) if tt.mockResponse != "" { - _, _ = w.Write([]byte(tt.mockResponse)) + w.Write([]byte(tt.mockResponse)) } })) defer server.Close() diff --git a/sdk/edgeconnect/v2/types.go b/sdk/edgeconnect/v2/types.go index 7dea92e..0bb6875 100644 --- a/sdk/edgeconnect/v2/types.go +++ b/sdk/edgeconnect/v2/types.go @@ -60,74 +60,74 @@ const ( // AppInstance field constants for partial updates (based on EdgeXR API specification) const ( - AppInstFieldKey = "2" - AppInstFieldKeyAppKey = "2.1" - AppInstFieldKeyAppKeyOrganization = "2.1.1" - AppInstFieldKeyAppKeyName = "2.1.2" - AppInstFieldKeyAppKeyVersion = "2.1.3" - AppInstFieldKeyClusterInstKey = "2.4" - AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1" - AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1" - AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2" - AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1" - AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2" + AppInstFieldKey = "2" + AppInstFieldKeyAppKey = "2.1" + AppInstFieldKeyAppKeyOrganization = "2.1.1" + AppInstFieldKeyAppKeyName = "2.1.2" + AppInstFieldKeyAppKeyVersion = "2.1.3" + AppInstFieldKeyClusterInstKey = "2.4" + AppInstFieldKeyClusterInstKeyClusterKey = "2.4.1" + AppInstFieldKeyClusterInstKeyClusterKeyName = "2.4.1.1" + AppInstFieldKeyClusterInstKeyCloudletKey = "2.4.2" + AppInstFieldKeyClusterInstKeyCloudletKeyOrganization = "2.4.2.1" + AppInstFieldKeyClusterInstKeyCloudletKeyName = "2.4.2.2" AppInstFieldKeyClusterInstKeyCloudletKeyFederatedOrganization = "2.4.2.3" - AppInstFieldKeyClusterInstKeyOrganization = "2.4.3" - AppInstFieldCloudletLoc = "3" - AppInstFieldCloudletLocLatitude = "3.1" - AppInstFieldCloudletLocLongitude = "3.2" - AppInstFieldCloudletLocHorizontalAccuracy = "3.3" - AppInstFieldCloudletLocVerticalAccuracy = "3.4" - AppInstFieldCloudletLocAltitude = "3.5" - AppInstFieldCloudletLocCourse = "3.6" - AppInstFieldCloudletLocSpeed = "3.7" - AppInstFieldCloudletLocTimestamp = "3.8" - AppInstFieldCloudletLocTimestampSeconds = "3.8.1" - AppInstFieldCloudletLocTimestampNanos = "3.8.2" - AppInstFieldUri = "4" - AppInstFieldLiveness = "6" - AppInstFieldMappedPorts = "9" - AppInstFieldMappedPortsProto = "9.1" - AppInstFieldMappedPortsInternalPort = "9.2" - AppInstFieldMappedPortsPublicPort = "9.3" - AppInstFieldMappedPortsFqdnPrefix = "9.5" - AppInstFieldMappedPortsEndPort = "9.6" - AppInstFieldMappedPortsTls = "9.7" - AppInstFieldMappedPortsNginx = "9.8" - AppInstFieldMappedPortsMaxPktSize = "9.9" - AppInstFieldFlavor = "12" - AppInstFieldFlavorName = "12.1" - AppInstFieldState = "14" - AppInstFieldErrors = "15" - AppInstFieldCrmOverride = "16" - AppInstFieldRuntimeInfo = "17" - AppInstFieldRuntimeInfoContainerIds = "17.1" - AppInstFieldCreatedAt = "21" - AppInstFieldCreatedAtSeconds = "21.1" - AppInstFieldCreatedAtNanos = "21.2" - AppInstFieldAutoClusterIpAccess = "22" - AppInstFieldRevision = "24" - AppInstFieldForceUpdate = "25" - AppInstFieldUpdateMultiple = "26" - AppInstFieldConfigs = "27" - AppInstFieldConfigsKind = "27.1" - AppInstFieldConfigsConfig = "27.2" - AppInstFieldHealthCheck = "29" - AppInstFieldPowerState = "31" - AppInstFieldExternalVolumeSize = "32" - AppInstFieldAvailabilityZone = "33" - AppInstFieldVmFlavor = "34" - AppInstFieldOptRes = "35" - AppInstFieldUpdatedAt = "36" - AppInstFieldUpdatedAtSeconds = "36.1" - AppInstFieldUpdatedAtNanos = "36.2" - AppInstFieldRealClusterName = "37" - AppInstFieldInternalPortToLbIp = "38" - AppInstFieldInternalPortToLbIpKey = "38.1" - AppInstFieldInternalPortToLbIpValue = "38.2" - AppInstFieldDedicatedIp = "39" - AppInstFieldUniqueId = "40" - AppInstFieldDnsLabel = "41" + AppInstFieldKeyClusterInstKeyOrganization = "2.4.3" + AppInstFieldCloudletLoc = "3" + AppInstFieldCloudletLocLatitude = "3.1" + AppInstFieldCloudletLocLongitude = "3.2" + AppInstFieldCloudletLocHorizontalAccuracy = "3.3" + AppInstFieldCloudletLocVerticalAccuracy = "3.4" + AppInstFieldCloudletLocAltitude = "3.5" + AppInstFieldCloudletLocCourse = "3.6" + AppInstFieldCloudletLocSpeed = "3.7" + AppInstFieldCloudletLocTimestamp = "3.8" + AppInstFieldCloudletLocTimestampSeconds = "3.8.1" + AppInstFieldCloudletLocTimestampNanos = "3.8.2" + AppInstFieldUri = "4" + AppInstFieldLiveness = "6" + AppInstFieldMappedPorts = "9" + AppInstFieldMappedPortsProto = "9.1" + AppInstFieldMappedPortsInternalPort = "9.2" + AppInstFieldMappedPortsPublicPort = "9.3" + AppInstFieldMappedPortsFqdnPrefix = "9.5" + AppInstFieldMappedPortsEndPort = "9.6" + AppInstFieldMappedPortsTls = "9.7" + AppInstFieldMappedPortsNginx = "9.8" + AppInstFieldMappedPortsMaxPktSize = "9.9" + AppInstFieldFlavor = "12" + AppInstFieldFlavorName = "12.1" + AppInstFieldState = "14" + AppInstFieldErrors = "15" + AppInstFieldCrmOverride = "16" + AppInstFieldRuntimeInfo = "17" + AppInstFieldRuntimeInfoContainerIds = "17.1" + AppInstFieldCreatedAt = "21" + AppInstFieldCreatedAtSeconds = "21.1" + AppInstFieldCreatedAtNanos = "21.2" + AppInstFieldAutoClusterIpAccess = "22" + AppInstFieldRevision = "24" + AppInstFieldForceUpdate = "25" + AppInstFieldUpdateMultiple = "26" + AppInstFieldConfigs = "27" + AppInstFieldConfigsKind = "27.1" + AppInstFieldConfigsConfig = "27.2" + AppInstFieldHealthCheck = "29" + AppInstFieldPowerState = "31" + AppInstFieldExternalVolumeSize = "32" + AppInstFieldAvailabilityZone = "33" + AppInstFieldVmFlavor = "34" + AppInstFieldOptRes = "35" + AppInstFieldUpdatedAt = "36" + AppInstFieldUpdatedAtSeconds = "36.1" + AppInstFieldUpdatedAtNanos = "36.2" + AppInstFieldRealClusterName = "37" + AppInstFieldInternalPortToLbIp = "38" + AppInstFieldInternalPortToLbIpKey = "38.1" + AppInstFieldInternalPortToLbIpValue = "38.2" + AppInstFieldDedicatedIp = "39" + AppInstFieldUniqueId = "40" + AppInstFieldDnsLabel = "41" ) // Message interface for types that can provide error messages @@ -291,8 +291,7 @@ type DeleteAppInstanceInput struct { // Response wraps a single API response type Response[T Message] struct { - ResultResponse `json:",inline"` - Data T `json:"data"` + Data T `json:"data"` } func (res *Response[T]) HasData() bool { @@ -327,7 +326,6 @@ func (r *ResultResponse) GetCode() int { type Responses[T Message] struct { Responses []Response[T] `json:"responses,omitempty"` StatusCode int `json:"-"` - Errors []error `json:"-"` } func (r *Responses[T]) GetData() []T { @@ -346,15 +344,12 @@ func (r *Responses[T]) GetMessages() []string { if v.IsMessage() { messages = append(messages, v.Data.GetMessage()) } - if v.Result.Message != "" { - messages = append(messages, v.Result.Message) - } } return messages } func (r *Responses[T]) IsSuccessful() bool { - return len(r.Errors) == 0 && (r.StatusCode >= 200 && r.StatusCode < 400) + return r.StatusCode >= 200 && r.StatusCode < 400 } func (r *Responses[T]) Error() error { @@ -415,7 +410,3 @@ type CloudletResourceUsage struct { Region string `json:"region"` Usage map[string]interface{} `json:"usage"` } - -type ErrorMessage struct { - Message string -} diff --git a/sdk/examples/comprehensive/k8s-deployment.yaml b/sdk/examples/comprehensive/k8s-deployment.yaml index dff3649..2a0a741 100644 --- a/sdk/examples/comprehensive/k8s-deployment.yaml +++ b/sdk/examples/comprehensive/k8s-deployment.yaml @@ -18,7 +18,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: edgeconnect-coder-deployment - #namespace: gitea spec: replicas: 1 selector: diff --git a/sdk/examples/comprehensive/main.go b/sdk/examples/comprehensive/main.go index 25a4aa5..d3fb922 100644 --- a/sdk/examples/comprehensive/main.go +++ b/sdk/examples/comprehensive/main.go @@ -12,7 +12,7 @@ import ( "strings" "time" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) func main() { @@ -193,7 +193,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow }, } - instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, v2.AppKey{}, config.Region, 5*time.Minute) + instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, config.Region, 5*time.Minute) if err != nil { return fmt.Errorf("failed to wait for instance ready: %w", err) } @@ -207,7 +207,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow // 6. List Application Instances fmt.Println("\n6️⃣ Listing application instances...") - instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, v2.AppKey{}, config.Region) + instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, config.Region) if err != nil { return fmt.Errorf("failed to list app instances: %w", err) } @@ -306,7 +306,7 @@ func getEnvOrDefault(key, defaultValue string) string { } // waitForInstanceReady polls the instance status until it's no longer "Creating" or timeout -func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string, timeout time.Duration) (v2.AppInstance, error) { +func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, region string, timeout time.Duration) (v2.AppInstance, error) { timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -321,7 +321,7 @@ func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppI return v2.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout) case <-ticker.C: - instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, appKey, region) + instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region) if err != nil { // Log error but continue polling fmt.Printf(" ⚠️ Error checking instance state: %v\n", err) diff --git a/sdk/examples/deploy_app.go b/sdk/examples/deploy_app.go index d35ff9c..84297dc 100644 --- a/sdk/examples/deploy_app.go +++ b/sdk/examples/deploy_app.go @@ -12,7 +12,7 @@ import ( "strings" "time" - v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2" + v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2" ) func main() { diff --git a/sdk/examples/forgejo-runner/EdgeConnectConfig_v1.yaml b/sdk/examples/forgejo-runner/EdgeConnectConfig.yaml similarity index 100% rename from sdk/examples/forgejo-runner/EdgeConnectConfig_v1.yaml rename to sdk/examples/forgejo-runner/EdgeConnectConfig.yaml diff --git a/sdk/examples/forgejo-runner/EdgeConnectConfig_v2.yaml b/sdk/examples/forgejo-runner/EdgeConnectConfig_v2.yaml deleted file mode 100644 index 5afcf4b..0000000 --- a/sdk/examples/forgejo-runner/EdgeConnectConfig_v2.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# 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" diff --git a/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v1.yaml b/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v1.yaml deleted file mode 100644 index 9710327..0000000 --- a/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v1.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# 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" diff --git a/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v2.yaml b/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v2.yaml deleted file mode 100644 index 9fb80df..0000000 --- a/sdk/examples/ubuntu-buildkit/EdgeConnectConfig_v2.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# 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" diff --git a/sdk/examples/ubuntu-buildkit/k8s-deployment.yaml b/sdk/examples/ubuntu-buildkit/k8s-deployment.yaml deleted file mode 100644 index d4d3dd8..0000000 --- a/sdk/examples/ubuntu-buildkit/k8s-deployment.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# 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 diff --git a/sdk/internal/http/transport.go b/sdk/internal/http/transport.go index 35b71b8..c3bbab1 100644 --- a/sdk/internal/http/transport.go +++ b/sdk/internal/http/transport.go @@ -162,9 +162,7 @@ func (t *Transport) CallJSON(ctx context.Context, method, url string, body inter if err != nil { return resp, err } - defer func() { - _ = resp.Body.Close() - }() + defer resp.Body.Close() // Read response body respBody, err := io.ReadAll(resp.Body)