feat(sdk): Added update endpoints for app and appinst
This commit is contained in:
parent
5d6fd8fc59
commit
240a9028b3
6 changed files with 395 additions and 2 deletions
|
|
@ -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.
|
||||
This SDK follows the same license as the parent edge-connect-client project.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue