feat(swagger_v2): added support for the orca staging environment

This commit is contained in:
Richard Robert Reitz 2025-10-20 13:05:36 +02:00
parent 0f71239db6
commit 1413836b68
9 changed files with 186 additions and 39 deletions

View file

@ -3,6 +3,7 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -60,16 +61,23 @@ func newSDKClient() *edgeconnect.Client {
os.Exit(1) os.Exit(1)
} }
// Build options
opts := []edgeconnect.Option{
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
}
// Add logger only if debug flag is set
if debug {
logger := log.New(os.Stderr, "[DEBUG] ", log.LstdFlags)
opts = append(opts, edgeconnect.WithLogger(logger))
}
if username != "" && password != "" { if username != "" && password != "" {
return edgeconnect.NewClientWithCredentials(baseURL, username, password, return edgeconnect.NewClientWithCredentials(baseURL, username, password, opts...)
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
} }
// 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, return edgeconnect.NewClient(baseURL, opts...)
edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
} }
var appCmd = &cobra.Command{ var appCmd = &cobra.Command{

View file

@ -13,6 +13,7 @@ var (
baseURL string baseURL string
username string username string
password string password string
debug bool
) )
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
@ -39,6 +40,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&baseURL, "base-url", "", "base URL for the Edge Connect API") rootCmd.PersistentFlags().StringVar(&baseURL, "base-url", "", "base URL for the Edge Connect API")
rootCmd.PersistentFlags().StringVar(&username, "username", "", "username for authentication") rootCmd.PersistentFlags().StringVar(&username, "username", "", "username for authentication")
rootCmd.PersistentFlags().StringVar(&password, "password", "", "password for authentication") rootCmd.PersistentFlags().StringVar(&password, "password", "", "password for authentication")
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url")) viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")) viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username"))

View file

@ -7,6 +7,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"gopkg.in/yaml.v3"
) )
// EdgeConnectConfig represents the top-level configuration structure // EdgeConnectConfig represents the top-level configuration structure
@ -98,10 +100,75 @@ func (c *EdgeConnectConfig) GetImagePath() string {
if c.Spec.IsDockerApp() && c.Spec.DockerApp.Image != "" { if c.Spec.IsDockerApp() && c.Spec.DockerApp.Image != "" {
return c.Spec.DockerApp.Image return c.Spec.DockerApp.Image
} }
// Default for kubernetes apps
// For kubernetes apps, extract image from manifest
if c.Spec.IsK8sApp() && c.Spec.K8sApp.ManifestFile != "" {
if image, err := extractImageFromK8sManifest(c.Spec.K8sApp.ManifestFile); err == nil && image != "" {
return image
}
}
// Fallback default for kubernetes apps
return "https://registry-1.docker.io/library/nginx:latest" return "https://registry-1.docker.io/library/nginx:latest"
} }
// extractImageFromK8sManifest extracts the container image from a Kubernetes manifest
func extractImageFromK8sManifest(manifestPath string) (string, error) {
data, err := os.ReadFile(manifestPath)
if err != nil {
return "", fmt.Errorf("failed to read manifest: %w", err)
}
// Parse multi-document YAML
decoder := yaml.NewDecoder(strings.NewReader(string(data)))
for {
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
break // End of documents or error
}
// Check if this is a Deployment
kind, ok := doc["kind"].(string)
if !ok || kind != "Deployment" {
continue
}
// Navigate to spec.template.spec.containers[0].image
spec, ok := doc["spec"].(map[string]interface{})
if !ok {
continue
}
template, ok := spec["template"].(map[string]interface{})
if !ok {
continue
}
templateSpec, ok := template["spec"].(map[string]interface{})
if !ok {
continue
}
containers, ok := templateSpec["containers"].([]interface{})
if !ok || len(containers) == 0 {
continue
}
firstContainer, ok := containers[0].(map[string]interface{})
if !ok {
continue
}
image, ok := firstContainer["image"].(string)
if ok && image != "" {
return image, nil
}
}
return "", fmt.Errorf("no image found in Deployment manifest")
}
// Validate validates metadata fields // Validate validates metadata fields
func (m *Metadata) Validate() error { func (m *Metadata) Validate() error {
if m.Name == "" { if m.Name == "" {

View file

@ -4,9 +4,11 @@
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"
@ -164,18 +166,17 @@ func (c *Client) RefreshAppInstance(ctx context.Context, appInstKey AppInstanceK
return nil return nil
} }
// DeleteAppInstance removes an application instance from the specified region // DeleteAppInstance removes an application instance
// 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"
filter := AppInstanceFilter{ input := DeleteAppInstanceInput{
AppInstance: AppInstance{Key: appInstKey}, Key: appInstKey,
Region: region,
} }
resp, err := transport.Call(ctx, "POST", url, filter) resp, err := transport.Call(ctx, "POST", url, input)
if err != nil { if err != nil {
return fmt.Errorf("DeleteAppInstance failed: %w", err) return fmt.Errorf("DeleteAppInstance failed: %w", err)
} }
@ -194,13 +195,29 @@ 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(resp.Body, func(line []byte) error { parseErr := sdkhttp.ParseJSONLines(io.NopCloser(bytes.NewReader(bodyBytes)), 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 != "" {

View file

@ -4,6 +4,7 @@
package edgeconnect package edgeconnect
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -142,12 +143,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"
filter := AppFilter{ input := DeleteAppInput{
App: App{Key: appKey}, Key: appKey,
Region: region, Region: region,
} }
resp, err := transport.Call(ctx, "POST", url, filter) resp, err := transport.Call(ctx, "POST", url, input)
if err != nil { if err != nil {
return fmt.Errorf("DeleteApp failed: %w", err) return fmt.Errorf("DeleteApp failed: %w", err)
} }
@ -166,9 +167,27 @@ 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 {
var responses []Response[App] bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
parseErr := sdkhttp.ParseJSONLines(resp.Body, func(line []byte) error { // 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] var response Response[App]
if err := json.Unmarshal(line, &response); err != nil { if err := json.Unmarshal(line, &response); err != nil {
return err return err
@ -182,9 +201,6 @@ 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)

View file

@ -184,24 +184,33 @@ 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"`
Flavor Flavor `json:"flavor,omitempty"` CloudletLoc CloudletLoc `json:"cloudlet_loc,omitempty"`
State string `json:"state,omitempty"` Flavor Flavor `json:"flavor,omitempty"`
PowerState string `json:"power_state,omitempty"` State string `json:"state,omitempty"`
Fields []string `json:"fields,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 // Cloudlet represents edge infrastructure
@ -224,6 +233,12 @@ 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
@ -256,6 +271,17 @@ 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

View file

@ -3,8 +3,8 @@
kind: edgeconnect-deployment kind: edgeconnect-deployment
metadata: metadata:
name: "edge-app-demo" # name could be used for appName name: "edge-app-demo" # name could be used for appName
appVersion: "1.0.0" appVersion: "1"
organization: "edp2" organization: "edp2-orca"
spec: spec:
# dockerApp: # Docker is OBSOLETE # dockerApp: # Docker is OBSOLETE
# appVersion: "1.0.0" # appVersion: "1.0.0"
@ -13,10 +13,10 @@ spec:
k8sApp: k8sApp:
manifestFile: "./k8s-deployment.yaml" manifestFile: "./k8s-deployment.yaml"
infraTemplate: infraTemplate:
- region: "EU" - region: "US"
cloudletOrg: "TelekomOP" cloudletOrg: "TelekomOp"
cloudletName: "Munich" cloudletName: "gardener-shepherd-test"
flavorName: "EU.small" flavorName: "defualt"
network: network:
outboundConnections: outboundConnections:
- protocol: "tcp" - protocol: "tcp"

View file

@ -32,7 +32,7 @@ spec:
volumes: volumes:
containers: containers:
- name: edgeconnect-coder - name: edgeconnect-coder
image: nginx:latest image: edp.buildth.ing/devfw-cicd/fibonacci_pipeline:edge_platform_demo
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 80 - containerPort: 80

View file

@ -98,10 +98,12 @@ func NewTransport(opts RetryOptions, auth AuthProvider, logger Logger) *Transpor
// Call executes an HTTP request with retry logic and returns typed response // Call executes an HTTP request with retry logic and returns typed response
func (t *Transport) Call(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { func (t *Transport) Call(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
var reqBody io.Reader var reqBody io.Reader
var jsonData []byte
// Marshal request body if provided // Marshal request body if provided
if body != nil { if body != nil {
jsonData, err := json.Marshal(body) var err error
jsonData, err = json.Marshal(body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err) return nil, fmt.Errorf("failed to marshal request body: %w", err)
} }
@ -127,8 +129,16 @@ func (t *Transport) Call(ctx context.Context, method, url string, body interface
// Log request // Log request
if t.logger != nil { if t.logger != nil {
t.logger.Printf("HTTP %s %s", method, url) t.logger.Printf("=== HTTP REQUEST ===")
t.logger.Printf("BODY %s", reqBody) t.logger.Printf("%s %s", method, url)
if len(jsonData) > 0 {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, jsonData, "", " "); err == nil {
t.logger.Printf("Request Body:\n%s", prettyJSON.String())
} else {
t.logger.Printf("Request Body: %s", string(jsonData))
}
}
} }
// Execute request // Execute request
@ -139,7 +149,8 @@ func (t *Transport) Call(ctx context.Context, method, url string, body interface
// Log response // Log response
if t.logger != nil { if t.logger != nil {
t.logger.Printf("HTTP %s %s -> %d", method, url, resp.StatusCode) t.logger.Printf("=== HTTP RESPONSE ===")
t.logger.Printf("%s %s -> %d", method, url, resp.StatusCode)
} }
return resp, nil return resp, nil