229 lines
6 KiB
Go
229 lines
6 KiB
Go
// ABOUTME: Application lifecycle management APIs for EdgeXR Master Controller
|
|
// ABOUTME: Provides typed methods for creating, querying, and deleting applications
|
|
|
|
package client
|
|
|
|
import (
|
|
"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
|
|
}
|
|
|
|
// 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"
|
|
|
|
filter := AppFilter{
|
|
App: App{Key: appKey},
|
|
Region: region,
|
|
}
|
|
|
|
resp, err := transport.Call(ctx, "POST", url, filter)
|
|
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 {
|
|
var responses []Response[App]
|
|
|
|
parseErr := sdkhttp.ParseJSONLines(resp.Body, 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
|
|
var apps []App
|
|
var messages []string
|
|
|
|
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,
|
|
}
|
|
}
|