Compare commits

...

10 commits
v2.0.1 ... main

Author SHA1 Message Date
02856be541
fix: Fixed error handling
All checks were successful
test / test (push) Successful in 44s
ci / goreleaser (push) Successful in 1m39s
2025-11-17 15:43:08 +01:00
e38d7e84d5
parseStreamingResponse is now unified for all objects under both versions
All checks were successful
test / test (push) Successful in 45s
2025-11-17 14:40:47 +01:00
2909e0d1b4
feat(api): add nicer error message to format issues indicating permission denied
All checks were successful
test / test (push) Successful in 42s
2025-11-14 12:11:24 +01:00
ece2955a2a
feat(api): Added AppKey to ShowAppInstances
All checks were successful
test / test (push) Successful in 56s
ci / goreleaser (push) Successful in 54s
2025-11-13 16:59:38 +01:00
a51e2ae454
feat(api): Added AppKey property to ShowAppInstances
All checks were successful
test / test (push) Successful in 55s
ci / goreleaser (push) Successful in 1m9s
2025-11-13 16:15:15 +01:00
ece3dddfe6 feat(edge): Added ubuntu buildkit edge v1 (running) and v2 (not running) example
All checks were successful
test / test (push) Successful in 1m10s
2025-10-27 16:32:57 +01:00
9772a072e8 chore(linting): Fixed all linter errors
All checks were successful
test / test (push) Successful in 46s
2025-10-22 12:47:15 +02:00
f3cbfa3723 fix(deploy): Fixed glitch when updating an app inst with an invalid manifest
All checks were successful
test / test (push) Successful in 16s
2025-10-22 10:31:03 +02:00
26ba07200e test(orca-forgjo-runner): added v2 example to deploy forgejo runner in orca
All checks were successful
test / test (push) Successful in 16s
2025-10-21 13:44:33 +02:00
716c8e79e4 fix(version): update imports and go.mod to allow v2
All checks were successful
test / test (push) Successful in 51s
ci / goreleaser (push) Successful in 24s
2025-10-21 11:40:35 +02:00
59 changed files with 1118 additions and 553 deletions

2
.gitignore vendored
View file

@ -5,3 +5,5 @@ dist/
### direnv ###
.direnv
.envrc
edge-connect-client

View file

@ -28,7 +28,7 @@ clean:
# Lint the code
lint:
golangci-lint run
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2 run
# Run all checks (generate, test, lint)
check: test lint

View file

@ -10,8 +10,8 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@ -37,7 +37,7 @@ func validateBaseURL(baseURL string) error {
return fmt.Errorf("user and or password should not be set")
}
if !(url.Path == "" || url.Path == "/") {
if url.Path != "" && url.Path != "/" {
return fmt.Errorf("should not contain any path '%s'", url.Path)
}
@ -291,12 +291,18 @@ func init() {
cmd.Flags().StringVarP(&appName, "name", "n", "", "application name")
cmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
cmd.Flags().StringVarP(&region, "region", "r", "", "region (required)")
cmd.MarkFlagRequired("org")
cmd.MarkFlagRequired("region")
if err := cmd.MarkFlagRequired("org"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("region"); err != nil {
panic(err)
}
}
// Add required name flag for specific commands
for _, cmd := range []*cobra.Command{createAppCmd, showAppCmd, deleteAppCmd} {
cmd.MarkFlagRequired("name")
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(err)
}
}
}

View file

@ -10,9 +10,9 @@ import (
"path/filepath"
"strings"
applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v1"
applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/apply/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
applyv1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v1"
applyv2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/apply/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"github.com/spf13/cobra"
)
@ -31,7 +31,7 @@ the necessary changes to deploy your applications across multiple cloudlets.`,
Run: func(cmd *cobra.Command, args []string) {
if configFile == "" {
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
cmd.Usage()
_ = cmd.Usage()
os.Exit(1)
}
@ -208,20 +208,6 @@ func runApplyV2(cfg *config.EdgeConnectConfig, manifestContent string, isDryRun
return displayDeploymentResults(deployResult)
}
type deploymentResult interface {
IsSuccess() bool
GetDuration() string
GetCompletedActions() []actionResult
GetFailedActions() []actionResult
GetError() error
}
type actionResult interface {
GetType() string
GetTarget() string
GetError() error
}
func displayDeploymentResults(result interface{}) error {
// Use reflection or type assertion to handle both v1 and v2 result types
// For now, we'll use a simple approach that works with both
@ -288,7 +274,7 @@ func displayDeploymentResultsV2(deployResult *applyv2.ExecutionResult) error {
func confirmDeployment() bool {
fmt.Print("Do you want to proceed? (yes/no): ")
var response string
fmt.Scanln(&response)
_, _ = fmt.Scanln(&response)
switch response {
case "yes", "y", "YES", "Y":
@ -305,5 +291,7 @@ func init() {
applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without applying them")
applyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "automatically approve the deployment plan")
applyCmd.MarkFlagRequired("file")
if err := applyCmd.MarkFlagRequired("file"); err != nil {
panic(err)
}
}

View file

@ -10,9 +10,9 @@ import (
"path/filepath"
"strings"
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v1"
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/delete/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
deletev1 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v1"
deletev2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/delete/v2"
"github.com/spf13/cobra"
)
@ -31,7 +31,7 @@ Instances are always deleted before the application.`,
Run: func(cmd *cobra.Command, args []string) {
if deleteConfigFile == "" {
fmt.Fprintf(os.Stderr, "Error: configuration file is required\n")
cmd.Usage()
_ = cmd.Usage()
os.Exit(1)
}
@ -273,7 +273,7 @@ func displayDeletionResultsV2(deleteResult *deletev2.DeletionResult) error {
func confirmDeletion() bool {
fmt.Print("Do you want to proceed with deletion? (yes/no): ")
var response string
fmt.Scanln(&response)
_, _ = fmt.Scanln(&response)
switch response {
case "yes", "y", "YES", "Y":
@ -290,5 +290,7 @@ func init() {
deleteCmd.Flags().BoolVar(&deleteDryRun, "dry-run", false, "preview deletion without actually deleting resources")
deleteCmd.Flags().BoolVar(&deleteAutoApprove, "auto-approve", false, "automatically approve the deletion plan")
deleteCmd.MarkFlagRequired("file")
if err := deleteCmd.MarkFlagRequired("file"); err != nil {
panic(err)
}
}

View file

@ -5,8 +5,8 @@ import (
"fmt"
"os"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/spf13/cobra"
)
@ -15,6 +15,7 @@ var (
cloudletOrg string
instanceName string
flavorName string
appId string
)
var appInstanceCmd = &cobra.Command{
@ -104,7 +105,8 @@ var showInstanceCmd = &cobra.Command{
Name: cloudletName,
},
}
instance, err := c.ShowAppInstance(context.Background(), instanceKey, region)
appkey := edgeconnect.AppKey{Name: appId}
instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region)
if err != nil {
fmt.Printf("Error showing app instance: %v\n", err)
os.Exit(1)
@ -120,7 +122,8 @@ var showInstanceCmd = &cobra.Command{
Name: cloudletName,
},
}
instance, err := c.ShowAppInstance(context.Background(), instanceKey, region)
appkey := v2.AppKey{Name: appId}
instance, err := c.ShowAppInstance(context.Background(), instanceKey, appkey, region)
if err != nil {
fmt.Printf("Error showing app instance: %v\n", err)
os.Exit(1)
@ -146,7 +149,8 @@ var listInstancesCmd = &cobra.Command{
Name: cloudletName,
},
}
instances, err := c.ShowAppInstances(context.Background(), instanceKey, region)
appKey := edgeconnect.AppKey{Name: appId}
instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region)
if err != nil {
fmt.Printf("Error listing app instances: %v\n", err)
os.Exit(1)
@ -165,7 +169,8 @@ var listInstancesCmd = &cobra.Command{
Name: cloudletName,
},
}
instances, err := c.ShowAppInstances(context.Background(), instanceKey, region)
appKey := v2.AppKey{Name: appId}
instances, err := c.ShowAppInstances(context.Background(), instanceKey, appKey, region)
if err != nil {
fmt.Printf("Error listing app instances: %v\n", err)
os.Exit(1)
@ -229,18 +234,33 @@ func init() {
cmd.Flags().StringVarP(&cloudletName, "cloudlet", "c", "", "cloudlet name (required)")
cmd.Flags().StringVarP(&cloudletOrg, "cloudlet-org", "", "", "cloudlet organization (required)")
cmd.Flags().StringVarP(&region, "region", "r", "", "region (required)")
cmd.Flags().StringVarP(&appId, "app-id", "i", "", "application id")
cmd.MarkFlagRequired("org")
cmd.MarkFlagRequired("name")
cmd.MarkFlagRequired("cloudlet")
cmd.MarkFlagRequired("cloudlet-org")
cmd.MarkFlagRequired("region")
if err := cmd.MarkFlagRequired("org"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("cloudlet"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("cloudlet-org"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("region"); err != nil {
panic(err)
}
}
// Add additional flags for create command
createInstanceCmd.Flags().StringVarP(&appName, "app", "a", "", "application name (required)")
createInstanceCmd.Flags().StringVarP(&appVersion, "version", "v", "", "application version")
createInstanceCmd.Flags().StringVarP(&flavorName, "flavor", "f", "", "flavor name (required)")
createInstanceCmd.MarkFlagRequired("app")
createInstanceCmd.MarkFlagRequired("flavor")
if err := createInstanceCmd.MarkFlagRequired("app"); err != nil {
panic(err)
}
if err := createInstanceCmd.MarkFlagRequired("flavor"); err != nil {
panic(err)
}
}

View file

@ -44,19 +44,35 @@ func init() {
rootCmd.PersistentFlags().StringVar(&apiVersion, "api-version", "v2", "API version to use (v1 or v2)")
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username"))
viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password"))
viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version"))
if err := viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")); err != nil {
panic(err)
}
if err := viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")); err != nil {
panic(err)
}
if err := viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")); err != nil {
panic(err)
}
if err := viper.BindPFlag("api_version", rootCmd.PersistentFlags().Lookup("api-version")); err != nil {
panic(err)
}
}
func initConfig() {
viper.AutomaticEnv()
viper.SetEnvPrefix("EDGE_CONNECT")
viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL")
viper.BindEnv("username", "EDGE_CONNECT_USERNAME")
viper.BindEnv("password", "EDGE_CONNECT_PASSWORD")
viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION")
if err := viper.BindEnv("base_url", "EDGE_CONNECT_BASE_URL"); err != nil {
panic(err)
}
if err := viper.BindEnv("username", "EDGE_CONNECT_USERNAME"); err != nil {
panic(err)
}
if err := viper.BindEnv("password", "EDGE_CONNECT_PASSWORD"); err != nil {
panic(err)
}
if err := viper.BindEnv("api_version", "EDGE_CONNECT_API_VERSION"); err != nil {
panic(err)
}
if cfgFile != "" {
viper.SetConfigFile(cfgFile)

2
go.mod
View file

@ -1,4 +1,4 @@
module edp.buildth.ing/DevFW-CICD/edge-connect-client
module edp.buildth.ing/DevFW-CICD/edge-connect-client/v2
go 1.25.1

View file

@ -7,8 +7,8 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// ResourceManagerInterface defines the interface for resource management

View file

@ -10,8 +10,8 @@ import (
"testing"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

View file

@ -11,8 +11,8 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// EdgeConnectClientInterface defines the methods needed for deployment planning
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error
UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error)
ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error)
CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error
UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
@ -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,12 +323,7 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
// Extract outbound connections from the app
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
for i, conn := range app.RequiredOutboundConnections {
current.OutboundConnections[i] = SecurityRule{
Protocol: conn.Protocol,
PortRangeMin: conn.PortRangeMin,
PortRangeMax: conn.PortRangeMax,
RemoteCIDR: conn.RemoteCIDR,
}
current.OutboundConnections[i] = SecurityRule(conn)
}
return current, nil
@ -347,8 +342,11 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
Name: desired.CloudletName,
},
}
appKey := edgeconnect.AppKey{
Name: desired.AppName,
}
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region)
if err != nil {
return nil, err
}
@ -392,7 +390,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str
// Compare outbound connections
outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
if len(outboundChanges) > 0 {
sb:= strings.Builder{}
sb := strings.Builder{}
sb.WriteString("Outbound connections changed:\n")
for _, change := range outboundChanges {
sb.WriteString(change)
@ -470,7 +468,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
if err != nil {
return "", fmt.Errorf("failed to open manifest file: %w", err)
}
defer file.Close()
defer func() {
_ = file.Close()
}()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
@ -505,18 +505,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
var duration time.Duration
// App operations
if plan.AppAction.Type == ActionCreate {
switch plan.AppAction.Type {
case ActionCreate:
duration += 30 * time.Second
} else if plan.AppAction.Type == ActionUpdate {
case ActionUpdate:
duration += 15 * time.Second
}
// Instance operations (can be done in parallel)
instanceDuration := time.Duration(0)
for _, action := range plan.InstanceActions {
if action.Type == ActionCreate {
switch action.Type {
case ActionCreate:
instanceDuration = max(instanceDuration, 2*time.Minute)
} else if action.Type == ActionUpdate {
case ActionUpdate:
instanceDuration = max(instanceDuration, 1*time.Minute)
}
}

View file

@ -9,8 +9,8 @@ import (
"testing"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect.
return args.Get(0).(edgeconnect.App), args.Error(1)
}
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error) {
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) (edgeconnect.AppInstance, error) {
args := m.Called(ctx, instanceKey, region)
if args.Get(0) == nil {
return edgeconnect.AppInstance{}, args.Error(1)
@ -75,14 +75,6 @@ func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey edgeconnect
return args.Get(0).([]edgeconnect.App), args.Error(1)
}
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error) {
args := m.Called(ctx, instanceKey, region)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]edgeconnect.AppInstance), args.Error(1)
}
func TestNewPlanner(t *testing.T) {
mockClient := &MockEdgeConnectClient{}
planner := NewPlanner(mockClient)

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
)
// DeploymentStrategy represents the type of deployment strategy

View file

@ -10,8 +10,8 @@ import (
"sync"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// RecreateStrategy implements the recreate deployment strategy

View file

@ -7,8 +7,8 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// SecurityRule defines network access rules (alias to SDK type for consistency)

View file

@ -4,11 +4,13 @@ package v2
import (
"context"
"errors"
"fmt"
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// ResourceManagerInterface defines the interface for resource management
@ -204,7 +206,8 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re
rollbackErrors := []error{}
// Rollback completed instances (in reverse order)
// Phase 1: Delete resources that were created in this deployment attempt (in reverse order)
rm.logf("Phase 1: Rolling back created resources")
for i := len(result.CompletedActions) - 1; i >= 0; i-- {
action := result.CompletedActions[i]
@ -218,6 +221,32 @@ func (rm *EdgeConnectResourceManager) RollbackDeployment(ctx context.Context, re
}
}
// Phase 2: Restore resources that were deleted before the failed deployment
// This is critical for RecreateStrategy which deletes everything before recreating
if result.DeletedAppBackup != nil || len(result.DeletedInstancesBackup) > 0 {
rm.logf("Phase 2: Restoring deleted resources")
// Restore app first (must exist before instances can be created)
if result.DeletedAppBackup != nil {
if err := rm.restoreApp(ctx, result.DeletedAppBackup); err != nil {
rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore app: %w", err))
rm.logf("Failed to restore app: %v", err)
} else {
rm.logf("Successfully restored app: %s", result.DeletedAppBackup.App.Key.Name)
}
}
// Restore instances
for _, backup := range result.DeletedInstancesBackup {
if err := rm.restoreInstance(ctx, &backup); err != nil {
rollbackErrors = append(rollbackErrors, fmt.Errorf("failed to restore instance %s: %w", backup.Instance.Key.Name, err))
rm.logf("Failed to restore instance %s: %v", backup.Instance.Key.Name, err)
} else {
rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name)
}
}
}
if len(rollbackErrors) > 0 {
return fmt.Errorf("rollback encountered %d errors: %v", len(rollbackErrors), rollbackErrors)
}
@ -278,6 +307,125 @@ func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, acti
return fmt.Errorf("instance action not found for rollback: %s", action.Target)
}
// restoreApp recreates an app that was deleted during deployment
func (rm *EdgeConnectResourceManager) restoreApp(ctx context.Context, backup *AppBackup) error {
rm.logf("Restoring app: %s/%s version %s",
backup.App.Key.Organization, backup.App.Key.Name, backup.App.Key.Version)
// Build a clean app input with only creation-safe fields
// We must exclude read-only fields like CreatedAt, UpdatedAt, etc.
appInput := &v2.NewAppInput{
Region: backup.Region,
App: v2.App{
Key: backup.App.Key,
Deployment: backup.App.Deployment,
ImageType: backup.App.ImageType,
ImagePath: backup.App.ImagePath,
AllowServerless: backup.App.AllowServerless,
DefaultFlavor: backup.App.DefaultFlavor,
ServerlessConfig: backup.App.ServerlessConfig,
DeploymentManifest: backup.App.DeploymentManifest,
DeploymentGenerator: backup.App.DeploymentGenerator,
RequiredOutboundConnections: backup.App.RequiredOutboundConnections,
// Explicitly omit read-only fields like CreatedAt, UpdatedAt, Fields, etc.
},
}
if err := rm.client.CreateApp(ctx, appInput); err != nil {
return fmt.Errorf("failed to restore app: %w", err)
}
rm.logf("Successfully restored app: %s", backup.App.Key.Name)
return nil
}
// restoreInstance recreates an instance that was deleted during deployment
func (rm *EdgeConnectResourceManager) restoreInstance(ctx context.Context, backup *InstanceBackup) error {
rm.logf("Restoring instance: %s on %s:%s",
backup.Instance.Key.Name,
backup.Instance.Key.CloudletKey.Organization,
backup.Instance.Key.CloudletKey.Name)
// Build a clean instance input with only creation-safe fields
// We must exclude read-only fields like CloudletLoc, CreatedAt, etc.
instanceInput := &v2.NewAppInstanceInput{
Region: backup.Region,
AppInst: v2.AppInstance{
Key: backup.Instance.Key,
AppKey: backup.Instance.AppKey,
Flavor: backup.Instance.Flavor,
// Explicitly omit read-only fields like CloudletLoc, State, PowerState, CreatedAt, etc.
},
}
// Retry logic to handle namespace termination race conditions
maxRetries := 5
retryDelay := 10 * time.Second
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
rm.logf("Retrying instance restore %s (attempt %d/%d)", backup.Instance.Key.Name, attempt, maxRetries)
select {
case <-time.After(retryDelay):
case <-ctx.Done():
return ctx.Err()
}
}
err := rm.client.CreateAppInstance(ctx, instanceInput)
if err == nil {
rm.logf("Successfully restored instance: %s", backup.Instance.Key.Name)
return nil
}
lastErr = err
// Check if error is retryable
if !rm.isRetryableError(err) {
rm.logf("Failed to restore instance %s: %v (non-retryable error, giving up)", backup.Instance.Key.Name, err)
return fmt.Errorf("failed to restore instance: %w", err)
}
if attempt < maxRetries {
rm.logf("Failed to restore instance %s: %v (will retry)", backup.Instance.Key.Name, err)
}
}
return fmt.Errorf("failed to restore instance after %d attempts: %w", maxRetries+1, lastErr)
}
// isRetryableError determines if an error should be retried
func (rm *EdgeConnectResourceManager) isRetryableError(err error) bool {
if err == nil {
return false
}
errStr := strings.ToLower(err.Error())
// Special case: Kubernetes namespace termination race condition
// This is a transient 400 error that should be retried
if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") {
return true
}
// Check if it's an APIError with a status code
var apiErr *v2.APIError
if errors.As(err, &apiErr) {
// Don't retry client errors (4xx)
if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 {
return false
}
// Retry server errors (5xx)
if apiErr.StatusCode >= 500 {
return true
}
}
// Retry all other errors (network issues, timeouts, etc.)
return true
}
// logf logs a message if a logger is configured
func (rm *EdgeConnectResourceManager) logf(format string, v ...interface{}) {
if rm.logger != nil {

View file

@ -7,11 +7,12 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -464,6 +465,111 @@ func TestRollbackDeploymentFailure(t *testing.T) {
mockClient.AssertExpectations(t)
}
func TestRollbackDeploymentWithRestore(t *testing.T) {
mockClient := &MockResourceClient{}
logger := &TestLogger{}
manager := NewResourceManager(mockClient, WithLogger(logger), WithStrategyConfig(createTestStrategyConfig()))
plan := createTestDeploymentPlan()
// Simulate a RecreateStrategy scenario:
// 1. Old app and instance were deleted and backed up
// 2. New app was created successfully
// 3. New instance creation failed
// 4. Rollback should: delete new app, restore old app, restore old instance
oldApp := v2.App{
Key: v2.AppKey{
Organization: "test-org",
Name: "test-app",
Version: "1.0.0",
},
Deployment: "kubernetes",
DeploymentManifest: "old-manifest-content",
}
oldInstance := v2.AppInstance{
Key: v2.AppInstanceKey{
Organization: "test-org",
Name: "test-app-1.0.0-instance",
CloudletKey: v2.CloudletKey{
Organization: "test-cloudlet-org",
Name: "test-cloudlet",
},
},
AppKey: v2.AppKey{
Organization: "test-org",
Name: "test-app",
Version: "1.0.0",
},
Flavor: v2.Flavor{Name: "small"},
}
result := &ExecutionResult{
Plan: plan,
// Completed actions: new app was created before failure
CompletedActions: []ActionResult{
{
Type: ActionCreate,
Target: "test-app",
Success: true,
},
},
// Failed action: new instance creation failed
FailedActions: []ActionResult{
{
Type: ActionCreate,
Target: "test-app-1.0.0-instance",
Success: false,
},
},
// Backup of deleted resources
DeletedAppBackup: &AppBackup{
App: oldApp,
Region: "US",
ManifestContent: "old-manifest-content",
},
DeletedInstancesBackup: []InstanceBackup{
{
Instance: oldInstance,
Region: "US",
},
},
}
// Mock rollback operations in order:
// 1. Delete newly created app (rollback create)
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
Return(nil).Once()
// 2. Restore old app (from backup)
mockClient.On("CreateApp", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInput) bool {
return input.App.Key.Name == "test-app" && input.App.DeploymentManifest == "old-manifest-content"
})).Return(nil).Once()
// 3. Restore old instance (from backup)
mockClient.On("CreateAppInstance", mock.Anything, mock.MatchedBy(func(input *v2.NewAppInstanceInput) bool {
return input.AppInst.Key.Name == "test-app-1.0.0-instance"
})).Return(nil).Once()
ctx := context.Background()
err := manager.RollbackDeployment(ctx, result)
require.NoError(t, err)
mockClient.AssertExpectations(t)
// Verify rollback was logged
assert.Greater(t, len(logger.messages), 0)
// Should have messages about rolling back created resources and restoring deleted resources
hasRestoreLog := false
for _, msg := range logger.messages {
if strings.Contains(msg, "Restoring deleted resources") {
hasRestoreLog = true
break
}
}
assert.True(t, hasRestoreLog, "Should log restoration of deleted resources")
}
func TestConvertNetworkRules(t *testing.T) {
network := &config.NetworkConfig{
OutboundConnections: []config.OutboundConnection{

View file

@ -11,8 +11,8 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// EdgeConnectClientInterface defines the methods needed for deployment planning
@ -21,7 +21,7 @@ type EdgeConnectClientInterface interface {
CreateApp(ctx context.Context, input *v2.NewAppInput) error
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error)
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error)
CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
@ -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,12 +323,7 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
// Extract outbound connections from the app
current.OutboundConnections = make([]SecurityRule, len(app.RequiredOutboundConnections))
for i, conn := range app.RequiredOutboundConnections {
current.OutboundConnections[i] = SecurityRule{
Protocol: conn.Protocol,
PortRangeMin: conn.PortRangeMin,
PortRangeMax: conn.PortRangeMax,
RemoteCIDR: conn.RemoteCIDR,
}
current.OutboundConnections[i] = SecurityRule(conn)
}
return current, nil
@ -348,7 +343,9 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
},
}
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, desired.Region)
appKey := v2.AppKey{Name: desired.AppName}
instance, err := p.client.ShowAppInstance(timeoutCtx, instanceKey, appKey, desired.Region)
if err != nil {
return nil, err
}
@ -392,7 +389,7 @@ func (p *EdgeConnectPlanner) compareAppStates(current, desired *AppState) ([]str
// Compare outbound connections
outboundChanges := p.compareOutboundConnections(current.OutboundConnections, desired.OutboundConnections)
if len(outboundChanges) > 0 {
sb:= strings.Builder{}
sb := strings.Builder{}
sb.WriteString("Outbound connections changed:\n")
for _, change := range outboundChanges {
sb.WriteString(change)
@ -470,7 +467,9 @@ func (p *EdgeConnectPlanner) calculateManifestHash(manifestPath string) (string,
if err != nil {
return "", fmt.Errorf("failed to open manifest file: %w", err)
}
defer file.Close()
defer func() {
_ = file.Close()
}()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
@ -505,18 +504,20 @@ func (p *EdgeConnectPlanner) estimateDeploymentDuration(plan *DeploymentPlan) ti
var duration time.Duration
// App operations
if plan.AppAction.Type == ActionCreate {
switch plan.AppAction.Type {
case ActionCreate:
duration += 30 * time.Second
} else if plan.AppAction.Type == ActionUpdate {
case ActionUpdate:
duration += 15 * time.Second
}
// Instance operations (can be done in parallel)
instanceDuration := time.Duration(0)
for _, action := range plan.InstanceActions {
if action.Type == ActionCreate {
switch action.Type {
case ActionCreate:
instanceDuration = max(instanceDuration, 2*time.Minute)
} else if action.Type == ActionUpdate {
case ActionUpdate:
instanceDuration = max(instanceDuration, 1*time.Minute)
}
}

View file

@ -9,8 +9,8 @@ import (
"testing"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -29,7 +29,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
return args.Get(0).(v2.App), args.Error(1)
}
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error) {
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) (v2.AppInstance, error) {
args := m.Called(ctx, instanceKey, region)
if args.Get(0) == nil {
return v2.AppInstance{}, args.Error(1)

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
)
// DeploymentStrategy represents the type of deployment strategy

View file

@ -10,8 +10,8 @@ import (
"sync"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// RecreateStrategy implements the recreate deployment strategy
@ -159,6 +159,19 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo
return nil
}
// Backup instances before deleting them (for rollback restoration)
r.logf("Backing up %d existing instances before deletion", len(instancesToDelete))
for _, action := range instancesToDelete {
backup, err := r.backupInstance(ctx, action, config)
if err != nil {
r.logf("Warning: failed to backup instance %s before deletion: %v", action.InstanceName, err)
// Continue with deletion even if backup fails - this is best effort
} else {
result.DeletedInstancesBackup = append(result.DeletedInstancesBackup, *backup)
r.logf("Backed up instance: %s", action.InstanceName)
}
}
deleteResults := r.executeInstanceActionsWithRetry(ctx, instancesToDelete, "delete", config)
for _, deleteResult := range deleteResults {
@ -172,6 +185,19 @@ func (r *RecreateStrategy) deleteInstancesPhase(ctx context.Context, plan *Deplo
}
r.logf("Phase 1 complete: deleted %d instances", len(deleteResults))
// Wait for Kubernetes namespace termination to complete
// This prevents "namespace is being terminated" errors when recreating instances
if len(deleteResults) > 0 {
waitTime := 5 * time.Second
r.logf("Waiting %v for namespace termination to complete...", waitTime)
select {
case <-time.After(waitTime):
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
@ -184,6 +210,17 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP
r.logf("Phase 2: Deleting existing application")
// Backup app before deleting it (for rollback restoration)
r.logf("Backing up existing app before deletion")
backup, err := r.backupApp(ctx, plan, config)
if err != nil {
r.logf("Warning: failed to backup app before deletion: %v", err)
// Continue with deletion even if backup fails - this is best effort
} else {
result.DeletedAppBackup = backup
r.logf("Backed up app: %s", plan.AppAction.Desired.Name)
}
appKey := v2.AppKey{
Organization: plan.AppAction.Desired.Organization,
Name: plan.AppAction.Desired.Name,
@ -516,6 +553,54 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi
return true, nil
}
// backupApp fetches and stores the current app state before deletion
func (r *RecreateStrategy) backupApp(ctx context.Context, plan *DeploymentPlan, config *config.EdgeConnectConfig) (*AppBackup, error) {
appKey := v2.AppKey{
Organization: plan.AppAction.Desired.Organization,
Name: plan.AppAction.Desired.Name,
Version: plan.AppAction.Desired.Version,
}
app, err := r.client.ShowApp(ctx, appKey, plan.AppAction.Desired.Region)
if err != nil {
return nil, fmt.Errorf("failed to fetch app for backup: %w", err)
}
backup := &AppBackup{
App: app,
Region: plan.AppAction.Desired.Region,
ManifestContent: app.DeploymentManifest,
}
return backup, nil
}
// backupInstance fetches and stores the current instance state before deletion
func (r *RecreateStrategy) backupInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (*InstanceBackup, error) {
instanceKey := v2.AppInstanceKey{
Organization: action.Desired.Organization,
Name: action.InstanceName,
CloudletKey: v2.CloudletKey{
Organization: action.Target.CloudletOrg,
Name: action.Target.CloudletName,
},
}
appKey := v2.AppKey{Name: action.Desired.AppName}
instance, err := r.client.ShowAppInstance(ctx, instanceKey, appKey, action.Target.Region)
if err != nil {
return nil, fmt.Errorf("failed to fetch instance for backup: %w", err)
}
backup := &InstanceBackup{
Instance: instance,
Region: action.Target.Region,
}
return backup, nil
}
// logf logs a message if a logger is configured
func (r *RecreateStrategy) logf(format string, v ...interface{}) {
if r.logger != nil {
@ -530,6 +615,14 @@ func isRetryableError(err error) bool {
return false
}
errStr := strings.ToLower(err.Error())
// Special case: Kubernetes namespace termination race condition
// This is a transient 400 error that should be retried
if strings.Contains(errStr, "being terminated") || strings.Contains(errStr, "is being terminated") {
return true
}
// Check if it's an APIError with a status code
var apiErr *v2.APIError
if errors.As(err, &apiErr) {

View file

@ -7,8 +7,8 @@ import (
"strings"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// SecurityRule defines network access rules (alias to SDK type for consistency)
@ -271,6 +271,12 @@ type ExecutionResult struct {
// RollbackSuccess indicates if rollback was successful
RollbackSuccess bool
// DeletedAppBackup stores the app that was deleted (for rollback restoration)
DeletedAppBackup *AppBackup
// DeletedInstancesBackup stores instances that were deleted (for rollback restoration)
DeletedInstancesBackup []InstanceBackup
}
// ActionResult represents the result of executing a single action
@ -294,6 +300,27 @@ type ActionResult struct {
Details string
}
// AppBackup stores a deleted app's complete state for rollback restoration
type AppBackup struct {
// App is the full app object that was deleted
App v2.App
// Region where the app was deployed
Region string
// ManifestContent is the deployment manifest content
ManifestContent string
}
// InstanceBackup stores a deleted instance's complete state for rollback restoration
type InstanceBackup struct {
// Instance is the full instance object that was deleted
Instance v2.AppInstance
// Region where the instance was deployed
Region string
}
// IsEmpty returns true if the deployment plan has no actions to perform
func (dp *DeploymentPlan) IsEmpty() bool {
if dp.AppAction.Type != ActionNone {

View file

@ -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{
{

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// ResourceManagerInterface defines the interface for resource management

View file

@ -7,14 +7,14 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect"
)
// EdgeConnectClientInterface defines the methods needed for deletion planning
type EdgeConnectClientInterface interface {
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error)
ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, appKey edgeconnect.AppKey, region string) ([]edgeconnect.AppInstance, error)
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
}
@ -154,8 +154,9 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *
Name: infra.CloudletName,
},
}
appKey := edgeconnect.AppKey{Name: config.Metadata.Name}
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region)
if err != nil {
// If it's a not found error, just continue
if isNotFoundError(err) {

View file

@ -7,7 +7,7 @@ import (
"fmt"
"time"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// ResourceManagerInterface defines the interface for resource management

View file

@ -8,7 +8,7 @@ import (
"testing"
"time"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -27,7 +27,7 @@ func (m *MockResourceClient) ShowApp(ctx context.Context, appKey v2.AppKey, regi
return args.Get(0).(v2.App), args.Error(1)
}
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
func (m *MockResourceClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) {
args := m.Called(ctx, instanceKey, region)
if args.Get(0) == nil {
return nil, args.Error(1)

View file

@ -7,14 +7,14 @@ import (
"fmt"
"time"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// EdgeConnectClientInterface defines the methods needed for deletion planning
type EdgeConnectClientInterface interface {
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error)
ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error)
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
}
@ -154,8 +154,9 @@ func (p *EdgeConnectPlanner) findInstancesToDelete(ctx context.Context, config *
Name: infra.CloudletName,
},
}
appKey := v2.AppKey{Name: config.Metadata.Name}
instances, err := p.client.ShowAppInstances(ctx, instanceKey, infra.Region)
instances, err := p.client.ShowAppInstances(ctx, instanceKey, appKey, infra.Region)
if err != nil {
// If it's a not found error, just continue
if isNotFoundError(err) {

View file

@ -8,8 +8,8 @@ import (
"path/filepath"
"testing"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
"edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/internal/config"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -28,7 +28,7 @@ func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, r
return args.Get(0).(v2.App), args.Error(1)
}
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string) ([]v2.AppInstance, error) {
args := m.Called(ctx, instanceKey, region)
if args.Get(0) == nil {
return nil, args.Error(1)

View file

@ -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,

View file

@ -1,6 +1,6 @@
package main
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/cmd"
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/cmd"
func main() {
cmd.Execute()

View file

@ -16,7 +16,7 @@ A comprehensive Go SDK for the EdgeXR Master Controller API, providing typed int
### Installation
```go
import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
```
### Authentication

View file

@ -9,7 +9,7 @@ import (
"fmt"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
)
// CreateAppInstance creates a new application instance in the specified region
@ -23,7 +23,9 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
if err != nil {
return fmt.Errorf("CreateAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateAppInstance")
@ -43,12 +45,12 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
// ShowAppInstance retrieves a single application instance by key and region
// Maps to POST /auth/ctrl/ShowAppInst
func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) (AppInstance, error) {
func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
filter := AppInstanceFilter{
AppInstance: AppInstance{Key: appInstKey},
AppInstance: AppInstance{AppKey: appKey, Key: appInstKey},
Region: region,
}
@ -56,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
if err != nil {
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
@ -83,12 +87,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
// ShowAppInstances retrieves all application instances matching the filter criteria
// Maps to POST /auth/ctrl/ShowAppInst
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) {
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
filter := AppInstanceFilter{
AppInstance: AppInstance{Key: appInstKey},
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
Region: region,
}
@ -96,7 +100,9 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
if err != nil {
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
@ -125,7 +131,9 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
if err != nil {
return fmt.Errorf("UpdateAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "UpdateAppInstance")
@ -152,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
if err != nil {
return fmt.Errorf("RefreshAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "RefreshAppInstance")
@ -179,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
if err != nil {
return fmt.Errorf("DeleteAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -201,6 +213,10 @@ func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result i
var errorMessage string
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
// On permission denied, Edge API returns just an empty array []!
if len(line) == 0 || line[0] == '[' {
return fmt.Errorf("%w", ErrFaultyResponsePerhaps403)
}
// Try parsing as ResultResponse first (error format)
var resultResp ResultResponse
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {

View file

@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -156,6 +156,7 @@ func TestCreateAppInstance(t *testing.T) {
func TestShowAppInstance(t *testing.T) {
tests := []struct {
name string
appKey AppKey
appInstKey AppInstanceKey
region string
mockStatusCode int
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
Name: "testcloudlet",
},
},
appKey: AppKey{Name: "test-app-id"},
region: "us-west",
mockStatusCode: 200,
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
@ -190,6 +192,7 @@ func TestShowAppInstance(t *testing.T) {
Name: "testcloudlet",
},
},
appKey: AppKey{Name: "test-app-id"},
region: "us-west",
mockStatusCode: 404,
mockResponse: "",
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
w.WriteHeader(tt.mockStatusCode)
if tt.mockResponse != "" {
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}
}))
defer server.Close()
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
// Execute test
ctx := context.Background()
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region)
// Verify results
if tt.expectError {
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
`
w.WriteHeader(200)
w.Write([]byte(response))
_, _ = w.Write([]byte(response))
}))
defer server.Close()
client := NewClient(server.URL)
ctx := context.Background()
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west")
require.NoError(t, err)
assert.Len(t, appInstances, 2)
@ -361,7 +364,7 @@ func TestUpdateAppInstance(t *testing.T) {
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()

View file

@ -10,12 +10,13 @@ import (
"io"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
)
var (
// ErrResourceNotFound indicates the requested resource was not found
ErrResourceNotFound = fmt.Errorf("resource not found")
ErrResourceNotFound = fmt.Errorf("resource not found")
ErrFaultyResponsePerhaps403 = fmt.Errorf("faulty response from API, may indicate permission denied")
)
// CreateApp creates a new application in the specified region
@ -28,7 +29,9 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
if err != nil {
return fmt.Errorf("CreateApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateApp")
@ -55,7 +58,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
if err != nil {
return App{}, fmt.Errorf("ShowApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
@ -95,7 +100,9 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
if err != nil {
return nil, fmt.Errorf("ShowApps failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowApps")
@ -124,7 +131,9 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
if err != nil {
return fmt.Errorf("UpdateApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "UpdateApp")
@ -151,7 +160,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
if err != nil {
return fmt.Errorf("DeleteApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -169,6 +180,10 @@ func (c *Client) parseStreamingResponse(resp *http.Response, result interface{})
var responses []Response[App]
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
// On permission denied, Edge API returns just an empty array []!
if len(line) == 0 || line[0] == '[' {
return fmt.Errorf("%w", ErrFaultyResponsePerhaps403)
}
var response Response[App]
if err := json.Unmarshal(line, &response); err != nil {
return err
@ -238,7 +253,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
bodyBytes := []byte{}
if resp.Body != nil {
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
bodyBytes, _ = io.ReadAll(resp.Body)
messages = append(messages, string(bodyBytes))
}

View file

@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
w.WriteHeader(tt.mockStatusCode)
if tt.mockResponse != "" {
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}
}))
defer server.Close()
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
`
w.WriteHeader(200)
w.Write([]byte(response))
_, _ = w.Write([]byte(response))
}))
defer server.Close()
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
assert.Equal(t, 400, err.StatusCode)
assert.Len(t, err.Messages, 2)
}
// Helper function to create a test server that handles streaming JSON responses
func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
for _, response := range responses {
w.Write([]byte(response + "\n"))
}
}))
}

View file

@ -138,7 +138,9 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e
if err != nil {
return "", err
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// Read response body - same as existing implementation
body, err := io.ReadAll(resp.Body)

View file

@ -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()

View file

@ -9,7 +9,7 @@ import (
"fmt"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
)
// CreateCloudlet creates a new cloudlet in the specified region
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
if err != nil {
return fmt.Errorf("CreateCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateCloudlet")
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
if err != nil {
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
@ -89,7 +93,9 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
if err != nil {
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
if err != nil {
return fmt.Errorf("DeleteCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
if err != nil {
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
@ -189,7 +199,9 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud
if err != nil {
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",

View file

@ -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()

View file

@ -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

View file

@ -10,8 +10,7 @@ import (
"fmt"
"io"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
"strings"
)
// CreateAppInstance creates a new application instance in the specified region
@ -25,15 +24,16 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
if err != nil {
return fmt.Errorf("CreateAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateAppInstance")
}
// Parse streaming JSON response
var appInstances []AppInstance
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
if _, err = parseStreamingResponse[AppInstance](resp); err != nil {
return fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
}
@ -45,7 +45,7 @@ func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInp
// ShowAppInstance retrieves a single application instance by key and region
// Maps to POST /auth/ctrl/ShowAppInst
func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) (AppInstance, error) {
func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) (AppInstance, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
@ -58,7 +58,9 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
if err != nil {
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
@ -71,7 +73,7 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
// Parse streaming JSON response
var appInstances []AppInstance
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil {
return AppInstance{}, fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
}
@ -85,12 +87,12 @@ func (c *Client) ShowAppInstance(ctx context.Context, appInstKey AppInstanceKey,
// ShowAppInstances retrieves all application instances matching the filter criteria
// Maps to POST /auth/ctrl/ShowAppInst
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) {
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, appKey AppKey, region string) ([]AppInstance, error) {
transport := c.getTransport()
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
filter := AppInstanceFilter{
AppInstance: AppInstance{Key: appInstKey},
AppInstance: AppInstance{Key: appInstKey, AppKey: appKey},
Region: region,
}
@ -98,18 +100,20 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey
if err != nil {
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
}
var appInstances []AppInstance
if resp.StatusCode == http.StatusNotFound {
return appInstances, nil // Return empty slice for not found
return []AppInstance{}, nil // Return empty slice for not found
}
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
var appInstances []AppInstance
if appInstances, err = parseStreamingResponse[AppInstance](resp); err != nil {
return nil, fmt.Errorf("ShowAppInstances failed to parse response: %w", err)
}
@ -127,7 +131,9 @@ func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstance
if err != nil {
return fmt.Errorf("UpdateAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "UpdateAppInstance")
@ -154,7 +160,9 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
if err != nil {
return fmt.Errorf("RefreshAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "RefreshAppInstance")
@ -181,7 +189,9 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
if err != nil {
return fmt.Errorf("DeleteAppInstance failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -195,88 +205,89 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
}
// parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances
func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result interface{}) error {
func parseStreamingResponse[T Message](resp *http.Response) ([]T, error) {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
return []T{}, fmt.Errorf("failed to read response body: %w", err)
}
// Try parsing as a direct JSON array first (v2 API format)
switch v := result.(type) {
case *[]AppInstance:
var appInstances []AppInstance
if err := json.Unmarshal(bodyBytes, &appInstances); err == nil {
*v = appInstances
return nil
}
// todo finish check the responses, test them, and make a unify result, probably need
// to update the response parameter to the message type e.g. App or AppInst
isV2, err := isV2Response(bodyBytes)
if err != nil {
return []T{}, fmt.Errorf("failed to parse streaming response: %w", err)
}
// Fall back to streaming format (v1 API format)
var appInstances []AppInstance
var messages []string
var hasError bool
var errorCode int
var errorMessage string
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
// Try parsing as ResultResponse first (error format)
var resultResp ResultResponse
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
if resultResp.IsError() {
hasError = true
errorCode = resultResp.GetCode()
errorMessage = resultResp.GetMessage()
}
return nil
if isV2 {
resultV2, err := parseStreamingResponseV2[T](resp.StatusCode, bodyBytes)
if err != nil {
return []T{}, err
}
// Try parsing as Response[AppInstance]
var response Response[AppInstance]
if err := json.Unmarshal(line, &response); err != nil {
return err
}
if response.HasData() {
appInstances = append(appInstances, response.Data)
}
if response.IsMessage() {
msg := response.Data.GetMessage()
messages = append(messages, msg)
// Check for error indicators in messages
if msg == "CreateError" || msg == "UpdateError" || msg == "DeleteError" {
hasError = true
}
}
return nil
})
if parseErr != nil {
return parseErr
return resultV2, nil
}
// 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
resultV1, err := parseStreamingResponseV1[T](resp.StatusCode, bodyBytes)
if err != nil {
return nil, err
}
// Set result based on type
switch v := result.(type) {
case *[]AppInstance:
*v = appInstances
default:
return fmt.Errorf("unsupported result type: %T", result)
if !resultV1.IsSuccessful() {
return []T{}, resultV1.Error()
}
return nil
return resultV1.GetData(), nil
}
func parseStreamingResponseV1[T Message](statusCode int, bodyBytes []byte) (Responses[T], error) {
// Fall back to streaming format (v1 API format)
var responses Responses[T]
responses.StatusCode = statusCode
decoder := json.NewDecoder(bytes.NewReader(bodyBytes))
for {
var d Response[T]
if err := decoder.Decode(&d); err != nil {
if err.Error() == "EOF" {
break
}
return Responses[T]{}, fmt.Errorf("error in parsing json object into Message: %w", err)
}
if d.Result.Message != "" && d.Result.Code != 0 {
responses.StatusCode = d.Result.Code
}
if strings.Contains(d.Data.GetMessage(), "CreateError") {
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "CreateError"))
}
if strings.Contains(d.Data.GetMessage(), "UpdateError") {
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "UpdateError"))
}
if strings.Contains(d.Data.GetMessage(), "DeleteError") {
responses.Errors = append(responses.Errors, fmt.Errorf("server responded with: %s", "DeleteError"))
}
responses.Responses = append(responses.Responses, d)
}
return responses, nil
}
func isV2Response(bodyBytes []byte) (bool, error) {
if len(bodyBytes) == 0 {
return false, fmt.Errorf("malformatted response body")
}
return bodyBytes[0] == '[', nil
}
func parseStreamingResponseV2[T Message](statusCode int, bodyBytes []byte) ([]T, error) {
var result []T
if err := json.Unmarshal(bodyBytes, &result); err != nil {
return result, fmt.Errorf("failed to read response body: %w", err)
}
return result, nil
}

View file

@ -126,7 +126,7 @@ func TestCreateAppInstance(t *testing.T) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -157,6 +157,7 @@ func TestShowAppInstance(t *testing.T) {
tests := []struct {
name string
appInstKey AppInstanceKey
appKey AppKey
region string
mockStatusCode int
mockResponse string
@ -173,6 +174,7 @@ func TestShowAppInstance(t *testing.T) {
Name: "testcloudlet",
},
},
appKey: AppKey{Name: "testapp"},
region: "us-west",
mockStatusCode: 200,
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
@ -190,6 +192,7 @@ func TestShowAppInstance(t *testing.T) {
Name: "testcloudlet",
},
},
appKey: AppKey{Name: "testapp"},
region: "us-west",
mockStatusCode: 404,
mockResponse: "",
@ -207,7 +210,7 @@ func TestShowAppInstance(t *testing.T) {
w.WriteHeader(tt.mockStatusCode)
if tt.mockResponse != "" {
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}
}))
defer server.Close()
@ -219,7 +222,7 @@ func TestShowAppInstance(t *testing.T) {
// Execute test
ctx := context.Background()
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.appKey, tt.region)
// Verify results
if tt.expectError {
@ -254,14 +257,14 @@ func TestShowAppInstances(t *testing.T) {
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
`
w.WriteHeader(200)
w.Write([]byte(response))
_, _ = w.Write([]byte(response))
}))
defer server.Close()
client := NewClient(server.URL)
ctx := context.Background()
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, AppKey{}, "us-west")
require.NoError(t, err)
assert.Len(t, appInstances, 2)
@ -361,7 +364,7 @@ func TestUpdateAppInstance(t *testing.T) {
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()

View file

@ -4,14 +4,12 @@
package v2
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
)
var (
@ -29,7 +27,9 @@ func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
if err != nil {
return fmt.Errorf("CreateApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateApp")
@ -56,7 +56,9 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
if err != nil {
return App{}, fmt.Errorf("ShowApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
@ -69,7 +71,7 @@ func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App
// Parse streaming JSON response
var apps []App
if err := c.parseStreamingResponse(resp, &apps); err != nil {
if apps, err = parseStreamingResponse[App](resp); err != nil {
return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err)
}
@ -96,18 +98,20 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]
if err != nil {
return nil, fmt.Errorf("ShowApps failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowApps")
}
var apps []App
if resp.StatusCode == http.StatusNotFound {
return apps, nil // Return empty slice for not found
return []App{}, nil // Return empty slice for not found
}
if err := c.parseStreamingResponse(resp, &apps); err != nil {
var apps []App
if apps, err = parseStreamingResponse[App](resp); err != nil {
return nil, fmt.Errorf("ShowApps failed to parse response: %w", err)
}
@ -125,7 +129,9 @@ func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
if err != nil {
return fmt.Errorf("UpdateApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "UpdateApp")
@ -152,7 +158,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
if err != nil {
return fmt.Errorf("DeleteApp failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -165,70 +173,6 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
return nil
}
// parseStreamingResponse parses the EdgeXR streaming JSON response format
func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) error {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
// Try parsing as a direct JSON array first (v2 API format)
switch v := result.(type) {
case *[]App:
var apps []App
if err := json.Unmarshal(bodyBytes, &apps); err == nil {
*v = apps
return nil
}
}
// Fall back to streaming format (v1 API format)
var responses []Response[App]
var apps []App
var messages []string
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
var response Response[App]
if err := json.Unmarshal(line, &response); err != nil {
return err
}
responses = append(responses, response)
return nil
})
if parseErr != nil {
return parseErr
}
// Extract data from responses
for _, response := range responses {
if response.HasData() {
apps = append(apps, response.Data)
}
if response.IsMessage() {
messages = append(messages, response.Data.GetMessage())
}
}
// If we have error messages, return them
if len(messages) > 0 {
return &APIError{
StatusCode: resp.StatusCode,
Messages: messages,
}
}
// Set result based on type
switch v := result.(type) {
case *[]App:
*v = apps
default:
return fmt.Errorf("unsupported result type: %T", result)
}
return nil
}
// getTransport creates an HTTP transport with current client settings
func (c *Client) getTransport() *sdkhttp.Transport {
return sdkhttp.NewTransport(
@ -254,7 +198,9 @@ func (c *Client) handleErrorResponse(resp *http.Response, operation string) erro
bodyBytes := []byte{}
if resp.Body != nil {
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
bodyBytes, _ = io.ReadAll(resp.Body)
messages = append(messages, string(bodyBytes))
}

View file

@ -67,7 +67,7 @@ func TestCreateApp(t *testing.T) {
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -139,7 +139,7 @@ func TestShowApp(t *testing.T) {
w.WriteHeader(tt.mockStatusCode)
if tt.mockResponse != "" {
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}
}))
defer server.Close()
@ -186,7 +186,7 @@ func TestShowApps(t *testing.T) {
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
`
w.WriteHeader(200)
w.Write([]byte(response))
_, _ = w.Write([]byte(response))
}))
defer server.Close()
@ -277,7 +277,7 @@ func TestUpdateApp(t *testing.T) {
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
w.WriteHeader(tt.mockStatusCode)
w.Write([]byte(tt.mockResponse))
_, _ = w.Write([]byte(tt.mockResponse))
}))
defer server.Close()
@ -407,13 +407,3 @@ func TestAPIError(t *testing.T) {
assert.Equal(t, 400, err.StatusCode)
assert.Len(t, err.Messages, 2)
}
// Helper function to create a test server that handles streaming JSON responses
func createStreamingJSONServer(responses []string, statusCode int) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
for _, response := range responses {
w.Write([]byte(response + "\n"))
}
}))
}

View file

@ -138,7 +138,9 @@ func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, e
if err != nil {
return "", err
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// Read response body - same as existing implementation
body, err := io.ReadAll(resp.Body)

View file

@ -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()

View file

@ -9,7 +9,7 @@ import (
"fmt"
"net/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/internal/http"
)
// CreateCloudlet creates a new cloudlet in the specified region
@ -22,7 +22,9 @@ func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) er
if err != nil {
return fmt.Errorf("CreateCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return c.handleErrorResponse(resp, "CreateCloudlet")
@ -49,7 +51,9 @@ func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, regi
if err != nil {
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
@ -89,7 +93,9 @@ func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, reg
if err != nil {
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
@ -123,7 +129,9 @@ func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, re
if err != nil {
return fmt.Errorf("DeleteCloudlet failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// 404 is acceptable for delete operations (already deleted)
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
@ -151,7 +159,9 @@ func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKe
if err != nil {
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
@ -189,7 +199,9 @@ func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey Cloud
if err != nil {
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",

View file

@ -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()

View file

@ -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,7 +291,8 @@ type DeleteAppInstanceInput struct {
// Response wraps a single API response
type Response[T Message] struct {
Data T `json:"data"`
ResultResponse `json:",inline"`
Data T `json:"data"`
}
func (res *Response[T]) HasData() bool {
@ -326,6 +327,7 @@ func (r *ResultResponse) GetCode() int {
type Responses[T Message] struct {
Responses []Response[T] `json:"responses,omitempty"`
StatusCode int `json:"-"`
Errors []error `json:"-"`
}
func (r *Responses[T]) GetData() []T {
@ -344,12 +346,15 @@ func (r *Responses[T]) GetMessages() []string {
if v.IsMessage() {
messages = append(messages, v.Data.GetMessage())
}
if v.Result.Message != "" {
messages = append(messages, v.Result.Message)
}
}
return messages
}
func (r *Responses[T]) IsSuccessful() bool {
return r.StatusCode >= 200 && r.StatusCode < 400
return len(r.Errors) == 0 && (r.StatusCode >= 200 && r.StatusCode < 400)
}
func (r *Responses[T]) Error() error {
@ -410,3 +415,7 @@ type CloudletResourceUsage struct {
Region string `json:"region"`
Usage map[string]interface{} `json:"usage"`
}
type ErrorMessage struct {
Message string
}

View file

@ -18,6 +18,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: edgeconnect-coder-deployment
#namespace: gitea
spec:
replicas: 1
selector:

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
func main() {
@ -193,7 +193,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow
},
}
instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, config.Region, 5*time.Minute)
instanceDetails, err := waitForInstanceReady(ctx, c, instanceKey, v2.AppKey{}, config.Region, 5*time.Minute)
if err != nil {
return fmt.Errorf("failed to wait for instance ready: %w", err)
}
@ -207,7 +207,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config Workflow
// 6. List Application Instances
fmt.Println("\n6⃣ Listing application instances...")
instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, config.Region)
instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, v2.AppKey{}, config.Region)
if err != nil {
return fmt.Errorf("failed to list app instances: %w", err)
}
@ -306,7 +306,7 @@ func getEnvOrDefault(key, defaultValue string) string {
}
// waitForInstanceReady polls the instance status until it's no longer "Creating" or timeout
func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, region string, timeout time.Duration) (v2.AppInstance, error) {
func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, appKey v2.AppKey, region string, timeout time.Duration) (v2.AppInstance, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
@ -321,7 +321,7 @@ func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppI
return v2.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
case <-ticker.C:
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, appKey, region)
if err != nil {
// Log error but continue polling
fmt.Printf(" ⚠️ Error checking instance state: %v\n", err)

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
func main() {

View file

@ -0,0 +1,29 @@
# Is there a swagger file for the new EdgeConnect API?
# How does it differ from the EdgeXR API?
kind: edgeconnect-deployment
metadata:
name: "forgejo-runner-orca" # name could be used for appName
appVersion: "1"
organization: "edp2-orca"
spec:
# dockerApp: # Docker is OBSOLETE
# appVersion: "1.0.0"
# manifestFile: "./docker-compose.yaml"
# image: "https://registry-1.docker.io/library/nginx:latest"
k8sApp:
manifestFile: "./forgejo-runner-deployment.yaml"
infraTemplate:
- region: "US"
cloudletOrg: "TelekomOp"
cloudletName: "gardener-shepherd-test"
flavorName: "defualt"
network:
outboundConnections:
- protocol: "tcp"
portRangeMin: 80
portRangeMax: 80
remoteCIDR: "0.0.0.0/0"
- protocol: "tcp"
portRangeMin: 443
portRangeMax: 443
remoteCIDR: "0.0.0.0/0"

View file

@ -0,0 +1,29 @@
# Is there a swagger file for the new EdgeConnect API?
# How does it differ from the EdgeXR API?
kind: edgeconnect-deployment
metadata:
name: "edge-ubuntu-buildkit" # name could be used for appName
appVersion: "1.0.0"
organization: "edp2"
spec:
# dockerApp: # Docker is OBSOLETE
# appVersion: "1.0.0"
# manifestFile: "./docker-compose.yaml"
# image: "https://registry-1.docker.io/library/nginx:latest"
k8sApp:
manifestFile: "./k8s-deployment.yaml"
infraTemplate:
- region: "EU"
cloudletOrg: "TelekomOP"
cloudletName: "Munich"
flavorName: "EU.small"
network:
outboundConnections:
- protocol: "tcp"
portRangeMin: 80
portRangeMax: 80
remoteCIDR: "0.0.0.0/0"
- protocol: "tcp"
portRangeMin: 443
portRangeMax: 443
remoteCIDR: "0.0.0.0/0"

View file

@ -0,0 +1,29 @@
# Is there a swagger file for the new EdgeConnect API?
# How does it differ from the EdgeXR API?
kind: edgeconnect-deployment
metadata:
name: "edge-ubuntu-buildkit" # name could be used for appName
appVersion: "1"
organization: "edp2-orca"
spec:
# dockerApp: # Docker is OBSOLETE
# appVersion: "1.0.0"
# manifestFile: "./docker-compose.yaml"
# image: "https://registry-1.docker.io/library/nginx:latest"
k8sApp:
manifestFile: "./k8s-deployment.yaml"
infraTemplate:
- region: "US"
cloudletOrg: "TelekomOp"
cloudletName: "gardener-shepherd-test"
flavorName: "defualt"
network:
outboundConnections:
- protocol: "tcp"
portRangeMin: 80
portRangeMax: 80
remoteCIDR: "0.0.0.0/0"
- protocol: "tcp"
portRangeMin: 443
portRangeMax: 443
remoteCIDR: "0.0.0.0/0"

View file

@ -0,0 +1,57 @@
# Add remote buildx builder:
# docker buildx create --use --name sidecar tcp://127.0.0.1:1234
# Run build:
# docker buildx build .
apiVersion: v1
kind: Service
metadata:
name: ubuntu-runner
labels:
run: ubuntu-runner
spec:
type: LoadBalancer
ports:
- name: tcp80
protocol: TCP
port: 80
targetPort: 80
selector:
run: ubuntu-runner
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: ubuntu-runner
name: ubuntu-runner
spec:
replicas: 1
selector:
matchLabels:
app: ubuntu-runner
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ubuntu-runner
annotations:
container.apparmor.security.beta.kubernetes.io/buildkitd: unconfined
spec:
containers:
- name: ubuntu
image: edp.buildth.ing/devfw-cicd/catthehacker/ubuntu:act-22.04-amd64
command:
- sleep
- 7d
- args:
- --allow-insecure-entitlement=network.host
- --oci-worker-no-process-sandbox
- --addr
- tcp://127.0.0.1:1234
image: moby/buildkit:v0.25.1-rootless
imagePullPolicy: IfNotPresent
name: buildkitd

View file

@ -162,7 +162,9 @@ func (t *Transport) CallJSON(ctx context.Context, method, url string, body inter
if err != nil {
return resp, err
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close()
}()
// Read response body
respBody, err := io.ReadAll(resp.Body)