From 05daaec70daf3dc7c5a7b70530379000e05a57a6 Mon Sep 17 00:00:00 2001 From: Waldemar Date: Tue, 30 Sep 2025 12:09:00 +0200 Subject: [PATCH] feat(sdk): Added update endpoints for app and appinst --- sdk/README.md | 4 +- sdk/edgeconnect/appinstance.go | 22 +++++ sdk/edgeconnect/appinstance_test.go | 114 +++++++++++++++++++++++ sdk/edgeconnect/apps.go | 22 +++++ sdk/edgeconnect/apps_test.go | 100 +++++++++++++++++++++ sdk/edgeconnect/types.go | 135 ++++++++++++++++++++++++++++ 6 files changed, 395 insertions(+), 2 deletions(-) diff --git a/sdk/README.md b/sdk/README.md index 5124b92..0f16b12 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -16,7 +16,7 @@ A comprehensive Go SDK for the EdgeXR Master Controller API, providing typed int ### Installation ```go -import "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/client" +import "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" ``` ### Authentication @@ -260,4 +260,4 @@ make build ## License -This SDK follows the same license as the parent edge-connect-client project. \ No newline at end of file +This SDK follows the same license as the parent edge-connect-client project. diff --git a/sdk/edgeconnect/appinstance.go b/sdk/edgeconnect/appinstance.go index 44c8b0b..8d568a8 100644 --- a/sdk/edgeconnect/appinstance.go +++ b/sdk/edgeconnect/appinstance.go @@ -108,6 +108,28 @@ func (c *Client) ShowAppInstances(ctx context.Context, appInstKey AppInstanceKey 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 { diff --git a/sdk/edgeconnect/appinstance_test.go b/sdk/edgeconnect/appinstance_test.go index c97f62a..fc8bfc4 100644 --- a/sdk/edgeconnect/appinstance_test.go +++ b/sdk/edgeconnect/appinstance_test.go @@ -216,6 +216,120 @@ func TestShowAppInstances(t *testing.T) { 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 diff --git a/sdk/edgeconnect/apps.go b/sdk/edgeconnect/apps.go index af07a3f..70f5dea 100644 --- a/sdk/edgeconnect/apps.go +++ b/sdk/edgeconnect/apps.go @@ -114,6 +114,28 @@ func (c *Client) ShowApps(ctx context.Context, appKey AppKey, region string) ([] 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 { diff --git a/sdk/edgeconnect/apps_test.go b/sdk/edgeconnect/apps_test.go index e424bbf..30531f6 100644 --- a/sdk/edgeconnect/apps_test.go +++ b/sdk/edgeconnect/apps_test.go @@ -201,6 +201,106 @@ func TestShowApps(t *testing.T) { 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 diff --git a/sdk/edgeconnect/types.go b/sdk/edgeconnect/types.go index 67ccb87..6f82d51 100644 --- a/sdk/edgeconnect/types.go +++ b/sdk/edgeconnect/types.go @@ -9,6 +9,127 @@ import ( "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 @@ -69,6 +190,7 @@ type App struct { DeploymentGenerator string `json:"deployment_generator,omitempty"` DeploymentManifest string `json:"deployment_manifest,omitempty"` RequiredOutboundConnections []SecurityRule `json:"required_outbound_connections"` + Fields []string `json:"fields,omitempty"` } // AppInstance represents a deployed application instance @@ -79,6 +201,7 @@ type AppInstance struct { Flavor Flavor `json:"flavor,omitempty"` State string `json:"state,omitempty"` PowerState string `json:"power_state,omitempty"` + Fields []string `json:"fields,omitempty"` } // Cloudlet represents edge infrastructure @@ -121,6 +244,18 @@ type NewCloudletInput struct { 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"` +} + // Response wrapper types // Response wraps a single API response