refactor(sdk): restructure to follow Go module versioning conventions
Reorganize SDK to support both v1 and v2 APIs following Go conventions: - sdk/edgeconnect/ now contains v1 SDK (from revision/v1 branch) - sdk/edgeconnect/v2/ contains v2 SDK with package v2 - Update all CLI and internal imports to use v2 path - Update SDK examples and documentation for v2 import path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1413836b68
commit
3486b2228d
24 changed files with 3328 additions and 278 deletions
26
cmd/app.go
26
cmd/app.go
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
@ -50,7 +50,7 @@ func validateBaseURL(baseURL string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSDKClient() *edgeconnect.Client {
|
func newSDKClient() *v2.Client {
|
||||||
baseURL := viper.GetString("base_url")
|
baseURL := viper.GetString("base_url")
|
||||||
username := viper.GetString("username")
|
username := viper.GetString("username")
|
||||||
password := viper.GetString("password")
|
password := viper.GetString("password")
|
||||||
|
|
@ -62,22 +62,22 @@ func newSDKClient() *edgeconnect.Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build options
|
// Build options
|
||||||
opts := []edgeconnect.Option{
|
opts := []v2.Option{
|
||||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add logger only if debug flag is set
|
// Add logger only if debug flag is set
|
||||||
if debug {
|
if debug {
|
||||||
logger := log.New(os.Stderr, "[DEBUG] ", log.LstdFlags)
|
logger := log.New(os.Stderr, "[DEBUG] ", log.LstdFlags)
|
||||||
opts = append(opts, edgeconnect.WithLogger(logger))
|
opts = append(opts, v2.WithLogger(logger))
|
||||||
}
|
}
|
||||||
|
|
||||||
if username != "" && password != "" {
|
if username != "" && password != "" {
|
||||||
return edgeconnect.NewClientWithCredentials(baseURL, username, password, opts...)
|
return v2.NewClientWithCredentials(baseURL, username, password, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to no auth for now - in production should require auth
|
// Fallback to no auth for now - in production should require auth
|
||||||
return edgeconnect.NewClient(baseURL, opts...)
|
return v2.NewClient(baseURL, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var appCmd = &cobra.Command{
|
var appCmd = &cobra.Command{
|
||||||
|
|
@ -91,10 +91,10 @@ var createAppCmd = &cobra.Command{
|
||||||
Short: "Create a new Edge Connect application",
|
Short: "Create a new Edge Connect application",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
input := &edgeconnect.NewAppInput{
|
input := &v2.NewAppInput{
|
||||||
Region: region,
|
Region: region,
|
||||||
App: edgeconnect.App{
|
App: v2.App{
|
||||||
Key: edgeconnect.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: appName,
|
Name: appName,
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
|
|
@ -116,7 +116,7 @@ var showAppCmd = &cobra.Command{
|
||||||
Short: "Show details of an Edge Connect application",
|
Short: "Show details of an Edge Connect application",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: appName,
|
Name: appName,
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
|
|
@ -136,7 +136,7 @@ var listAppsCmd = &cobra.Command{
|
||||||
Short: "List Edge Connect applications",
|
Short: "List Edge Connect applications",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: appName,
|
Name: appName,
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
|
|
@ -159,7 +159,7 @@ var deleteAppCmd = &cobra.Command{
|
||||||
Short: "Delete an Edge Connect application",
|
Short: "Delete an Edge Connect application",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: appName,
|
Name: appName,
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,23 +27,23 @@ var createInstanceCmd = &cobra.Command{
|
||||||
Short: "Create a new Edge Connect application instance",
|
Short: "Create a new Edge Connect application instance",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
input := &edgeconnect.NewAppInstanceInput{
|
input := &v2.NewAppInstanceInput{
|
||||||
Region: region,
|
Region: region,
|
||||||
AppInst: edgeconnect.AppInstance{
|
AppInst: v2.AppInstance{
|
||||||
Key: edgeconnect.AppInstanceKey{
|
Key: v2.AppInstanceKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: instanceName,
|
Name: instanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: cloudletOrg,
|
Organization: cloudletOrg,
|
||||||
Name: cloudletName,
|
Name: cloudletName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppKey: edgeconnect.AppKey{
|
AppKey: v2.AppKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: appName,
|
Name: appName,
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
},
|
},
|
||||||
Flavor: edgeconnect.Flavor{
|
Flavor: v2.Flavor{
|
||||||
Name: flavorName,
|
Name: flavorName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -63,10 +63,10 @@ var showInstanceCmd = &cobra.Command{
|
||||||
Short: "Show details of an Edge Connect application instance",
|
Short: "Show details of an Edge Connect application instance",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: instanceName,
|
Name: instanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: cloudletOrg,
|
Organization: cloudletOrg,
|
||||||
Name: cloudletName,
|
Name: cloudletName,
|
||||||
},
|
},
|
||||||
|
|
@ -86,10 +86,10 @@ var listInstancesCmd = &cobra.Command{
|
||||||
Short: "List Edge Connect application instances",
|
Short: "List Edge Connect application instances",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: instanceName,
|
Name: instanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: cloudletOrg,
|
Organization: cloudletOrg,
|
||||||
Name: cloudletName,
|
Name: cloudletName,
|
||||||
},
|
},
|
||||||
|
|
@ -112,10 +112,10 @@ var deleteInstanceCmd = &cobra.Command{
|
||||||
Short: "Delete an Edge Connect application instance",
|
Short: "Delete an Edge Connect application instance",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
c := newSDKClient()
|
c := newSDKClient()
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: organization,
|
Organization: organization,
|
||||||
Name: instanceName,
|
Name: instanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: cloudletOrg,
|
Organization: cloudletOrg,
|
||||||
Name: cloudletName,
|
Name: cloudletName,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceManagerInterface defines the interface for resource management
|
// ResourceManagerInterface defines the interface for resource management
|
||||||
|
|
@ -250,7 +250,7 @@ func (rm *EdgeConnectResourceManager) rollbackCreateAction(ctx context.Context,
|
||||||
|
|
||||||
// rollbackApp deletes an application that was created
|
// rollbackApp deletes an application that was created
|
||||||
func (rm *EdgeConnectResourceManager) rollbackApp(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
func (rm *EdgeConnectResourceManager) rollbackApp(ctx context.Context, action ActionResult, plan *DeploymentPlan) error {
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: plan.AppAction.Desired.Organization,
|
Organization: plan.AppAction.Desired.Organization,
|
||||||
Name: plan.AppAction.Desired.Name,
|
Name: plan.AppAction.Desired.Name,
|
||||||
Version: plan.AppAction.Desired.Version,
|
Version: plan.AppAction.Desired.Version,
|
||||||
|
|
@ -264,10 +264,10 @@ func (rm *EdgeConnectResourceManager) rollbackInstance(ctx context.Context, acti
|
||||||
// Find the instance action to get the details
|
// Find the instance action to get the details
|
||||||
for _, instanceAction := range plan.InstanceActions {
|
for _, instanceAction := range plan.InstanceActions {
|
||||||
if instanceAction.InstanceName == action.Target {
|
if instanceAction.InstanceName == action.Target {
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: plan.AppAction.Desired.Organization,
|
Organization: plan.AppAction.Desired.Organization,
|
||||||
Name: instanceAction.InstanceName,
|
Name: instanceAction.InstanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: instanceAction.Target.CloudletOrg,
|
Organization: instanceAction.Target.CloudletOrg,
|
||||||
Name: instanceAction.Target.CloudletName,
|
Name: instanceAction.Target.CloudletName,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
@ -22,32 +22,32 @@ type MockResourceClient struct {
|
||||||
MockEdgeConnectClient
|
MockEdgeConnectClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error {
|
func (m *MockResourceClient) CreateApp(ctx context.Context, input *v2.NewAppInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error {
|
func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error {
|
func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
||||||
args := m.Called(ctx, appKey, region)
|
args := m.Called(ctx, appKey, region)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error {
|
func (m *MockResourceClient) UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error {
|
func (m *MockResourceClient) UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error {
|
func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
||||||
args := m.Called(ctx, instanceKey, region)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
@ -185,9 +185,9 @@ func TestApplyDeploymentSuccess(t *testing.T) {
|
||||||
config := createTestManagerConfig(t)
|
config := createTestManagerConfig(t)
|
||||||
|
|
||||||
// Mock successful operations
|
// Mock successful operations
|
||||||
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")).
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
||||||
Return(nil)
|
Return(nil)
|
||||||
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")).
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
||||||
Return(nil)
|
Return(nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -216,8 +216,8 @@ func TestApplyDeploymentAppFailure(t *testing.T) {
|
||||||
config := createTestManagerConfig(t)
|
config := createTestManagerConfig(t)
|
||||||
|
|
||||||
// Mock app creation failure - deployment should stop here
|
// Mock app creation failure - deployment should stop here
|
||||||
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")).
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
||||||
Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
result, err := manager.ApplyDeployment(ctx, plan, config, "test manifest content")
|
||||||
|
|
@ -241,13 +241,13 @@ func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) {
|
||||||
config := createTestManagerConfig(t)
|
config := createTestManagerConfig(t)
|
||||||
|
|
||||||
// Mock successful app creation but failed instance creation
|
// Mock successful app creation but failed instance creation
|
||||||
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")).
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
||||||
Return(nil)
|
Return(nil)
|
||||||
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")).
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
||||||
Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}})
|
||||||
|
|
||||||
// Mock rollback operations
|
// Mock rollback operations
|
||||||
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(nil)
|
Return(nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -333,9 +333,9 @@ func TestApplyDeploymentMultipleInstances(t *testing.T) {
|
||||||
config := createTestManagerConfig(t)
|
config := createTestManagerConfig(t)
|
||||||
|
|
||||||
// Mock successful operations
|
// Mock successful operations
|
||||||
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")).
|
mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*v2.NewAppInput")).
|
||||||
Return(nil)
|
Return(nil)
|
||||||
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")).
|
mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*v2.NewAppInstanceInput")).
|
||||||
Return(nil)
|
Return(nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -421,9 +421,9 @@ func TestRollbackDeployment(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock rollback operations
|
// Mock rollback operations
|
||||||
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||||
Return(nil)
|
Return(nil)
|
||||||
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(nil)
|
Return(nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -453,8 +453,8 @@ func TestRollbackDeploymentFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock rollback failure
|
// Mock rollback failure
|
||||||
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Delete failed"}})
|
Return(&v2.APIError{StatusCode: 500, Messages: []string{"Delete failed"}})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := manager.RollbackDeployment(ctx, result)
|
err := manager.RollbackDeployment(ctx, result)
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,19 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
// EdgeConnectClientInterface defines the methods needed for deployment planning
|
||||||
type EdgeConnectClientInterface interface {
|
type EdgeConnectClientInterface interface {
|
||||||
ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error)
|
ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error)
|
||||||
CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error
|
CreateApp(ctx context.Context, input *v2.NewAppInput) error
|
||||||
UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error
|
UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error
|
||||||
DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error
|
DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error
|
||||||
ShowAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) (edgeconnect.AppInstance, error)
|
ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error)
|
||||||
CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error
|
CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error
|
||||||
UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error
|
UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error
|
||||||
DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error
|
DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Planner defines the interface for deployment planning
|
// Planner defines the interface for deployment planning
|
||||||
|
|
@ -285,7 +285,7 @@ func (p *EdgeConnectPlanner) getCurrentAppState(ctx context.Context, desired *Ap
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: desired.Organization,
|
Organization: desired.Organization,
|
||||||
Name: desired.Name,
|
Name: desired.Name,
|
||||||
Version: desired.Version,
|
Version: desired.Version,
|
||||||
|
|
@ -339,10 +339,10 @@ func (p *EdgeConnectPlanner) getCurrentInstanceState(ctx context.Context, desire
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: desired.Organization,
|
Organization: desired.Organization,
|
||||||
Name: desired.Name,
|
Name: desired.Name,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: desired.CloudletOrg,
|
Organization: desired.CloudletOrg,
|
||||||
Name: desired.CloudletName,
|
Name: desired.CloudletName,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
@ -21,66 +21,66 @@ type MockEdgeConnectClient struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey edgeconnect.AppKey, region string) (edgeconnect.App, error) {
|
func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error) {
|
||||||
args := m.Called(ctx, appKey, region)
|
args := m.Called(ctx, appKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return edgeconnect.App{}, args.Error(1)
|
return v2.App{}, args.Error(1)
|
||||||
}
|
}
|
||||||
return args.Get(0).(edgeconnect.App), args.Error(1)
|
return args.Get(0).(v2.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 v2.AppInstanceKey, region string) (v2.AppInstance, error) {
|
||||||
args := m.Called(ctx, instanceKey, region)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return edgeconnect.AppInstance{}, args.Error(1)
|
return v2.AppInstance{}, args.Error(1)
|
||||||
}
|
}
|
||||||
return args.Get(0).(edgeconnect.AppInstance), args.Error(1)
|
return args.Get(0).(v2.AppInstance), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error {
|
func (m *MockEdgeConnectClient) CreateApp(ctx context.Context, input *v2.NewAppInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error {
|
func (m *MockEdgeConnectClient) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error {
|
func (m *MockEdgeConnectClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
||||||
args := m.Called(ctx, appKey, region)
|
args := m.Called(ctx, appKey, region)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) UpdateApp(ctx context.Context, input *edgeconnect.UpdateAppInput) error {
|
func (m *MockEdgeConnectClient) UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) UpdateAppInstance(ctx context.Context, input *edgeconnect.UpdateAppInstanceInput) error {
|
func (m *MockEdgeConnectClient) UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error {
|
||||||
args := m.Called(ctx, input)
|
args := m.Called(ctx, input)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error {
|
func (m *MockEdgeConnectClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
||||||
args := m.Called(ctx, instanceKey, region)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey edgeconnect.AppKey, region string) ([]edgeconnect.App, error) {
|
func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey v2.AppKey, region string) ([]v2.App, error) {
|
||||||
args := m.Called(ctx, appKey, region)
|
args := m.Called(ctx, appKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
return args.Get(0).([]edgeconnect.App), args.Error(1)
|
return args.Get(0).([]v2.App), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) ([]edgeconnect.AppInstance, error) {
|
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
||||||
args := m.Called(ctx, instanceKey, region)
|
args := m.Called(ctx, instanceKey, region)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
return args.Get(0).([]edgeconnect.AppInstance), args.Error(1)
|
return args.Get(0).([]v2.AppInstance), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewPlanner(t *testing.T) {
|
func TestNewPlanner(t *testing.T) {
|
||||||
|
|
@ -148,11 +148,11 @@ func TestPlanNewDeployment(t *testing.T) {
|
||||||
testConfig := createTestConfig(t)
|
testConfig := createTestConfig(t)
|
||||||
|
|
||||||
// Mock API calls to return "not found" errors
|
// Mock API calls to return "not found" errors
|
||||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||||
|
|
||||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
result, err := planner.Plan(ctx, testConfig)
|
result, err := planner.Plan(ctx, testConfig)
|
||||||
|
|
@ -186,15 +186,15 @@ func TestPlanExistingDeploymentNoChanges(t *testing.T) {
|
||||||
|
|
||||||
// Mock existing app with same manifest hash and outbound connections
|
// Mock existing app with same manifest hash and outbound connections
|
||||||
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
||||||
existingApp := &edgeconnect.App{
|
existingApp := &v2.App{
|
||||||
Key: edgeconnect.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: "testorg",
|
Organization: "testorg",
|
||||||
Name: "test-app",
|
Name: "test-app",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
Deployment: "kubernetes",
|
Deployment: "kubernetes",
|
||||||
DeploymentManifest: manifestContent,
|
DeploymentManifest: manifestContent,
|
||||||
RequiredOutboundConnections: []edgeconnect.SecurityRule{
|
RequiredOutboundConnections: []v2.SecurityRule{
|
||||||
{
|
{
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
PortRangeMin: 80,
|
PortRangeMin: 80,
|
||||||
|
|
@ -206,31 +206,31 @@ func TestPlanExistingDeploymentNoChanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock existing instance
|
// Mock existing instance
|
||||||
existingInstance := &edgeconnect.AppInstance{
|
existingInstance := &v2.AppInstance{
|
||||||
Key: edgeconnect.AppInstanceKey{
|
Key: v2.AppInstanceKey{
|
||||||
Organization: "testorg",
|
Organization: "testorg",
|
||||||
Name: "test-app-1.0.0-instance",
|
Name: "test-app-1.0.0-instance",
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: "TestCloudletOrg",
|
Organization: "TestCloudletOrg",
|
||||||
Name: "TestCloudlet",
|
Name: "TestCloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppKey: edgeconnect.AppKey{
|
AppKey: v2.AppKey{
|
||||||
Organization: "testorg",
|
Organization: "testorg",
|
||||||
Name: "test-app",
|
Name: "test-app",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
},
|
},
|
||||||
Flavor: edgeconnect.Flavor{
|
Flavor: v2.Flavor{
|
||||||
Name: "small",
|
Name: "small",
|
||||||
},
|
},
|
||||||
State: "Ready",
|
State: "Ready",
|
||||||
PowerState: "PowerOn",
|
PowerState: "PowerOn",
|
||||||
}
|
}
|
||||||
|
|
||||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(*existingApp, nil)
|
Return(*existingApp, nil)
|
||||||
|
|
||||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||||
Return(*existingInstance, nil)
|
Return(*existingInstance, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -293,14 +293,14 @@ func TestPlanMultipleInfrastructures(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock API calls to return "not found" errors
|
// Mock API calls to return "not found" errors
|
||||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
||||||
|
|
||||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US").
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||||
|
|
||||||
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "EU").
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "EU").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
result, err := planner.Plan(ctx, testConfig)
|
result, err := planner.Plan(ctx, testConfig)
|
||||||
|
|
@ -628,10 +628,10 @@ func TestIsResourceNotFoundError(t *testing.T) {
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{"nil error", nil, false},
|
{"nil error", nil, false},
|
||||||
{"not found error", &edgeconnect.APIError{StatusCode: 404, Messages: []string{"Resource not found"}}, true},
|
{"not found error", &v2.APIError{StatusCode: 404, Messages: []string{"Resource not found"}}, true},
|
||||||
{"does not exist error", &edgeconnect.APIError{Messages: []string{"App does not exist"}}, true},
|
{"does not exist error", &v2.APIError{Messages: []string{"App does not exist"}}, true},
|
||||||
{"404 in message", &edgeconnect.APIError{Messages: []string{"HTTP 404 error"}}, true},
|
{"404 in message", &v2.APIError{Messages: []string{"HTTP 404 error"}}, true},
|
||||||
{"other error", &edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}}, false},
|
{"other error", &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}}, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -648,8 +648,8 @@ func TestPlanErrorHandling(t *testing.T) {
|
||||||
testConfig := createTestConfig(t)
|
testConfig := createTestConfig(t)
|
||||||
|
|
||||||
// Mock API call to return a non-404 error
|
// Mock API call to return a non-404 error
|
||||||
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US").
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
||||||
Return(nil, &edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
Return(nil, &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
result, err := planner.Plan(ctx, testConfig)
|
result, err := planner.Plan(ctx, testConfig)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecreateStrategy implements the recreate deployment strategy
|
// RecreateStrategy implements the recreate deployment strategy
|
||||||
|
|
@ -184,7 +184,7 @@ func (r *RecreateStrategy) deleteAppPhase(ctx context.Context, plan *DeploymentP
|
||||||
|
|
||||||
r.logf("Phase 2: Deleting existing application")
|
r.logf("Phase 2: Deleting existing application")
|
||||||
|
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: plan.AppAction.Desired.Organization,
|
Organization: plan.AppAction.Desired.Organization,
|
||||||
Name: plan.AppAction.Desired.Name,
|
Name: plan.AppAction.Desired.Name,
|
||||||
Version: plan.AppAction.Desired.Version,
|
Version: plan.AppAction.Desired.Version,
|
||||||
|
|
@ -426,10 +426,10 @@ func (r *RecreateStrategy) executeAppActionWithRetry(ctx context.Context, action
|
||||||
|
|
||||||
// deleteInstance deletes an instance (reuse existing logic from manager.go)
|
// deleteInstance deletes an instance (reuse existing logic from manager.go)
|
||||||
func (r *RecreateStrategy) deleteInstance(ctx context.Context, action InstanceAction) (bool, error) {
|
func (r *RecreateStrategy) deleteInstance(ctx context.Context, action InstanceAction) (bool, error) {
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: action.Desired.Organization,
|
Organization: action.Desired.Organization,
|
||||||
Name: action.InstanceName,
|
Name: action.InstanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: action.Target.CloudletOrg,
|
Organization: action.Target.CloudletOrg,
|
||||||
Name: action.Target.CloudletName,
|
Name: action.Target.CloudletName,
|
||||||
},
|
},
|
||||||
|
|
@ -445,23 +445,23 @@ func (r *RecreateStrategy) deleteInstance(ctx context.Context, action InstanceAc
|
||||||
|
|
||||||
// createInstance creates an instance (extracted from manager.go logic)
|
// createInstance creates an instance (extracted from manager.go logic)
|
||||||
func (r *RecreateStrategy) createInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (bool, error) {
|
func (r *RecreateStrategy) createInstance(ctx context.Context, action InstanceAction, config *config.EdgeConnectConfig) (bool, error) {
|
||||||
instanceInput := &edgeconnect.NewAppInstanceInput{
|
instanceInput := &v2.NewAppInstanceInput{
|
||||||
Region: action.Target.Region,
|
Region: action.Target.Region,
|
||||||
AppInst: edgeconnect.AppInstance{
|
AppInst: v2.AppInstance{
|
||||||
Key: edgeconnect.AppInstanceKey{
|
Key: v2.AppInstanceKey{
|
||||||
Organization: action.Desired.Organization,
|
Organization: action.Desired.Organization,
|
||||||
Name: action.InstanceName,
|
Name: action.InstanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: action.Target.CloudletOrg,
|
Organization: action.Target.CloudletOrg,
|
||||||
Name: action.Target.CloudletName,
|
Name: action.Target.CloudletName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppKey: edgeconnect.AppKey{
|
AppKey: v2.AppKey{
|
||||||
Organization: action.Desired.Organization,
|
Organization: action.Desired.Organization,
|
||||||
Name: config.Metadata.Name,
|
Name: config.Metadata.Name,
|
||||||
Version: config.Metadata.AppVersion,
|
Version: config.Metadata.AppVersion,
|
||||||
},
|
},
|
||||||
Flavor: edgeconnect.Flavor{
|
Flavor: v2.Flavor{
|
||||||
Name: action.Target.FlavorName,
|
Name: action.Target.FlavorName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -481,10 +481,10 @@ func (r *RecreateStrategy) createInstance(ctx context.Context, action InstanceAc
|
||||||
// updateApplication creates/recreates an application (always uses CreateApp since we delete first)
|
// updateApplication creates/recreates an application (always uses CreateApp since we delete first)
|
||||||
func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppAction, config *config.EdgeConnectConfig, manifestContent string) (bool, error) {
|
func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppAction, config *config.EdgeConnectConfig, manifestContent string) (bool, error) {
|
||||||
// Build the app create input - always create since recreate strategy deletes first
|
// Build the app create input - always create since recreate strategy deletes first
|
||||||
appInput := &edgeconnect.NewAppInput{
|
appInput := &v2.NewAppInput{
|
||||||
Region: action.Desired.Region,
|
Region: action.Desired.Region,
|
||||||
App: edgeconnect.App{
|
App: v2.App{
|
||||||
Key: edgeconnect.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: action.Desired.Organization,
|
Organization: action.Desired.Organization,
|
||||||
Name: action.Desired.Name,
|
Name: action.Desired.Name,
|
||||||
Version: action.Desired.Version,
|
Version: action.Desired.Version,
|
||||||
|
|
@ -493,7 +493,7 @@ func (r *RecreateStrategy) updateApplication(ctx context.Context, action AppActi
|
||||||
ImageType: "ImageTypeDocker",
|
ImageType: "ImageTypeDocker",
|
||||||
ImagePath: config.GetImagePath(),
|
ImagePath: config.GetImagePath(),
|
||||||
AllowServerless: true,
|
AllowServerless: true,
|
||||||
DefaultFlavor: edgeconnect.Flavor{Name: config.Spec.InfraTemplate[0].FlavorName},
|
DefaultFlavor: v2.Flavor{Name: config.Spec.InfraTemplate[0].FlavorName},
|
||||||
ServerlessConfig: struct{}{},
|
ServerlessConfig: struct{}{},
|
||||||
DeploymentManifest: manifestContent,
|
DeploymentManifest: manifestContent,
|
||||||
DeploymentGenerator: "kubernetes-basic",
|
DeploymentGenerator: "kubernetes-basic",
|
||||||
|
|
@ -531,7 +531,7 @@ func isRetryableError(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's an APIError with a status code
|
// Check if it's an APIError with a status code
|
||||||
var apiErr *edgeconnect.APIError
|
var apiErr *v2.APIError
|
||||||
if errors.As(err, &apiErr) {
|
if errors.As(err, &apiErr) {
|
||||||
// Don't retry client errors (4xx)
|
// Don't retry client errors (4xx)
|
||||||
if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 {
|
if apiErr.StatusCode >= 400 && apiErr.StatusCode < 500 {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecurityRule defines network access rules (alias to SDK type for consistency)
|
// SecurityRule defines network access rules (alias to SDK type for consistency)
|
||||||
type SecurityRule = edgeconnect.SecurityRule
|
type SecurityRule = v2.SecurityRule
|
||||||
|
|
||||||
// ActionType represents the type of action to be performed
|
// ActionType represents the type of action to be performed
|
||||||
type ActionType string
|
type ActionType string
|
||||||
|
|
@ -446,11 +446,11 @@ func (dp *DeploymentPlan) Clone() *DeploymentPlan {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertNetworkRules converts config network rules to EdgeConnect SecurityRules
|
// convertNetworkRules converts config network rules to EdgeConnect SecurityRules
|
||||||
func convertNetworkRules(network *config.NetworkConfig) []edgeconnect.SecurityRule {
|
func convertNetworkRules(network *config.NetworkConfig) []v2.SecurityRule {
|
||||||
rules := make([]edgeconnect.SecurityRule, len(network.OutboundConnections))
|
rules := make([]v2.SecurityRule, len(network.OutboundConnections))
|
||||||
|
|
||||||
for i, conn := range network.OutboundConnections {
|
for i, conn := range network.OutboundConnections {
|
||||||
rules[i] = edgeconnect.SecurityRule{
|
rules[i] = v2.SecurityRule{
|
||||||
Protocol: conn.Protocol,
|
Protocol: conn.Protocol,
|
||||||
PortRangeMin: conn.PortRangeMin,
|
PortRangeMin: conn.PortRangeMin,
|
||||||
PortRangeMax: conn.PortRangeMax,
|
PortRangeMax: conn.PortRangeMax,
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,18 @@ A comprehensive Go SDK for the EdgeXR Master Controller API, providing typed int
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
import v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Username/password (recommended)
|
// Username/password (recommended)
|
||||||
client := client.NewClientWithCredentials(baseURL, username, password)
|
client := v2.NewClientWithCredentials(baseURL, username, password)
|
||||||
|
|
||||||
// Static Bearer token
|
// Static Bearer token
|
||||||
client := client.NewClient(baseURL,
|
client := v2.NewClient(baseURL,
|
||||||
client.WithAuthProvider(client.NewStaticTokenProvider(token)))
|
v2.WithAuthProvider(v2.NewStaticTokenProvider(token)))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
@ -36,10 +36,10 @@ client := client.NewClient(baseURL,
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Create an application
|
// Create an application
|
||||||
app := &client.NewAppInput{
|
app := &v2.NewAppInput{
|
||||||
Region: "us-west",
|
Region: "us-west",
|
||||||
App: client.App{
|
App: v2.App{
|
||||||
Key: client.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: "myorg",
|
Organization: "myorg",
|
||||||
Name: "my-app",
|
Name: "my-app",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
|
|
@ -49,28 +49,28 @@ app := &client.NewAppInput{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.CreateApp(ctx, app); err != nil {
|
if err := v2.CreateApp(ctx, app); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy an application instance
|
// Deploy an application instance
|
||||||
instance := &client.NewAppInstanceInput{
|
instance := &v2.NewAppInstanceInput{
|
||||||
Region: "us-west",
|
Region: "us-west",
|
||||||
AppInst: client.AppInstance{
|
AppInst: v2.AppInstance{
|
||||||
Key: client.AppInstanceKey{
|
Key: v2.AppInstanceKey{
|
||||||
Organization: "myorg",
|
Organization: "myorg",
|
||||||
Name: "my-instance",
|
Name: "my-instance",
|
||||||
CloudletKey: client.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: "cloudlet-provider",
|
Organization: "cloudlet-provider",
|
||||||
Name: "edge-cloudlet",
|
Name: "edge-cloudlet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppKey: app.App.Key,
|
AppKey: app.App.Key,
|
||||||
Flavor: client.Flavor{Name: "m4.small"},
|
Flavor: v2.Flavor{Name: "m4.small"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.CreateAppInstance(ctx, instance); err != nil {
|
if err := v2.CreateAppInstance(ctx, instance); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -101,22 +101,22 @@ if err := client.CreateAppInstance(ctx, instance); err != nil {
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
```go
|
```go
|
||||||
client := client.NewClient(baseURL,
|
client := v2.NewClient(baseURL,
|
||||||
// Custom HTTP client with timeout
|
// Custom HTTP client with timeout
|
||||||
client.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
|
|
||||||
// Authentication provider
|
// Authentication provider
|
||||||
client.WithAuthProvider(client.NewStaticTokenProvider(token)),
|
v2.WithAuthProvider(v2.NewStaticTokenProvider(token)),
|
||||||
|
|
||||||
// Retry configuration
|
// Retry configuration
|
||||||
client.WithRetryOptions(client.RetryOptions{
|
v2.WithRetryOptions(v2.RetryOptions{
|
||||||
MaxRetries: 5,
|
MaxRetries: 5,
|
||||||
InitialDelay: 1 * time.Second,
|
InitialDelay: 1 * time.Second,
|
||||||
MaxDelay: 30 * time.Second,
|
MaxDelay: 30 * time.Second,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Request logging
|
// Request logging
|
||||||
client.WithLogger(log.Default()),
|
v2.WithLogger(log.Default()),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -141,7 +141,7 @@ EDGEXR_USERNAME=user EDGEXR_PASSWORD=pass go run main.go
|
||||||
Uses the existing `/api/v1/login` endpoint with automatic token caching:
|
Uses the existing `/api/v1/login` endpoint with automatic token caching:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
client := client.NewClientWithCredentials(baseURL, username, password)
|
client := v2.NewClientWithCredentials(baseURL, username, password)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
@ -154,23 +154,23 @@ client := client.NewClientWithCredentials(baseURL, username, password)
|
||||||
For pre-obtained tokens:
|
For pre-obtained tokens:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
client := client.NewClient(baseURL,
|
client := v2.NewClient(baseURL,
|
||||||
client.WithAuthProvider(client.NewStaticTokenProvider(token)))
|
v2.WithAuthProvider(v2.NewStaticTokenProvider(token)))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
```go
|
```go
|
||||||
app, err := client.ShowApp(ctx, appKey, region)
|
app, err := v2.ShowApp(ctx, appKey, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check for specific error types
|
// Check for specific error types
|
||||||
if errors.Is(err, client.ErrResourceNotFound) {
|
if errors.Is(err, v2.ErrResourceNotFound) {
|
||||||
fmt.Println("App not found")
|
fmt.Println("App not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for API errors
|
// Check for API errors
|
||||||
var apiErr *client.APIError
|
var apiErr *v2.APIError
|
||||||
if errors.As(err, &apiErr) {
|
if errors.As(err, &apiErr) {
|
||||||
fmt.Printf("API Error %d: %s\n", apiErr.StatusCode, apiErr.Messages[0])
|
fmt.Printf("API Error %d: %s\n", apiErr.StatusCode, apiErr.Messages[0])
|
||||||
return
|
return
|
||||||
|
|
@ -213,13 +213,13 @@ The SDK provides a drop-in replacement with enhanced features:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Old approach
|
// Old approach
|
||||||
oldClient := &client.EdgeConnect{
|
oldClient := &v2.EdgeConnect{
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
Credentials: client.Credentials{Username: user, Password: pass},
|
Credentials: v2.Credentials{Username: user, Password: pass},
|
||||||
}
|
}
|
||||||
|
|
||||||
// New SDK approach
|
// New SDK approach
|
||||||
newClient := client.NewClientWithCredentials(baseURL, user, pass)
|
newClient := v2.NewClientWithCredentials(baseURL, user, pass)
|
||||||
|
|
||||||
// Same method calls, enhanced reliability
|
// Same method calls, enhanced reliability
|
||||||
err := newClient.CreateApp(ctx, input)
|
err := newClient.CreateApp(ctx, input)
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,9 @@
|
||||||
package edgeconnect
|
package edgeconnect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||||
|
|
@ -166,17 +164,18 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAppInstance removes an application instance
|
// DeleteAppInstance removes an application instance from the specified region
|
||||||
// Maps to POST /auth/ctrl/DeleteAppInst
|
// Maps to POST /auth/ctrl/DeleteAppInst
|
||||||
func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
||||||
transport := c.getTransport()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst"
|
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst"
|
||||||
|
|
||||||
input := DeleteAppInstanceInput{
|
filter := AppInstanceFilter{
|
||||||
Key: appInstKey,
|
AppInstance: AppInstance{Key: appInstKey},
|
||||||
|
Region: region,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := transport.Call(ctx, "POST", url, input)
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -195,29 +194,13 @@ func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKe
|
||||||
|
|
||||||
// parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances
|
// parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances
|
||||||
func (c *Client) parseStreamingAppInstanceResponse(resp *http.Response, result interface{}) error {
|
func (c *Client) parseStreamingAppInstanceResponse(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 *[]AppInstance:
|
|
||||||
var appInstances []AppInstance
|
|
||||||
if err := json.Unmarshal(bodyBytes, &appInstances); err == nil {
|
|
||||||
*v = appInstances
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to streaming format (v1 API format)
|
|
||||||
var appInstances []AppInstance
|
var appInstances []AppInstance
|
||||||
var messages []string
|
var messages []string
|
||||||
var hasError bool
|
var hasError bool
|
||||||
var errorCode int
|
var errorCode int
|
||||||
var errorMessage string
|
var errorMessage string
|
||||||
|
|
||||||
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
|
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
||||||
// Try parsing as ResultResponse first (error format)
|
// Try parsing as ResultResponse first (error format)
|
||||||
var resultResp ResultResponse
|
var resultResp ResultResponse
|
||||||
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
|
if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
package edgeconnect
|
package edgeconnect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -143,12 +142,12 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
||||||
transport := c.getTransport()
|
transport := c.getTransport()
|
||||||
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
|
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
|
||||||
|
|
||||||
input := DeleteAppInput{
|
filter := AppFilter{
|
||||||
Key: appKey,
|
App: App{Key: appKey},
|
||||||
Region: region,
|
Region: region,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := transport.Call(ctx, "POST", url, input)
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DeleteApp failed: %w", err)
|
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -167,27 +166,9 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er
|
||||||
|
|
||||||
// parseStreamingResponse parses the EdgeXR streaming JSON response format
|
// parseStreamingResponse parses the EdgeXR streaming JSON response format
|
||||||
func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) error {
|
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 responses []Response[App]
|
||||||
var apps []App
|
|
||||||
var messages []string
|
|
||||||
|
|
||||||
parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), func(line []byte) error {
|
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
||||||
var response Response[App]
|
var response Response[App]
|
||||||
if err := json.Unmarshal(line, &response); err != nil {
|
if err := json.Unmarshal(line, &response); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -201,6 +182,9 @@ func (c *Client) parseStreamingResponse(resp *http.Response, result interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract data from responses
|
// Extract data from responses
|
||||||
|
var apps []App
|
||||||
|
var messages []string
|
||||||
|
|
||||||
for _, response := range responses {
|
for _, response := range responses {
|
||||||
if response.HasData() {
|
if response.HasData() {
|
||||||
apps = append(apps, response.Data)
|
apps = append(apps, response.Data)
|
||||||
|
|
|
||||||
|
|
@ -184,33 +184,24 @@ type App struct {
|
||||||
Deployment string `json:"deployment,omitempty"`
|
Deployment string `json:"deployment,omitempty"`
|
||||||
ImageType string `json:"image_type,omitempty"`
|
ImageType string `json:"image_type,omitempty"`
|
||||||
ImagePath string `json:"image_path,omitempty"`
|
ImagePath string `json:"image_path,omitempty"`
|
||||||
AccessPorts string `json:"access_ports,omitempty"`
|
|
||||||
AllowServerless bool `json:"allow_serverless,omitempty"`
|
AllowServerless bool `json:"allow_serverless,omitempty"`
|
||||||
DefaultFlavor Flavor `json:"defaultFlavor,omitempty"`
|
DefaultFlavor Flavor `json:"defaultFlavor,omitempty"`
|
||||||
ServerlessConfig interface{} `json:"serverless_config,omitempty"`
|
ServerlessConfig interface{} `json:"serverless_config,omitempty"`
|
||||||
DeploymentGenerator string `json:"deployment_generator,omitempty"`
|
DeploymentGenerator string `json:"deployment_generator,omitempty"`
|
||||||
DeploymentManifest string `json:"deployment_manifest,omitempty"`
|
DeploymentManifest string `json:"deployment_manifest,omitempty"`
|
||||||
RequiredOutboundConnections []SecurityRule `json:"required_outbound_connections"`
|
RequiredOutboundConnections []SecurityRule `json:"required_outbound_connections"`
|
||||||
GlobalID string `json:"global_id,omitempty"`
|
|
||||||
CreatedAt string `json:"created_at,omitempty"`
|
|
||||||
UpdatedAt string `json:"updated_at,omitempty"`
|
|
||||||
Fields []string `json:"fields,omitempty"`
|
Fields []string `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppInstance represents a deployed application instance
|
// AppInstance represents a deployed application instance
|
||||||
type AppInstance struct {
|
type AppInstance struct {
|
||||||
msg `json:",inline"`
|
msg `json:",inline"`
|
||||||
Key AppInstanceKey `json:"key"`
|
Key AppInstanceKey `json:"key"`
|
||||||
AppKey AppKey `json:"app_key,omitempty"`
|
AppKey AppKey `json:"app_key,omitempty"`
|
||||||
CloudletLoc CloudletLoc `json:"cloudlet_loc,omitempty"`
|
Flavor Flavor `json:"flavor,omitempty"`
|
||||||
Flavor Flavor `json:"flavor,omitempty"`
|
State string `json:"state,omitempty"`
|
||||||
State string `json:"state,omitempty"`
|
PowerState string `json:"power_state,omitempty"`
|
||||||
IngressURL string `json:"ingress_url,omitempty"`
|
Fields []string `json:"fields,omitempty"`
|
||||||
UniqueID string `json:"unique_id,omitempty"`
|
|
||||||
CreatedAt string `json:"created_at,omitempty"`
|
|
||||||
UpdatedAt string `json:"updated_at,omitempty"`
|
|
||||||
PowerState string `json:"power_state,omitempty"`
|
|
||||||
Fields []string `json:"fields,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloudlet represents edge infrastructure
|
// Cloudlet represents edge infrastructure
|
||||||
|
|
@ -233,12 +224,6 @@ type Location struct {
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudletLoc represents geographical coordinates for cloudlets
|
|
||||||
type CloudletLoc struct {
|
|
||||||
Latitude float64 `json:"latitude"`
|
|
||||||
Longitude float64 `json:"longitude"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input types for API operations
|
// Input types for API operations
|
||||||
|
|
||||||
// NewAppInput represents input for creating an application
|
// NewAppInput represents input for creating an application
|
||||||
|
|
@ -271,17 +256,6 @@ type UpdateAppInstanceInput struct {
|
||||||
AppInst AppInstance `json:"appinst"`
|
AppInst AppInstance `json:"appinst"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAppInput represents input for deleting an application
|
|
||||||
type DeleteAppInput struct {
|
|
||||||
Key AppKey `json:"key"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAppInstanceInput represents input for deleting an app instance
|
|
||||||
type DeleteAppInstanceInput struct {
|
|
||||||
Key AppInstanceKey `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response wrapper types
|
// Response wrapper types
|
||||||
|
|
||||||
// Response wraps a single API response
|
// Response wraps a single API response
|
||||||
|
|
|
||||||
281
sdk/edgeconnect/v2/appinstance.go
Normal file
281
sdk/edgeconnect/v2/appinstance.go
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
// ABOUTME: Application instance lifecycle management APIs for EdgeXR Master Controller
|
||||||
|
// ABOUTME: Provides typed methods for creating, querying, and deleting application instances
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAppInstance creates a new application instance in the specified region
|
||||||
|
// Maps to POST /auth/ctrl/CreateAppInst
|
||||||
|
func (c *Client) CreateAppInstance(ctx context.Context, input *NewAppInstanceInput) error {
|
||||||
|
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/CreateAppInst"
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CreateAppInstance failed: %w", err)
|
||||||
|
}
|
||||||
|
defer 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 {
|
||||||
|
return fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("CreateAppInstance: %s/%s created successfully",
|
||||||
|
input.AppInst.Key.Organization, input.AppInst.Key.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
|
filter := AppInstanceFilter{
|
||||||
|
AppInstance: AppInstance{Key: appInstKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return AppInstance{}, fmt.Errorf("ShowAppInstance failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return AppInstance{}, fmt.Errorf("app instance %s/%s: %w",
|
||||||
|
appInstKey.Organization, appInstKey.Name, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return AppInstance{}, c.handleErrorResponse(resp, "ShowAppInstance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse streaming JSON response
|
||||||
|
var appInstances []AppInstance
|
||||||
|
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||||
|
return AppInstance{}, fmt.Errorf("ShowAppInstance failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appInstances) == 0 {
|
||||||
|
return AppInstance{}, fmt.Errorf("app instance %s/%s in region %s: %w",
|
||||||
|
appInstKey.Organization, appInstKey.Name, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return appInstances[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowAppInstances retrieves all application instances matching the filter criteria
|
||||||
|
// Maps to POST /auth/ctrl/ShowAppInst
|
||||||
|
func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey, region string) ([]AppInstance, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowAppInst"
|
||||||
|
|
||||||
|
filter := AppInstanceFilter{
|
||||||
|
AppInstance: AppInstance{Key: appInstKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowAppInstances failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return nil, c.handleErrorResponse(resp, "ShowAppInstances")
|
||||||
|
}
|
||||||
|
|
||||||
|
var appInstances []AppInstance
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return appInstances, nil // Return empty slice for not found
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.parseStreamingAppInstanceResponse(resp, &appInstances); err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowAppInstances failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("ShowAppInstances: found %d app instances matching criteria", len(appInstances))
|
||||||
|
return appInstances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppInstance updates an application instance and then refreshes it
|
||||||
|
// Maps to POST /auth/ctrl/UpdateAppInst
|
||||||
|
func (c *Client) UpdateAppInstance(ctx context.Context, input *UpdateAppInstanceInput) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/UpdateAppInst"
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UpdateAppInstance failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return c.handleErrorResponse(resp, "UpdateAppInstance")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("UpdateAppInstance: %s/%s updated successfully",
|
||||||
|
input.AppInst.Key.Organization, input.AppInst.Key.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAppInstance refreshes an application instance's state
|
||||||
|
// Maps to POST /auth/ctrl/RefreshAppInst
|
||||||
|
func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/RefreshAppInst"
|
||||||
|
|
||||||
|
filter := AppInstanceFilter{
|
||||||
|
AppInstance: AppInstance{Key: appInstKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("RefreshAppInstance failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return c.handleErrorResponse(resp, "RefreshAppInstance")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("RefreshAppInstance: %s/%s refreshed successfully",
|
||||||
|
appInstKey.Organization, appInstKey.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAppInstance removes an application instance
|
||||||
|
// Maps to POST /auth/ctrl/DeleteAppInst
|
||||||
|
func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst"
|
||||||
|
|
||||||
|
input := DeleteAppInstanceInput{
|
||||||
|
Key: appInstKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DeleteAppInstance failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return c.handleErrorResponse(resp, "DeleteAppInstance")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("DeleteAppInstance: %s/%s deleted successfully",
|
||||||
|
appInstKey.Organization, appInstKey.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStreamingAppInstanceResponse parses the EdgeXR streaming JSON response format for app instances
|
||||||
|
func (c *Client) parseStreamingAppInstanceResponse(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 *[]AppInstance:
|
||||||
|
var appInstances []AppInstance
|
||||||
|
if err := json.Unmarshal(bodyBytes, &appInstances); err == nil {
|
||||||
|
*v = appInstances
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as Response[AppInstance]
|
||||||
|
var response Response[AppInstance]
|
||||||
|
if err := json.Unmarshal(line, &response); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.HasData() {
|
||||||
|
appInstances = append(appInstances, response.Data)
|
||||||
|
}
|
||||||
|
if response.IsMessage() {
|
||||||
|
msg := response.Data.GetMessage()
|
||||||
|
messages = append(messages, msg)
|
||||||
|
// Check for error indicators in messages
|
||||||
|
if msg == "CreateError" || msg == "UpdateError" || msg == "DeleteError" {
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if parseErr != nil {
|
||||||
|
return parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we detected an error, return it
|
||||||
|
if hasError {
|
||||||
|
apiErr := &APIError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Messages: messages,
|
||||||
|
}
|
||||||
|
if errorCode > 0 {
|
||||||
|
apiErr.StatusCode = errorCode
|
||||||
|
apiErr.Code = fmt.Sprintf("%d", errorCode)
|
||||||
|
}
|
||||||
|
if errorMessage != "" {
|
||||||
|
apiErr.Messages = append([]string{errorMessage}, apiErr.Messages...)
|
||||||
|
}
|
||||||
|
return apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set result based on type
|
||||||
|
switch v := result.(type) {
|
||||||
|
case *[]AppInstance:
|
||||||
|
*v = appInstances
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported result type: %T", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
524
sdk/edgeconnect/v2/appinstance_test.go
Normal file
524
sdk/edgeconnect/v2/appinstance_test.go
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
// ABOUTME: Unit tests for AppInstance management APIs using httptest mock server
|
||||||
|
// ABOUTME: Tests create, show, list, refresh, and delete operations with error conditions
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateAppInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *NewAppInstanceInput
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful creation",
|
||||||
|
input: &NewAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
Flavor: Flavor{Name: "m4.small"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"message": "success"}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
input: &NewAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "",
|
||||||
|
Name: "testinst",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 400,
|
||||||
|
mockResponse: `{"message": "organization is required"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP 200 with CreateError message",
|
||||||
|
input: &NewAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Flavor: Flavor{Name: "m4.small"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"data":{"message":"Creating"}}
|
||||||
|
{"data":{"message":"a service has been configured"}}
|
||||||
|
{"data":{"message":"CreateError"}}
|
||||||
|
{"data":{"message":"Deleting AppInst due to failure"}}
|
||||||
|
{"data":{"message":"Deleted AppInst successfully"}}`,
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "CreateError",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTP 200 with result error code",
|
||||||
|
input: &NewAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Flavor: Flavor{Name: "m4.small"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"data":{"message":"Creating"}}
|
||||||
|
{"data":{"message":"a service has been configured"}}
|
||||||
|
{"data":{"message":"CreateError"}}
|
||||||
|
{"data":{"message":"Deleting AppInst due to failure"}}
|
||||||
|
{"data":{"message":"Deleted AppInst successfully"}}
|
||||||
|
{"result":{"message":"Encountered failures: Create App Inst failed: deployments.apps is forbidden: User \"system:serviceaccount:edgexr:crm-telekomop-munich\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"gitea\"","code":400}}`,
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "deployments.apps is forbidden",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/CreateAppInst", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
WithAuthProvider(NewStaticTokenProvider("test-token")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.CreateAppInstance(ctx, tt.input)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.errorContains != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errorContains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowAppInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
appInstKey AppInstanceKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
expectNotFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful show",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testinst", "cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}}, "state": "Ready"}}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
expectNotFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instance not found",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: "",
|
||||||
|
expectError: true,
|
||||||
|
expectNotFound: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowAppInst", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
if tt.mockResponse != "" {
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
appInst, err := client.ShowAppInstance(ctx, tt.appInstKey, tt.region)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.expectNotFound {
|
||||||
|
assert.Contains(t, err.Error(), "resource not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.appInstKey.Organization, appInst.Key.Organization)
|
||||||
|
assert.Equal(t, tt.appInstKey.Name, appInst.Key.Name)
|
||||||
|
assert.Equal(t, "Ready", appInst.State)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowAppInstances(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowAppInst", r.URL.Path)
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var filter AppInstanceFilter
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&filter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "testorg", filter.AppInstance.Key.Organization)
|
||||||
|
assert.Equal(t, "us-west", filter.Region)
|
||||||
|
|
||||||
|
// Return multiple app instances
|
||||||
|
response := `{"data": {"key": {"organization": "testorg", "name": "inst1"}, "state": "Ready"}}
|
||||||
|
{"data": {"key": {"organization": "testorg", "name": "inst2"}, "state": "Creating"}}
|
||||||
|
`
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
appInstances, err := client.ShowAppInstances(ctx, AppInstanceKey{Organization: "testorg"}, "us-west")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, appInstances, 2)
|
||||||
|
assert.Equal(t, "inst1", appInstances[0].Key.Name)
|
||||||
|
assert.Equal(t, "Ready", appInstances[0].State)
|
||||||
|
assert.Equal(t, "inst2", appInstances[1].Key.Name)
|
||||||
|
assert.Equal(t, "Creating", appInstances[1].State)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateAppInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *UpdateAppInstanceInput
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful update",
|
||||||
|
input: &UpdateAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
Flavor: Flavor{Name: "m4.medium"},
|
||||||
|
PowerState: "PowerOn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"message": "success"}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
input: &UpdateAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 400,
|
||||||
|
mockResponse: `{"message": "organization is required"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instance not found",
|
||||||
|
input: &UpdateAppInstanceInput{
|
||||||
|
Region: "us-west",
|
||||||
|
AppInst: AppInstance{
|
||||||
|
Key: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: `{"message": "app instance not found"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/UpdateAppInst", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var input UpdateAppInstanceInput
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.input.Region, input.Region)
|
||||||
|
assert.Equal(t, tt.input.AppInst.Key.Organization, input.AppInst.Key.Organization)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
WithAuthProvider(NewStaticTokenProvider("test-token")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.UpdateAppInstance(ctx, tt.input)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshAppInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
appInstKey AppInstanceKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful refresh",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 500,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/RefreshAppInst", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := client.RefreshAppInstance(ctx, tt.appInstKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteAppInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
appInstKey AppInstanceKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful deletion",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already deleted (404 ok)",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error",
|
||||||
|
appInstKey: AppInstanceKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testinst",
|
||||||
|
CloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 500,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/DeleteAppInst", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := client.DeleteAppInstance(ctx, tt.appInstKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
267
sdk/edgeconnect/v2/apps.go
Normal file
267
sdk/edgeconnect/v2/apps.go
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// ABOUTME: Application lifecycle management APIs for EdgeXR Master Controller
|
||||||
|
// ABOUTME: Provides typed methods for creating, querying, and deleting applications
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrResourceNotFound indicates the requested resource was not found
|
||||||
|
ErrResourceNotFound = fmt.Errorf("resource not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateApp creates a new application in the specified region
|
||||||
|
// Maps to POST /auth/ctrl/CreateApp
|
||||||
|
func (c *Client) CreateApp(ctx context.Context, input *NewAppInput) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/CreateApp"
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CreateApp failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return c.handleErrorResponse(resp, "CreateApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("CreateApp: %s/%s version %s created successfully",
|
||||||
|
input.App.Key.Organization, input.App.Key.Name, input.App.Key.Version)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowApp retrieves a single application by key and region
|
||||||
|
// Maps to POST /auth/ctrl/ShowApp
|
||||||
|
func (c *Client) ShowApp(ctx context.Context, appKey AppKey, region string) (App, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
|
||||||
|
|
||||||
|
filter := AppFilter{
|
||||||
|
App: App{Key: appKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return App{}, fmt.Errorf("ShowApp failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||||
|
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return App{}, c.handleErrorResponse(resp, "ShowApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse streaming JSON response
|
||||||
|
var apps []App
|
||||||
|
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||||
|
return App{}, fmt.Errorf("ShowApp failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(apps) == 0 {
|
||||||
|
return App{}, fmt.Errorf("app %s/%s version %s in region %s: %w",
|
||||||
|
appKey.Organization, appKey.Name, appKey.Version, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowApps retrieves all applications matching the filter criteria
|
||||||
|
// Maps to POST /auth/ctrl/ShowApp
|
||||||
|
func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([]App, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowApp"
|
||||||
|
|
||||||
|
filter := AppFilter{
|
||||||
|
App: App{Key: appKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowApps failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return nil, c.handleErrorResponse(resp, "ShowApps")
|
||||||
|
}
|
||||||
|
|
||||||
|
var apps []App
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return apps, nil // Return empty slice for not found
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.parseStreamingResponse(resp, &apps); err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowApps failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("ShowApps: found %d apps matching criteria", len(apps))
|
||||||
|
return apps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateApp updates the definition of an application
|
||||||
|
// Maps to POST /auth/ctrl/UpdateApp
|
||||||
|
func (c *Client) UpdateApp(ctx context.Context, input *UpdateAppInput) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/UpdateApp"
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UpdateApp failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return c.handleErrorResponse(resp, "UpdateApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("UpdateApp: %s/%s version %s updated successfully",
|
||||||
|
input.App.Key.Organization, input.App.Key.Name, input.App.Key.Version)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteApp removes an application from the specified region
|
||||||
|
// Maps to POST /auth/ctrl/DeleteApp
|
||||||
|
func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp"
|
||||||
|
|
||||||
|
input := DeleteAppInput{
|
||||||
|
Key: appKey,
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DeleteApp failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return c.handleErrorResponse(resp, "DeleteApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("DeleteApp: %s/%s version %s deleted successfully",
|
||||||
|
appKey.Organization, appKey.Name, appKey.Version)
|
||||||
|
|
||||||
|
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(
|
||||||
|
sdkhttp.RetryOptions{
|
||||||
|
MaxRetries: c.RetryOpts.MaxRetries,
|
||||||
|
InitialDelay: c.RetryOpts.InitialDelay,
|
||||||
|
MaxDelay: c.RetryOpts.MaxDelay,
|
||||||
|
Multiplier: c.RetryOpts.Multiplier,
|
||||||
|
RetryableHTTPStatusCodes: c.RetryOpts.RetryableHTTPStatusCodes,
|
||||||
|
},
|
||||||
|
c.AuthProvider,
|
||||||
|
c.Logger,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleErrorResponse creates an appropriate error from HTTP error response
|
||||||
|
func (c *Client) handleErrorResponse(resp *http.Response, operation string) error {
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
fmt.Sprintf("%s failed with status %d", operation, resp.StatusCode),
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes := []byte{}
|
||||||
|
|
||||||
|
if resp.Body != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
bodyBytes, _ = io.ReadAll(resp.Body)
|
||||||
|
messages = append(messages, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &APIError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Messages: messages,
|
||||||
|
Body: bodyBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
419
sdk/edgeconnect/v2/apps_test.go
Normal file
419
sdk/edgeconnect/v2/apps_test.go
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
// ABOUTME: Unit tests for App management APIs using httptest mock server
|
||||||
|
// ABOUTME: Tests create, show, list, and delete operations with error conditions
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateApp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *NewAppInput
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful creation",
|
||||||
|
input: &NewAppInput{
|
||||||
|
Region: "us-west",
|
||||||
|
App: App{
|
||||||
|
Key: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
Deployment: "kubernetes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"message": "success"}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
input: &NewAppInput{
|
||||||
|
Region: "us-west",
|
||||||
|
App: App{
|
||||||
|
Key: AppKey{
|
||||||
|
Organization: "",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 400,
|
||||||
|
mockResponse: `{"message": "organization is required"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/CreateApp", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
WithAuthProvider(NewStaticTokenProvider("test-token")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.CreateApp(ctx, tt.input)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowApp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
appKey AppKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
expectNotFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful show",
|
||||||
|
appKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"data": {"key": {"organization": "testorg", "name": "testapp", "version": "1.0.0"}, "deployment": "kubernetes"}}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
expectNotFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app not found",
|
||||||
|
appKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: "",
|
||||||
|
expectError: true,
|
||||||
|
expectNotFound: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowApp", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
if tt.mockResponse != "" {
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
app, err := client.ShowApp(ctx, tt.appKey, tt.region)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.expectNotFound {
|
||||||
|
assert.Contains(t, err.Error(), "resource not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.appKey.Organization, app.Key.Organization)
|
||||||
|
assert.Equal(t, tt.appKey.Name, app.Key.Name)
|
||||||
|
assert.Equal(t, tt.appKey.Version, app.Key.Version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowApps(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowApp", r.URL.Path)
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var filter AppFilter
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&filter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "testorg", filter.App.Key.Organization)
|
||||||
|
assert.Equal(t, "us-west", filter.Region)
|
||||||
|
|
||||||
|
// Return multiple apps
|
||||||
|
response := `{"data": {"key": {"organization": "testorg", "name": "app1", "version": "1.0.0"}, "deployment": "kubernetes"}}
|
||||||
|
{"data": {"key": {"organization": "testorg", "name": "app2", "version": "1.0.0"}, "deployment": "docker"}}
|
||||||
|
`
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
apps, err := client.ShowApps(ctx, AppKey{Organization: "testorg"}, "us-west")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, apps, 2)
|
||||||
|
assert.Equal(t, "app1", apps[0].Key.Name)
|
||||||
|
assert.Equal(t, "app2", apps[1].Key.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateApp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *UpdateAppInput
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful update",
|
||||||
|
input: &UpdateAppInput{
|
||||||
|
Region: "us-west",
|
||||||
|
App: App{
|
||||||
|
Key: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
Deployment: "kubernetes",
|
||||||
|
ImagePath: "nginx:latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"message": "success"}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
input: &UpdateAppInput{
|
||||||
|
Region: "us-west",
|
||||||
|
App: App{
|
||||||
|
Key: AppKey{
|
||||||
|
Organization: "",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 400,
|
||||||
|
mockResponse: `{"message": "organization is required"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app not found",
|
||||||
|
input: &UpdateAppInput{
|
||||||
|
Region: "us-west",
|
||||||
|
App: App{
|
||||||
|
Key: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: `{"message": "app not found"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/UpdateApp", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var input UpdateAppInput
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.input.Region, input.Region)
|
||||||
|
assert.Equal(t, tt.input.App.Key.Organization, input.App.Key.Organization)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
WithAuthProvider(NewStaticTokenProvider("test-token")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.UpdateApp(ctx, tt.input)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteApp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
appKey AppKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful deletion",
|
||||||
|
appKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already deleted (404 ok)",
|
||||||
|
appKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error",
|
||||||
|
appKey: AppKey{
|
||||||
|
Organization: "testorg",
|
||||||
|
Name: "testapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 500,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/DeleteApp", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := client.DeleteApp(ctx, tt.appKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientOptions(t *testing.T) {
|
||||||
|
t.Run("with auth provider", func(t *testing.T) {
|
||||||
|
authProvider := NewStaticTokenProvider("test-token")
|
||||||
|
client := NewClient("https://example.com",
|
||||||
|
WithAuthProvider(authProvider),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, authProvider, client.AuthProvider)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with custom HTTP client", func(t *testing.T) {
|
||||||
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
client := NewClient("https://example.com",
|
||||||
|
WithHTTPClient(httpClient),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, httpClient, client.HTTPClient)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with retry options", func(t *testing.T) {
|
||||||
|
retryOpts := RetryOptions{MaxRetries: 5}
|
||||||
|
client := NewClient("https://example.com",
|
||||||
|
WithRetryOptions(retryOpts),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, 5, client.RetryOpts.MaxRetries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIError(t *testing.T) {
|
||||||
|
err := &APIError{
|
||||||
|
StatusCode: 400,
|
||||||
|
Messages: []string{"validation failed", "name is required"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, err.Error(), "validation failed")
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
184
sdk/edgeconnect/v2/auth.go
Normal file
184
sdk/edgeconnect/v2/auth.go
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
// ABOUTME: Authentication providers for EdgeXR Master Controller API
|
||||||
|
// ABOUTME: Supports Bearer token authentication with pluggable provider interface
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthProvider interface for attaching authentication to requests
|
||||||
|
type AuthProvider interface {
|
||||||
|
// Attach adds authentication headers to the request
|
||||||
|
Attach(ctx context.Context, req *http.Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticTokenProvider implements Bearer token authentication with a fixed token
|
||||||
|
type StaticTokenProvider struct {
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStaticTokenProvider creates a new static token provider
|
||||||
|
func NewStaticTokenProvider(token string) *StaticTokenProvider {
|
||||||
|
return &StaticTokenProvider{Token: token}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach adds the Bearer token to the request Authorization header
|
||||||
|
func (s *StaticTokenProvider) Attach(ctx context.Context, req *http.Request) error {
|
||||||
|
if s.Token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+s.Token)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsernamePasswordProvider implements dynamic token retrieval using username/password
|
||||||
|
// This matches the existing client/client.go RetrieveToken implementation
|
||||||
|
type UsernamePasswordProvider struct {
|
||||||
|
BaseURL string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
// Token caching
|
||||||
|
mu sync.RWMutex
|
||||||
|
cachedToken string
|
||||||
|
tokenExpiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUsernamePasswordProvider creates a new username/password auth provider
|
||||||
|
func NewUsernamePasswordProvider(baseURL, username, password string, httpClient *http.Client) *UsernamePasswordProvider {
|
||||||
|
if httpClient == nil {
|
||||||
|
httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UsernamePasswordProvider{
|
||||||
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach retrieves a token (with caching) and adds it to the Authorization header
|
||||||
|
func (u *UsernamePasswordProvider) Attach(ctx context.Context, req *http.Request) error {
|
||||||
|
token, err := u.getToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getToken retrieves a token, using cache if valid
|
||||||
|
func (u *UsernamePasswordProvider) getToken(ctx context.Context) (string, error) {
|
||||||
|
// Check cache first
|
||||||
|
u.mu.RLock()
|
||||||
|
if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
|
||||||
|
token := u.cachedToken
|
||||||
|
u.mu.RUnlock()
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
u.mu.RUnlock()
|
||||||
|
|
||||||
|
// Need to retrieve new token
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock
|
||||||
|
if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) {
|
||||||
|
return u.cachedToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve token using existing RetrieveToken logic
|
||||||
|
token, err := u.retrieveToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache token with reasonable expiry (assume 1 hour, can be configurable)
|
||||||
|
u.cachedToken = token
|
||||||
|
u.tokenExpiry = time.Now().Add(1 * time.Hour)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieveToken implements the same logic as the existing client/client.go RetrieveToken method
|
||||||
|
func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, error) {
|
||||||
|
// Marshal credentials - same as existing implementation
|
||||||
|
jsonData, err := json.Marshal(map[string]string{
|
||||||
|
"username": u.Username,
|
||||||
|
"password": u.Password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request - same as existing implementation
|
||||||
|
loginURL := u.BaseURL + "/api/v1/login"
|
||||||
|
request, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Execute request
|
||||||
|
resp, err := u.HTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read response body - same as existing implementation
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error reading response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response - same as existing implementation
|
||||||
|
var respData struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &respData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error parsing JSON (status %d): %v", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return respData.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateToken clears the cached token, forcing a new login on next request
|
||||||
|
func (u *UsernamePasswordProvider) InvalidateToken() {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
u.cachedToken = ""
|
||||||
|
u.tokenExpiry = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoAuthProvider implements no authentication (for testing or public endpoints)
|
||||||
|
type NoAuthProvider struct{}
|
||||||
|
|
||||||
|
// NewNoAuthProvider creates a new no-auth provider
|
||||||
|
func NewNoAuthProvider() *NoAuthProvider {
|
||||||
|
return &NoAuthProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach does nothing (no authentication)
|
||||||
|
func (n *NoAuthProvider) Attach(ctx context.Context, req *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
226
sdk/edgeconnect/v2/auth_test.go
Normal file
226
sdk/edgeconnect/v2/auth_test.go
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
// ABOUTME: Unit tests for authentication providers including username/password token flow
|
||||||
|
// ABOUTME: Tests token caching, login flow, and error conditions with mock servers
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStaticTokenProvider(t *testing.T) {
|
||||||
|
provider := NewStaticTokenProvider("test-token-123")
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Bearer test-token-123", req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticTokenProvider_EmptyToken(t *testing.T) {
|
||||||
|
provider := NewStaticTokenProvider("")
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsernamePasswordProvider_Success(t *testing.T) {
|
||||||
|
// Mock login server
|
||||||
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/login", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var creds map[string]string
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "testuser", creds["username"])
|
||||||
|
assert.Equal(t, "testpass", creds["password"])
|
||||||
|
|
||||||
|
// Return token
|
||||||
|
response := map[string]string{"token": "dynamic-token-456"}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "testuser", "testpass", nil)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Bearer dynamic-token-456", req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "baduser", "badpass", nil)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "login failed with status 401")
|
||||||
|
assert.Contains(t, err.Error(), "Invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsernamePasswordProvider_TokenCaching(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
|
||||||
|
// Mock login server that tracks calls
|
||||||
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
callCount++
|
||||||
|
response := map[string]string{"token": "cached-token-789"}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "testuser", "testpass", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// First request should call login
|
||||||
|
req1, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
err1 := provider.Attach(ctx, req1)
|
||||||
|
require.NoError(t, err1)
|
||||||
|
assert.Equal(t, "Bearer cached-token-789", req1.Header.Get("Authorization"))
|
||||||
|
assert.Equal(t, 1, callCount)
|
||||||
|
|
||||||
|
// Second request should use cached token (no additional login call)
|
||||||
|
req2, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
err2 := provider.Attach(ctx, req2)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
assert.Equal(t, "Bearer cached-token-789", req2.Header.Get("Authorization"))
|
||||||
|
assert.Equal(t, 1, callCount) // Still only 1 call
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsernamePasswordProvider_TokenExpiry(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
|
||||||
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
callCount++
|
||||||
|
response := map[string]string{"token": "refreshed-token-999"}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "testuser", "testpass", nil)
|
||||||
|
|
||||||
|
// Manually set expired token
|
||||||
|
provider.mu.Lock()
|
||||||
|
provider.cachedToken = "expired-token"
|
||||||
|
provider.tokenExpiry = time.Now().Add(-1 * time.Hour) // Already expired
|
||||||
|
provider.mu.Unlock()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Bearer refreshed-token-999", req.Header.Get("Authorization"))
|
||||||
|
assert.Equal(t, 1, callCount) // New token retrieved
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsernamePasswordProvider_InvalidateToken(t *testing.T) {
|
||||||
|
callCount := 0
|
||||||
|
|
||||||
|
loginServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
callCount++
|
||||||
|
response := map[string]string{"token": "new-token-after-invalidation"}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "testuser", "testpass", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// First request to get token
|
||||||
|
req1, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
err1 := provider.Attach(ctx, req1)
|
||||||
|
require.NoError(t, err1)
|
||||||
|
assert.Equal(t, 1, callCount)
|
||||||
|
|
||||||
|
// Invalidate token
|
||||||
|
provider.InvalidateToken()
|
||||||
|
|
||||||
|
// Next request should get new token
|
||||||
|
req2, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
err2 := provider.Attach(ctx, req2)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
assert.Equal(t, "Bearer new-token-after-invalidation", req2.Header.Get("Authorization"))
|
||||||
|
assert.Equal(t, 2, callCount) // New login call made
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}))
|
||||||
|
defer loginServer.Close()
|
||||||
|
|
||||||
|
provider := NewUsernamePasswordProvider(loginServer.URL, "testuser", "testpass", nil)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://api.example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "error parsing JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoAuthProvider(t *testing.T) {
|
||||||
|
provider := NewNoAuthProvider()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "https://example.com", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := provider.Attach(ctx, req)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClientWithCredentials(t *testing.T) {
|
||||||
|
client := NewClientWithCredentials("https://example.com", "testuser", "testpass")
|
||||||
|
|
||||||
|
assert.Equal(t, "https://example.com", client.BaseURL)
|
||||||
|
|
||||||
|
// Check that auth provider is UsernamePasswordProvider
|
||||||
|
authProvider, ok := client.AuthProvider.(*UsernamePasswordProvider)
|
||||||
|
require.True(t, ok, "AuthProvider should be UsernamePasswordProvider")
|
||||||
|
assert.Equal(t, "testuser", authProvider.Username)
|
||||||
|
assert.Equal(t, "testpass", authProvider.Password)
|
||||||
|
assert.Equal(t, "https://example.com", authProvider.BaseURL)
|
||||||
|
}
|
||||||
122
sdk/edgeconnect/v2/client.go
Normal file
122
sdk/edgeconnect/v2/client.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// ABOUTME: Core EdgeXR Master Controller SDK client with HTTP transport and auth
|
||||||
|
// ABOUTME: Provides typed APIs for app, instance, and cloudlet management operations
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents the EdgeXR Master Controller SDK client
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
AuthProvider AuthProvider
|
||||||
|
RetryOpts RetryOptions
|
||||||
|
Logger Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryOptions configures retry behavior for API calls
|
||||||
|
type RetryOptions struct {
|
||||||
|
MaxRetries int
|
||||||
|
InitialDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
Multiplier float64
|
||||||
|
RetryableHTTPStatusCodes []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger interface for optional logging
|
||||||
|
type Logger interface {
|
||||||
|
Printf(format string, v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRetryOptions returns sensible default retry configuration
|
||||||
|
func DefaultRetryOptions() RetryOptions {
|
||||||
|
return RetryOptions{
|
||||||
|
MaxRetries: 3,
|
||||||
|
InitialDelay: 1 * time.Second,
|
||||||
|
MaxDelay: 30 * time.Second,
|
||||||
|
Multiplier: 2.0,
|
||||||
|
RetryableHTTPStatusCodes: []int{
|
||||||
|
http.StatusRequestTimeout,
|
||||||
|
http.StatusTooManyRequests,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
http.StatusBadGateway,
|
||||||
|
http.StatusServiceUnavailable,
|
||||||
|
http.StatusGatewayTimeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option represents a configuration option for the client
|
||||||
|
type Option func(*Client)
|
||||||
|
|
||||||
|
// WithHTTPClient sets a custom HTTP client
|
||||||
|
func WithHTTPClient(client *http.Client) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.HTTPClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthProvider sets the authentication provider
|
||||||
|
func WithAuthProvider(auth AuthProvider) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.AuthProvider = auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRetryOptions sets retry configuration
|
||||||
|
func WithRetryOptions(opts RetryOptions) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.RetryOpts = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger sets a logger for debugging
|
||||||
|
func WithLogger(logger Logger) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.Logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new EdgeXR SDK client
|
||||||
|
func NewClient(baseURL string, options ...Option) *Client {
|
||||||
|
client := &Client{
|
||||||
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
AuthProvider: NewNoAuthProvider(),
|
||||||
|
RetryOpts: DefaultRetryOptions(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientWithCredentials creates a new EdgeXR SDK client with username/password authentication
|
||||||
|
// This matches the existing client pattern from client/client.go
|
||||||
|
func NewClientWithCredentials(baseURL, username, password string, options ...Option) *Client {
|
||||||
|
client := &Client{
|
||||||
|
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
HTTPClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
AuthProvider: NewUsernamePasswordProvider(baseURL, username, password, nil),
|
||||||
|
RetryOpts: DefaultRetryOptions(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// logf logs a message if a logger is configured
|
||||||
|
func (c *Client) logf(format string, v ...interface{}) {
|
||||||
|
if c.Logger != nil {
|
||||||
|
c.Logger.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
271
sdk/edgeconnect/v2/cloudlet.go
Normal file
271
sdk/edgeconnect/v2/cloudlet.go
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
// ABOUTME: Cloudlet management APIs for EdgeXR Master Controller
|
||||||
|
// ABOUTME: Provides typed methods for creating, querying, and managing edge cloudlets
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
sdkhttp "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/internal/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateCloudlet creates a new cloudlet in the specified region
|
||||||
|
// Maps to POST /auth/ctrl/CreateCloudlet
|
||||||
|
func (c *Client) CreateCloudlet(ctx context.Context, input *NewCloudletInput) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/CreateCloudlet"
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CreateCloudlet failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return c.handleErrorResponse(resp, "CreateCloudlet")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("CreateCloudlet: %s/%s created successfully",
|
||||||
|
input.Cloudlet.Key.Organization, input.Cloudlet.Key.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCloudlet retrieves a single cloudlet by key and region
|
||||||
|
// Maps to POST /auth/ctrl/ShowCloudlet
|
||||||
|
func (c *Client) ShowCloudlet(ctx context.Context, cloudletKey CloudletKey, region string) (Cloudlet, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowCloudlet"
|
||||||
|
|
||||||
|
filter := CloudletFilter{
|
||||||
|
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return Cloudlet{}, c.handleErrorResponse(resp, "ShowCloudlet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse streaming JSON response
|
||||||
|
var cloudlets []Cloudlet
|
||||||
|
if err := c.parseStreamingCloudletResponse(resp, &cloudlets); err != nil {
|
||||||
|
return Cloudlet{}, fmt.Errorf("ShowCloudlet failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cloudlets) == 0 {
|
||||||
|
return Cloudlet{}, fmt.Errorf("cloudlet %s/%s in region %s: %w",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloudlets[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowCloudlets retrieves all cloudlets matching the filter criteria
|
||||||
|
// Maps to POST /auth/ctrl/ShowCloudlet
|
||||||
|
func (c *Client) ShowCloudlets(ctx context.Context, cloudletKey CloudletKey, region string) ([]Cloudlet, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/ShowCloudlet"
|
||||||
|
|
||||||
|
filter := CloudletFilter{
|
||||||
|
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowCloudlets failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return nil, c.handleErrorResponse(resp, "ShowCloudlets")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloudlets []Cloudlet
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return cloudlets, nil // Return empty slice for not found
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.parseStreamingCloudletResponse(resp, &cloudlets); err != nil {
|
||||||
|
return nil, fmt.Errorf("ShowCloudlets failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("ShowCloudlets: found %d cloudlets matching criteria", len(cloudlets))
|
||||||
|
return cloudlets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCloudlet removes a cloudlet from the specified region
|
||||||
|
// Maps to POST /auth/ctrl/DeleteCloudlet
|
||||||
|
func (c *Client) DeleteCloudlet(ctx context.Context, cloudletKey CloudletKey, region string) error {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/DeleteCloudlet"
|
||||||
|
|
||||||
|
filter := CloudletFilter{
|
||||||
|
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DeleteCloudlet failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 404 is acceptable for delete operations (already deleted)
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound {
|
||||||
|
return c.handleErrorResponse(resp, "DeleteCloudlet")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("DeleteCloudlet: %s/%s deleted successfully",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCloudletManifest retrieves the deployment manifest for a cloudlet
|
||||||
|
// Maps to POST /auth/ctrl/GetCloudletManifest
|
||||||
|
func (c *Client) GetCloudletManifest(ctx context.Context, cloudletKey CloudletKey, region string) (*CloudletManifest, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/GetCloudletManifest"
|
||||||
|
|
||||||
|
filter := CloudletFilter{
|
||||||
|
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetCloudletManifest failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, fmt.Errorf("cloudlet manifest %s/%s in region %s: %w",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, c.handleErrorResponse(resp, "GetCloudletManifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response as CloudletManifest
|
||||||
|
var manifest CloudletManifest
|
||||||
|
if err := c.parseDirectJSONResponse(resp, &manifest); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetCloudletManifest failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("GetCloudletManifest: retrieved manifest for %s/%s",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name)
|
||||||
|
|
||||||
|
return &manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCloudletResourceUsage retrieves resource usage information for a cloudlet
|
||||||
|
// Maps to POST /auth/ctrl/GetCloudletResourceUsage
|
||||||
|
func (c *Client) GetCloudletResourceUsage(ctx context.Context, cloudletKey CloudletKey, region string) (*CloudletResourceUsage, error) {
|
||||||
|
transport := c.getTransport()
|
||||||
|
url := c.BaseURL + "/api/v1/auth/ctrl/GetCloudletResourceUsage"
|
||||||
|
|
||||||
|
filter := CloudletFilter{
|
||||||
|
Cloudlet: Cloudlet{Key: cloudletKey},
|
||||||
|
Region: region,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetCloudletResourceUsage failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, fmt.Errorf("cloudlet resource usage %s/%s in region %s: %w",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name, region, ErrResourceNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, c.handleErrorResponse(resp, "GetCloudletResourceUsage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response as CloudletResourceUsage
|
||||||
|
var usage CloudletResourceUsage
|
||||||
|
if err := c.parseDirectJSONResponse(resp, &usage); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetCloudletResourceUsage failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logf("GetCloudletResourceUsage: retrieved usage for %s/%s",
|
||||||
|
cloudletKey.Organization, cloudletKey.Name)
|
||||||
|
|
||||||
|
return &usage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStreamingCloudletResponse parses the EdgeXR streaming JSON response format for cloudlets
|
||||||
|
func (c *Client) parseStreamingCloudletResponse(resp *http.Response, result interface{}) error {
|
||||||
|
var responses []Response[Cloudlet]
|
||||||
|
|
||||||
|
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error {
|
||||||
|
var response Response[Cloudlet]
|
||||||
|
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
|
||||||
|
var cloudlets []Cloudlet
|
||||||
|
var messages []string
|
||||||
|
|
||||||
|
for _, response := range responses {
|
||||||
|
if response.HasData() {
|
||||||
|
cloudlets = append(cloudlets, 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 *[]Cloudlet:
|
||||||
|
*v = cloudlets
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported result type: %T", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDirectJSONResponse parses a direct JSON response (not streaming)
|
||||||
|
func (c *Client) parseDirectJSONResponse(resp *http.Response, result interface{}) error {
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
if err := decoder.Decode(result); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode JSON response: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
408
sdk/edgeconnect/v2/cloudlet_test.go
Normal file
408
sdk/edgeconnect/v2/cloudlet_test.go
Normal file
|
|
@ -0,0 +1,408 @@
|
||||||
|
// ABOUTME: Unit tests for Cloudlet management APIs using httptest mock server
|
||||||
|
// ABOUTME: Tests create, show, list, delete, manifest, and resource usage operations
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateCloudlet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *NewCloudletInput
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful creation",
|
||||||
|
input: &NewCloudletInput{
|
||||||
|
Region: "us-west",
|
||||||
|
Cloudlet: Cloudlet{
|
||||||
|
Key: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
Location: Location{
|
||||||
|
Latitude: 37.7749,
|
||||||
|
Longitude: -122.4194,
|
||||||
|
},
|
||||||
|
IpSupport: "IpSupportDynamic",
|
||||||
|
NumDynamicIps: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"message": "success"}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validation error",
|
||||||
|
input: &NewCloudletInput{
|
||||||
|
Region: "us-west",
|
||||||
|
Cloudlet: Cloudlet{
|
||||||
|
Key: CloudletKey{
|
||||||
|
Organization: "",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockStatusCode: 400,
|
||||||
|
mockResponse: `{"message": "organization is required"}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/CreateCloudlet", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
WithAuthProvider(NewStaticTokenProvider("test-token")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
err := client.CreateCloudlet(ctx, tt.input)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowCloudlet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cloudletKey CloudletKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
expectNotFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful show",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"data": {"key": {"organization": "cloudletorg", "name": "testcloudlet"}, "state": "Ready", "location": {"latitude": 37.7749, "longitude": -122.4194}}}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
expectNotFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cloudlet not found",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: "",
|
||||||
|
expectError: true,
|
||||||
|
expectNotFound: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create mock server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowCloudlet", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
if tt.mockResponse != "" {
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Create client
|
||||||
|
client := NewClient(server.URL,
|
||||||
|
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute test
|
||||||
|
ctx := context.Background()
|
||||||
|
cloudlet, err := client.ShowCloudlet(ctx, tt.cloudletKey, tt.region)
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.expectNotFound {
|
||||||
|
assert.Contains(t, err.Error(), "resource not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.cloudletKey.Organization, cloudlet.Key.Organization)
|
||||||
|
assert.Equal(t, tt.cloudletKey.Name, cloudlet.Key.Name)
|
||||||
|
assert.Equal(t, "Ready", cloudlet.State)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShowCloudlets(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/ShowCloudlet", r.URL.Path)
|
||||||
|
|
||||||
|
// Verify request body
|
||||||
|
var filter CloudletFilter
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&filter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "cloudletorg", filter.Cloudlet.Key.Organization)
|
||||||
|
assert.Equal(t, "us-west", filter.Region)
|
||||||
|
|
||||||
|
// Return multiple cloudlets
|
||||||
|
response := `{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet1"}, "state": "Ready"}}
|
||||||
|
{"data": {"key": {"organization": "cloudletorg", "name": "cloudlet2"}, "state": "Creating"}}
|
||||||
|
`
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cloudlets, err := client.ShowCloudlets(ctx, CloudletKey{Organization: "cloudletorg"}, "us-west")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, cloudlets, 2)
|
||||||
|
assert.Equal(t, "cloudlet1", cloudlets[0].Key.Name)
|
||||||
|
assert.Equal(t, "Ready", cloudlets[0].State)
|
||||||
|
assert.Equal(t, "cloudlet2", cloudlets[1].Key.Name)
|
||||||
|
assert.Equal(t, "Creating", cloudlets[1].State)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteCloudlet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cloudletKey CloudletKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful deletion",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already deleted (404 ok)",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 500,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/DeleteCloudlet", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := client.DeleteCloudlet(ctx, tt.cloudletKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCloudletManifest(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cloudletKey CloudletKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
expectNotFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful manifest retrieval",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"manifest": "apiVersion: v1\nkind: Deployment\nmetadata:\n name: test", "last_modified": "2024-01-01T00:00:00Z"}`,
|
||||||
|
expectError: false,
|
||||||
|
expectNotFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "manifest not found",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: "",
|
||||||
|
expectError: true,
|
||||||
|
expectNotFound: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/GetCloudletManifest", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
if tt.mockResponse != "" {
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
manifest, err := client.GetCloudletManifest(ctx, tt.cloudletKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.expectNotFound {
|
||||||
|
assert.Contains(t, err.Error(), "resource not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, manifest)
|
||||||
|
assert.Contains(t, manifest.Manifest, "apiVersion: v1")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCloudletResourceUsage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cloudletKey CloudletKey
|
||||||
|
region string
|
||||||
|
mockStatusCode int
|
||||||
|
mockResponse string
|
||||||
|
expectError bool
|
||||||
|
expectNotFound bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful usage retrieval",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "testcloudlet",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 200,
|
||||||
|
mockResponse: `{"cloudlet_key": {"organization": "cloudletorg", "name": "testcloudlet"}, "region": "us-west", "usage": {"cpu": "50%", "memory": "30%", "disk": "20%"}}`,
|
||||||
|
expectError: false,
|
||||||
|
expectNotFound: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "usage not found",
|
||||||
|
cloudletKey: CloudletKey{
|
||||||
|
Organization: "cloudletorg",
|
||||||
|
Name: "nonexistent",
|
||||||
|
},
|
||||||
|
region: "us-west",
|
||||||
|
mockStatusCode: 404,
|
||||||
|
mockResponse: "",
|
||||||
|
expectError: true,
|
||||||
|
expectNotFound: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "POST", r.Method)
|
||||||
|
assert.Equal(t, "/api/v1/auth/ctrl/GetCloudletResourceUsage", r.URL.Path)
|
||||||
|
|
||||||
|
w.WriteHeader(tt.mockStatusCode)
|
||||||
|
if tt.mockResponse != "" {
|
||||||
|
w.Write([]byte(tt.mockResponse))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
usage, err := client.GetCloudletResourceUsage(ctx, tt.cloudletKey, tt.region)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
if tt.expectNotFound {
|
||||||
|
assert.Contains(t, err.Error(), "resource not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, usage)
|
||||||
|
assert.Equal(t, "cloudletorg", usage.CloudletKey.Organization)
|
||||||
|
assert.Equal(t, "testcloudlet", usage.CloudletKey.Name)
|
||||||
|
assert.Equal(t, "us-west", usage.Region)
|
||||||
|
assert.Contains(t, usage.Usage, "cpu")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
407
sdk/edgeconnect/v2/types.go
Normal file
407
sdk/edgeconnect/v2/types.go
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
// ABOUTME: Core type definitions for EdgeXR Master Controller SDK
|
||||||
|
// ABOUTME: These types are based on the swagger API specification and existing client patterns
|
||||||
|
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App field constants for partial updates (based on EdgeXR API specification)
|
||||||
|
const (
|
||||||
|
AppFieldKey = "2"
|
||||||
|
AppFieldKeyOrganization = "2.1"
|
||||||
|
AppFieldKeyName = "2.2"
|
||||||
|
AppFieldKeyVersion = "2.3"
|
||||||
|
AppFieldImagePath = "4"
|
||||||
|
AppFieldImageType = "5"
|
||||||
|
AppFieldAccessPorts = "7"
|
||||||
|
AppFieldDefaultFlavor = "9"
|
||||||
|
AppFieldDefaultFlavorName = "9.1"
|
||||||
|
AppFieldAuthPublicKey = "12"
|
||||||
|
AppFieldCommand = "13"
|
||||||
|
AppFieldAnnotations = "14"
|
||||||
|
AppFieldDeployment = "15"
|
||||||
|
AppFieldDeploymentManifest = "16"
|
||||||
|
AppFieldDeploymentGenerator = "17"
|
||||||
|
AppFieldAndroidPackageName = "18"
|
||||||
|
AppFieldDelOpt = "20"
|
||||||
|
AppFieldConfigs = "21"
|
||||||
|
AppFieldConfigsKind = "21.1"
|
||||||
|
AppFieldConfigsConfig = "21.2"
|
||||||
|
AppFieldScaleWithCluster = "22"
|
||||||
|
AppFieldInternalPorts = "23"
|
||||||
|
AppFieldRevision = "24"
|
||||||
|
AppFieldOfficialFqdn = "25"
|
||||||
|
AppFieldMd5Sum = "26"
|
||||||
|
AppFieldAutoProvPolicy = "28"
|
||||||
|
AppFieldAccessType = "29"
|
||||||
|
AppFieldDeletePrepare = "31"
|
||||||
|
AppFieldAutoProvPolicies = "32"
|
||||||
|
AppFieldTemplateDelimiter = "33"
|
||||||
|
AppFieldSkipHcPorts = "34"
|
||||||
|
AppFieldCreatedAt = "35"
|
||||||
|
AppFieldCreatedAtSeconds = "35.1"
|
||||||
|
AppFieldCreatedAtNanos = "35.2"
|
||||||
|
AppFieldUpdatedAt = "36"
|
||||||
|
AppFieldUpdatedAtSeconds = "36.1"
|
||||||
|
AppFieldUpdatedAtNanos = "36.2"
|
||||||
|
AppFieldTrusted = "37"
|
||||||
|
AppFieldRequiredOutboundConnections = "38"
|
||||||
|
AppFieldAllowServerless = "39"
|
||||||
|
AppFieldServerlessConfig = "40"
|
||||||
|
AppFieldVmAppOsType = "41"
|
||||||
|
AppFieldAlertPolicies = "42"
|
||||||
|
AppFieldQosSessionProfile = "43"
|
||||||
|
AppFieldQosSessionDuration = "44"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message interface for types that can provide error messages
|
||||||
|
type Message interface {
|
||||||
|
GetMessage() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base message type for API responses
|
||||||
|
type msg struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m msg) GetMessage() string {
|
||||||
|
return m.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppKey uniquely identifies an application
|
||||||
|
type AppKey struct {
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloudletKey uniquely identifies a cloudlet
|
||||||
|
type CloudletKey struct {
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppInstanceKey uniquely identifies an application instance
|
||||||
|
type AppInstanceKey struct {
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CloudletKey CloudletKey `json:"cloudlet_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flavor defines resource allocation for instances
|
||||||
|
type Flavor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecurityRule defines network access rules
|
||||||
|
type SecurityRule struct {
|
||||||
|
PortRangeMax int `json:"port_range_max"`
|
||||||
|
PortRangeMin int `json:"port_range_min"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
RemoteCIDR string `json:"remote_cidr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// App represents an application definition
|
||||||
|
type App struct {
|
||||||
|
msg `json:",inline"`
|
||||||
|
Key AppKey `json:"key"`
|
||||||
|
Deployment string `json:"deployment,omitempty"`
|
||||||
|
ImageType string `json:"image_type,omitempty"`
|
||||||
|
ImagePath string `json:"image_path,omitempty"`
|
||||||
|
AccessPorts string `json:"access_ports,omitempty"`
|
||||||
|
AllowServerless bool `json:"allow_serverless,omitempty"`
|
||||||
|
DefaultFlavor Flavor `json:"defaultFlavor,omitempty"`
|
||||||
|
ServerlessConfig interface{} `json:"serverless_config,omitempty"`
|
||||||
|
DeploymentGenerator string `json:"deployment_generator,omitempty"`
|
||||||
|
DeploymentManifest string `json:"deployment_manifest,omitempty"`
|
||||||
|
RequiredOutboundConnections []SecurityRule `json:"required_outbound_connections"`
|
||||||
|
GlobalID string `json:"global_id,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt string `json:"updated_at,omitempty"`
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppInstance represents a deployed application instance
|
||||||
|
type AppInstance struct {
|
||||||
|
msg `json:",inline"`
|
||||||
|
Key AppInstanceKey `json:"key"`
|
||||||
|
AppKey AppKey `json:"app_key,omitempty"`
|
||||||
|
CloudletLoc CloudletLoc `json:"cloudlet_loc,omitempty"`
|
||||||
|
Flavor Flavor `json:"flavor,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
IngressURL string `json:"ingress_url,omitempty"`
|
||||||
|
UniqueID string `json:"unique_id,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt string `json:"updated_at,omitempty"`
|
||||||
|
PowerState string `json:"power_state,omitempty"`
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloudlet represents edge infrastructure
|
||||||
|
type Cloudlet struct {
|
||||||
|
msg `json:",inline"`
|
||||||
|
Key CloudletKey `json:"key"`
|
||||||
|
Location Location `json:"location"`
|
||||||
|
IpSupport string `json:"ip_support,omitempty"`
|
||||||
|
NumDynamicIps int32 `json:"num_dynamic_ips,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
Flavor Flavor `json:"flavor,omitempty"`
|
||||||
|
PhysicalName string `json:"physical_name,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
NotifySrvAddr string `json:"notify_srv_addr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location represents geographical coordinates
|
||||||
|
type Location struct {
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloudletLoc represents geographical coordinates for cloudlets
|
||||||
|
type CloudletLoc struct {
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input types for API operations
|
||||||
|
|
||||||
|
// NewAppInput represents input for creating an application
|
||||||
|
type NewAppInput struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
App App `json:"app"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppInstanceInput represents input for creating an app instance
|
||||||
|
type NewAppInstanceInput struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
AppInst AppInstance `json:"appinst"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCloudletInput represents input for creating a cloudlet
|
||||||
|
type NewCloudletInput struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Cloudlet Cloudlet `json:"cloudlet"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppInput represents input for updating an application
|
||||||
|
type UpdateAppInput struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
App App `json:"app"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAppInstanceInput represents input for updating an app instance
|
||||||
|
type UpdateAppInstanceInput struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
AppInst AppInstance `json:"appinst"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAppInput represents input for deleting an application
|
||||||
|
type DeleteAppInput struct {
|
||||||
|
Key AppKey `json:"key"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAppInstanceInput represents input for deleting an app instance
|
||||||
|
type DeleteAppInstanceInput struct {
|
||||||
|
Key AppInstanceKey `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response wrapper types
|
||||||
|
|
||||||
|
// Response wraps a single API response
|
||||||
|
type Response[T Message] struct {
|
||||||
|
Data T `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response[T]) HasData() bool {
|
||||||
|
return !res.IsMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response[T]) IsMessage() bool {
|
||||||
|
return res.Data.GetMessage() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultResponse represents an API result with error code
|
||||||
|
type ResultResponse struct {
|
||||||
|
Result struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResultResponse) IsError() bool {
|
||||||
|
return r.Result.Code >= 400
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResultResponse) GetMessage() string {
|
||||||
|
return r.Result.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ResultResponse) GetCode() int {
|
||||||
|
return r.Result.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responses wraps multiple API responses with metadata
|
||||||
|
type Responses[T Message] struct {
|
||||||
|
Responses []Response[T] `json:"responses,omitempty"`
|
||||||
|
StatusCode int `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responses[T]) GetData() []T {
|
||||||
|
var data []T
|
||||||
|
for _, v := range r.Responses {
|
||||||
|
if v.HasData() {
|
||||||
|
data = append(data, v.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responses[T]) GetMessages() []string {
|
||||||
|
var messages []string
|
||||||
|
for _, v := range r.Responses {
|
||||||
|
if v.IsMessage() {
|
||||||
|
messages = append(messages, v.Data.GetMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responses[T]) IsSuccessful() bool {
|
||||||
|
return r.StatusCode >= 200 && r.StatusCode < 400
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responses[T]) Error() error {
|
||||||
|
if r.IsSuccessful() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &APIError{
|
||||||
|
StatusCode: r.StatusCode,
|
||||||
|
Messages: r.GetMessages(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIError represents an API error with details
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Messages []string `json:"messages,omitempty"`
|
||||||
|
Body []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
jsonErr, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("API error: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("API error: %s", jsonErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter types for querying
|
||||||
|
|
||||||
|
// AppFilter represents filters for app queries
|
||||||
|
type AppFilter struct {
|
||||||
|
App App `json:"app"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppInstanceFilter represents filters for app instance queries
|
||||||
|
type AppInstanceFilter struct {
|
||||||
|
AppInstance AppInstance `json:"appinst"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloudletFilter represents filters for cloudlet queries
|
||||||
|
type CloudletFilter struct {
|
||||||
|
Cloudlet Cloudlet `json:"cloudlet"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloudletManifest represents cloudlet deployment manifest
|
||||||
|
type CloudletManifest struct {
|
||||||
|
Manifest string `json:"manifest"`
|
||||||
|
LastModified time.Time `json:"last_modified,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloudletResourceUsage represents cloudlet resource utilization
|
||||||
|
type CloudletResourceUsage struct {
|
||||||
|
CloudletKey CloudletKey `json:"cloudlet_key"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Usage map[string]interface{} `json:"usage"`
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -24,20 +24,20 @@ func main() {
|
||||||
username := getEnvOrDefault("EDGEXR_USERNAME", "")
|
username := getEnvOrDefault("EDGEXR_USERNAME", "")
|
||||||
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
|
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
|
||||||
|
|
||||||
var client *edgeconnect.Client
|
var client *v2.Client
|
||||||
|
|
||||||
if token != "" {
|
if token != "" {
|
||||||
fmt.Println("🔐 Using Bearer token authentication")
|
fmt.Println("🔐 Using Bearer token authentication")
|
||||||
client = edgeconnect.NewClient(baseURL,
|
client = v2.NewClient(baseURL,
|
||||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
edgeconnect.WithAuthProvider(edgeconnect.NewStaticTokenProvider(token)),
|
v2.WithAuthProvider(v2.NewStaticTokenProvider(token)),
|
||||||
edgeconnect.WithLogger(log.Default()),
|
v2.WithLogger(log.Default()),
|
||||||
)
|
)
|
||||||
} else if username != "" && password != "" {
|
} else if username != "" && password != "" {
|
||||||
fmt.Println("🔐 Using username/password authentication")
|
fmt.Println("🔐 Using username/password authentication")
|
||||||
client = edgeconnect.NewClientWithCredentials(baseURL, username, password,
|
client = v2.NewClientWithCredentials(baseURL, username, password,
|
||||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
edgeconnect.WithLogger(log.Default()),
|
v2.WithLogger(log.Default()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
|
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
|
||||||
|
|
@ -85,15 +85,15 @@ type WorkflowConfig struct {
|
||||||
FlavorName string
|
FlavorName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config WorkflowConfig) error {
|
func runComprehensiveWorkflow(ctx context.Context, c *v2.Client, config WorkflowConfig) error {
|
||||||
fmt.Println("═══ Phase 1: Application Management ═══")
|
fmt.Println("═══ Phase 1: Application Management ═══")
|
||||||
|
|
||||||
// 1. Create Application
|
// 1. Create Application
|
||||||
fmt.Println("\n1️⃣ Creating application...")
|
fmt.Println("\n1️⃣ Creating application...")
|
||||||
app := &edgeconnect.NewAppInput{
|
app := &v2.NewAppInput{
|
||||||
Region: config.Region,
|
Region: config.Region,
|
||||||
App: edgeconnect.App{
|
App: v2.App{
|
||||||
Key: edgeconnect.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: config.Organization,
|
Organization: config.Organization,
|
||||||
Name: config.AppName,
|
Name: config.AppName,
|
||||||
Version: config.AppVersion,
|
Version: config.AppVersion,
|
||||||
|
|
@ -101,10 +101,10 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
Deployment: "kubernetes",
|
Deployment: "kubernetes",
|
||||||
ImageType: "ImageTypeDocker", // field is ignored
|
ImageType: "ImageTypeDocker", // field is ignored
|
||||||
ImagePath: "https://registry-1.docker.io/library/nginx:latest", // must be set. Even for kubernetes
|
ImagePath: "https://registry-1.docker.io/library/nginx:latest", // must be set. Even for kubernetes
|
||||||
DefaultFlavor: edgeconnect.Flavor{Name: config.FlavorName},
|
DefaultFlavor: v2.Flavor{Name: config.FlavorName},
|
||||||
ServerlessConfig: struct{}{}, // must be set
|
ServerlessConfig: struct{}{}, // must be set
|
||||||
AllowServerless: true, // must be set to true for kubernetes
|
AllowServerless: true, // must be set to true for kubernetes
|
||||||
RequiredOutboundConnections: []edgeconnect.SecurityRule{
|
RequiredOutboundConnections: []v2.SecurityRule{
|
||||||
{
|
{
|
||||||
Protocol: "tcp",
|
Protocol: "tcp",
|
||||||
PortRangeMin: 80,
|
PortRangeMin: 80,
|
||||||
|
|
@ -128,7 +128,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 2. Show Application Details
|
// 2. Show Application Details
|
||||||
fmt.Println("\n2️⃣ Querying application details...")
|
fmt.Println("\n2️⃣ Querying application details...")
|
||||||
appKey := edgeconnect.AppKey{
|
appKey := v2.AppKey{
|
||||||
Organization: config.Organization,
|
Organization: config.Organization,
|
||||||
Name: config.AppName,
|
Name: config.AppName,
|
||||||
Version: config.AppVersion,
|
Version: config.AppVersion,
|
||||||
|
|
@ -146,7 +146,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 3. List Applications in Organization
|
// 3. List Applications in Organization
|
||||||
fmt.Println("\n3️⃣ Listing applications in organization...")
|
fmt.Println("\n3️⃣ Listing applications in organization...")
|
||||||
filter := edgeconnect.AppKey{Organization: config.Organization}
|
filter := v2.AppKey{Organization: config.Organization}
|
||||||
apps, err := c.ShowApps(ctx, filter, config.Region)
|
apps, err := c.ShowApps(ctx, filter, config.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list apps: %w", err)
|
return fmt.Errorf("failed to list apps: %w", err)
|
||||||
|
|
@ -160,19 +160,19 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 4. Create Application Instance
|
// 4. Create Application Instance
|
||||||
fmt.Println("\n4️⃣ Creating application instance...")
|
fmt.Println("\n4️⃣ Creating application instance...")
|
||||||
instance := &edgeconnect.NewAppInstanceInput{
|
instance := &v2.NewAppInstanceInput{
|
||||||
Region: config.Region,
|
Region: config.Region,
|
||||||
AppInst: edgeconnect.AppInstance{
|
AppInst: v2.AppInstance{
|
||||||
Key: edgeconnect.AppInstanceKey{
|
Key: v2.AppInstanceKey{
|
||||||
Organization: config.Organization,
|
Organization: config.Organization,
|
||||||
Name: config.InstanceName,
|
Name: config.InstanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: config.CloudletOrg,
|
Organization: config.CloudletOrg,
|
||||||
Name: config.CloudletName,
|
Name: config.CloudletName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AppKey: appKey,
|
AppKey: appKey,
|
||||||
Flavor: edgeconnect.Flavor{Name: config.FlavorName},
|
Flavor: v2.Flavor{Name: config.FlavorName},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,10 +184,10 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 5. Wait for Application Instance to be Ready
|
// 5. Wait for Application Instance to be Ready
|
||||||
fmt.Println("\n5️⃣ Waiting for application instance to be ready...")
|
fmt.Println("\n5️⃣ Waiting for application instance to be ready...")
|
||||||
instanceKey := edgeconnect.AppInstanceKey{
|
instanceKey := v2.AppInstanceKey{
|
||||||
Organization: config.Organization,
|
Organization: config.Organization,
|
||||||
Name: config.InstanceName,
|
Name: config.InstanceName,
|
||||||
CloudletKey: edgeconnect.CloudletKey{
|
CloudletKey: v2.CloudletKey{
|
||||||
Organization: config.CloudletOrg,
|
Organization: config.CloudletOrg,
|
||||||
Name: config.CloudletName,
|
Name: config.CloudletName,
|
||||||
},
|
},
|
||||||
|
|
@ -207,7 +207,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 6. List Application Instances
|
// 6. List Application Instances
|
||||||
fmt.Println("\n6️⃣ Listing application instances...")
|
fmt.Println("\n6️⃣ Listing application instances...")
|
||||||
instances, err := c.ShowAppInstances(ctx, edgeconnect.AppInstanceKey{Organization: config.Organization}, config.Region)
|
instances, err := c.ShowAppInstances(ctx, v2.AppInstanceKey{Organization: config.Organization}, config.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list app instances: %w", err)
|
return fmt.Errorf("failed to list app instances: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +228,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
|
|
||||||
// 8. Show Cloudlet Details
|
// 8. Show Cloudlet Details
|
||||||
fmt.Println("\n8️⃣ Querying cloudlet information...")
|
fmt.Println("\n8️⃣ Querying cloudlet information...")
|
||||||
cloudletKey := edgeconnect.CloudletKey{
|
cloudletKey := v2.CloudletKey{
|
||||||
Organization: config.CloudletOrg,
|
Organization: config.CloudletOrg,
|
||||||
Name: config.CloudletName,
|
Name: config.CloudletName,
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +287,7 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
||||||
// 13. Verify Cleanup
|
// 13. Verify Cleanup
|
||||||
fmt.Println("\n1️⃣3️⃣ Verifying cleanup...")
|
fmt.Println("\n1️⃣3️⃣ Verifying cleanup...")
|
||||||
_, err = c.ShowApp(ctx, appKey, config.Region)
|
_, err = c.ShowApp(ctx, appKey, config.Region)
|
||||||
if err != nil && fmt.Sprintf("%v", err) == edgeconnect.ErrResourceNotFound.Error() {
|
if err != nil && fmt.Sprintf("%v", err) == v2.ErrResourceNotFound.Error() {
|
||||||
fmt.Printf("✅ Cleanup verified - app no longer exists\n")
|
fmt.Printf("✅ Cleanup verified - app no longer exists\n")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
fmt.Printf("✅ Cleanup appears successful (verification returned: %v)\n", err)
|
fmt.Printf("✅ Cleanup appears successful (verification returned: %v)\n", err)
|
||||||
|
|
@ -306,7 +306,7 @@ func getEnvOrDefault(key, defaultValue string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForInstanceReady polls the instance status until it's no longer "Creating" or timeout
|
// waitForInstanceReady polls the instance status until it's no longer "Creating" or timeout
|
||||||
func waitForInstanceReady(ctx context.Context, c *edgeconnect.Client, instanceKey edgeconnect.AppInstanceKey, region string, timeout time.Duration) (edgeconnect.AppInstance, error) {
|
func waitForInstanceReady(ctx context.Context, c *v2.Client, instanceKey v2.AppInstanceKey, region string, timeout time.Duration) (v2.AppInstance, error) {
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
@ -318,7 +318,7 @@ func waitForInstanceReady(ctx context.Context, c *edgeconnect.Client, instanceKe
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timeoutCtx.Done():
|
case <-timeoutCtx.Done():
|
||||||
return edgeconnect.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
|
return v2.AppInstance{}, fmt.Errorf("timeout waiting for instance to be ready after %v", timeout)
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
|
instance, err := c.ShowAppInstance(timeoutCtx, instanceKey, region)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect"
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -24,22 +24,22 @@ func main() {
|
||||||
username := getEnvOrDefault("EDGEXR_USERNAME", "")
|
username := getEnvOrDefault("EDGEXR_USERNAME", "")
|
||||||
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
|
password := getEnvOrDefault("EDGEXR_PASSWORD", "")
|
||||||
|
|
||||||
var edgeClient *edgeconnect.Client
|
var edgeClient *v2.Client
|
||||||
|
|
||||||
if token != "" {
|
if token != "" {
|
||||||
// Use static token authentication
|
// Use static token authentication
|
||||||
fmt.Println("🔐 Using Bearer token authentication")
|
fmt.Println("🔐 Using Bearer token authentication")
|
||||||
edgeClient = edgeconnect.NewClient(baseURL,
|
edgeClient = v2.NewClient(baseURL,
|
||||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
edgeconnect.WithAuthProvider(edgeconnect.NewStaticTokenProvider(token)),
|
v2.WithAuthProvider(v2.NewStaticTokenProvider(token)),
|
||||||
edgeconnect.WithLogger(log.Default()),
|
v2.WithLogger(log.Default()),
|
||||||
)
|
)
|
||||||
} else if username != "" && password != "" {
|
} else if username != "" && password != "" {
|
||||||
// Use username/password authentication (matches existing client pattern)
|
// Use username/password authentication (matches existing client pattern)
|
||||||
fmt.Println("🔐 Using username/password authentication")
|
fmt.Println("🔐 Using username/password authentication")
|
||||||
edgeClient = edgeconnect.NewClientWithCredentials(baseURL, username, password,
|
edgeClient = v2.NewClientWithCredentials(baseURL, username, password,
|
||||||
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
v2.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
|
||||||
edgeconnect.WithLogger(log.Default()),
|
v2.WithLogger(log.Default()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
|
log.Fatal("Authentication required: Set either EDGEXR_TOKEN or both EDGEXR_USERNAME and EDGEXR_PASSWORD")
|
||||||
|
|
@ -48,10 +48,10 @@ func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Example application to deploy
|
// Example application to deploy
|
||||||
app := &edgeconnect.NewAppInput{
|
app := &v2.NewAppInput{
|
||||||
Region: "EU",
|
Region: "EU",
|
||||||
App: edgeconnect.App{
|
App: v2.App{
|
||||||
Key: edgeconnect.AppKey{
|
Key: v2.AppKey{
|
||||||
Organization: "edp2",
|
Organization: "edp2",
|
||||||
Name: "my-edge-app",
|
Name: "my-edge-app",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
|
|
@ -59,7 +59,7 @@ func main() {
|
||||||
Deployment: "docker",
|
Deployment: "docker",
|
||||||
ImageType: "ImageTypeDocker",
|
ImageType: "ImageTypeDocker",
|
||||||
ImagePath: "https://registry-1.docker.io/library/nginx:latest",
|
ImagePath: "https://registry-1.docker.io/library/nginx:latest",
|
||||||
DefaultFlavor: edgeconnect.Flavor{Name: "EU.small"},
|
DefaultFlavor: v2.Flavor{Name: "EU.small"},
|
||||||
ServerlessConfig: struct{}{},
|
ServerlessConfig: struct{}{},
|
||||||
AllowServerless: false,
|
AllowServerless: false,
|
||||||
},
|
},
|
||||||
|
|
@ -73,7 +73,7 @@ func main() {
|
||||||
fmt.Println("✅ SDK example completed successfully!")
|
fmt.Println("✅ SDK example completed successfully!")
|
||||||
}
|
}
|
||||||
|
|
||||||
func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client, input *edgeconnect.NewAppInput) error {
|
func demonstrateAppLifecycle(ctx context.Context, edgeClient *v2.Client, input *v2.NewAppInput) error {
|
||||||
appKey := input.App.Key
|
appKey := input.App.Key
|
||||||
region := input.Region
|
region := input.Region
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client
|
||||||
|
|
||||||
// Step 3: List applications in the organization
|
// Step 3: List applications in the organization
|
||||||
fmt.Println("\n3. Listing applications...")
|
fmt.Println("\n3. Listing applications...")
|
||||||
filter := edgeconnect.AppKey{Organization: appKey.Organization}
|
filter := v2.AppKey{Organization: appKey.Organization}
|
||||||
apps, err := edgeClient.ShowApps(ctx, filter, region)
|
apps, err := edgeClient.ShowApps(ctx, filter, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list apps: %w", err)
|
return fmt.Errorf("failed to list apps: %w", err)
|
||||||
|
|
@ -116,7 +116,7 @@ func demonstrateAppLifecycle(ctx context.Context, edgeClient *edgeconnect.Client
|
||||||
fmt.Println("\n5. Verifying deletion...")
|
fmt.Println("\n5. Verifying deletion...")
|
||||||
_, err = edgeClient.ShowApp(ctx, appKey, region)
|
_, err = edgeClient.ShowApp(ctx, appKey, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(fmt.Sprintf("%v", err), edgeconnect.ErrResourceNotFound.Error()) {
|
if strings.Contains(fmt.Sprintf("%v", err), v2.ErrResourceNotFound.Error()) {
|
||||||
fmt.Printf("✅ App successfully deleted (not found)\n")
|
fmt.Printf("✅ App successfully deleted (not found)\n")
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("unexpected error verifying deletion: %w", err)
|
return fmt.Errorf("unexpected error verifying deletion: %w", err)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue