// 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/v2/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{ Region: region, } input.App.Key = appKey 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, } }