diff --git a/cmd/app.go b/cmd/app.go index a9f187f..0273896 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "log" "net/http" "net/url" "os" @@ -60,16 +61,23 @@ func newSDKClient() *edgeconnect.Client { 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 != "" { - return edgeconnect.NewClientWithCredentials(baseURL, username, password, - edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}), - ) + return edgeconnect.NewClientWithCredentials(baseURL, username, password, opts...) } // Fallback to no auth for now - in production should require auth - return edgeconnect.NewClient(baseURL, - edgeconnect.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}), - ) + return edgeconnect.NewClient(baseURL, opts...) } var appCmd = &cobra.Command{ diff --git a/cmd/root.go b/cmd/root.go index 480d8f5..6fa2dd6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,7 @@ var ( baseURL string username string password string + debug bool ) // 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(&username, "username", "", "username 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("username", rootCmd.PersistentFlags().Lookup("username")) diff --git a/internal/config/types.go b/internal/config/types.go index 9b365dd..60128d4 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -7,6 +7,8 @@ import ( "os" "path/filepath" "strings" + + "gopkg.in/yaml.v3" ) // EdgeConnectConfig represents the top-level configuration structure @@ -98,10 +100,75 @@ func (c *EdgeConnectConfig) GetImagePath() string { if c.Spec.IsDockerApp() && 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" } +// 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 func (m *Metadata) Validate() error { if m.Name == "" { diff --git a/sdk/edgeconnect/appinstance.go b/sdk/edgeconnect/appinstance.go index a26f45c..ec4751a 100644 --- a/sdk/edgeconnect/appinstance.go +++ b/sdk/edgeconnect/appinstance.go @@ -4,9 +4,11 @@ package edgeconnect import ( + "bytes" "context" "encoding/json" "fmt" + "io" "net/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 } -// DeleteAppInstance removes an application instance from the specified region +// DeleteAppInstance removes an application instance // Maps to POST /auth/ctrl/DeleteAppInst func (c *Client) DeleteAppInstance(ctx context.Context, appInstKey AppInstanceKey, region string) error { transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/DeleteAppInst" - filter := AppInstanceFilter{ - AppInstance: AppInstance{Key: appInstKey}, - Region: region, + input := DeleteAppInstanceInput{ + Key: appInstKey, } - resp, err := transport.Call(ctx, "POST", url, filter) + resp, err := transport.Call(ctx, "POST", url, input) if err != nil { 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 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 messages []string var hasError bool var errorCode int 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) var resultResp ResultResponse if err := json.Unmarshal(line, &resultResp); err == nil && resultResp.Result.Message != "" { diff --git a/sdk/edgeconnect/apps.go b/sdk/edgeconnect/apps.go index 70f5dea..7010070 100644 --- a/sdk/edgeconnect/apps.go +++ b/sdk/edgeconnect/apps.go @@ -4,6 +4,7 @@ package edgeconnect import ( + "bytes" "context" "encoding/json" "fmt" @@ -142,12 +143,12 @@ func (c *Client) DeleteApp(ctx context.Context, appKey AppKey, region string) er transport := c.getTransport() url := c.BaseURL + "/api/v1/auth/ctrl/DeleteApp" - filter := AppFilter{ - App: App{Key: appKey}, + input := DeleteAppInput{ + Key: appKey, Region: region, } - resp, err := transport.Call(ctx, "POST", url, filter) + resp, err := transport.Call(ctx, "POST", url, input) if err != nil { 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 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] if err := json.Unmarshal(line, &response); err != nil { return err @@ -182,9 +201,6 @@ func (c *Client) parseStreamingResponse(resp *http.Response, result interface{}) } // Extract data from responses - var apps []App - var messages []string - for _, response := range responses { if response.HasData() { apps = append(apps, response.Data) diff --git a/sdk/edgeconnect/types.go b/sdk/edgeconnect/types.go index 7fd39fc..ffd5550 100644 --- a/sdk/edgeconnect/types.go +++ b/sdk/edgeconnect/types.go @@ -184,24 +184,33 @@ type App struct { Deployment string `json:"deployment,omitempty"` ImageType string `json:"image_type,omitempty"` ImagePath string `json:"image_path,omitempty"` + AccessPorts string `json:"access_ports,omitempty"` AllowServerless bool `json:"allow_serverless,omitempty"` DefaultFlavor Flavor `json:"defaultFlavor,omitempty"` ServerlessConfig interface{} `json:"serverless_config,omitempty"` DeploymentGenerator string `json:"deployment_generator,omitempty"` DeploymentManifest string `json:"deployment_manifest,omitempty"` 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"` } // AppInstance represents a deployed application instance type AppInstance struct { - msg `json:",inline"` - Key AppInstanceKey `json:"key"` - AppKey AppKey `json:"app_key,omitempty"` - Flavor Flavor `json:"flavor,omitempty"` - State string `json:"state,omitempty"` - PowerState string `json:"power_state,omitempty"` - Fields []string `json:"fields,omitempty"` + msg `json:",inline"` + Key AppInstanceKey `json:"key"` + AppKey AppKey `json:"app_key,omitempty"` + CloudletLoc CloudletLoc `json:"cloudlet_loc,omitempty"` + Flavor Flavor `json:"flavor,omitempty"` + State string `json:"state,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 @@ -224,6 +233,12 @@ type Location struct { 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 // NewAppInput represents input for creating an application @@ -256,6 +271,17 @@ type UpdateAppInstanceInput struct { 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 wraps a single API response diff --git a/sdk/examples/comprehensive/EdgeConnectConfig.yaml b/sdk/examples/comprehensive/EdgeConnectConfig.yaml index b45abc4..af41eaf 100644 --- a/sdk/examples/comprehensive/EdgeConnectConfig.yaml +++ b/sdk/examples/comprehensive/EdgeConnectConfig.yaml @@ -3,8 +3,8 @@ kind: edgeconnect-deployment metadata: name: "edge-app-demo" # name could be used for appName - appVersion: "1.0.0" - organization: "edp2" + appVersion: "1" + organization: "edp2-orca" spec: # dockerApp: # Docker is OBSOLETE # appVersion: "1.0.0" @@ -13,10 +13,10 @@ spec: k8sApp: manifestFile: "./k8s-deployment.yaml" infraTemplate: - - region: "EU" - cloudletOrg: "TelekomOP" - cloudletName: "Munich" - flavorName: "EU.small" + - region: "US" + cloudletOrg: "TelekomOp" + cloudletName: "gardener-shepherd-test" + flavorName: "defualt" network: outboundConnections: - protocol: "tcp" diff --git a/sdk/examples/comprehensive/k8s-deployment.yaml b/sdk/examples/comprehensive/k8s-deployment.yaml index 348b6f8..2a0a741 100644 --- a/sdk/examples/comprehensive/k8s-deployment.yaml +++ b/sdk/examples/comprehensive/k8s-deployment.yaml @@ -32,7 +32,7 @@ spec: volumes: containers: - name: edgeconnect-coder - image: nginx:latest + image: edp.buildth.ing/devfw-cicd/fibonacci_pipeline:edge_platform_demo imagePullPolicy: Always ports: - containerPort: 80 diff --git a/sdk/internal/http/transport.go b/sdk/internal/http/transport.go index 54e853c..c3bbab1 100644 --- a/sdk/internal/http/transport.go +++ b/sdk/internal/http/transport.go @@ -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 func (t *Transport) Call(ctx context.Context, method, url string, body interface{}) (*http.Response, error) { var reqBody io.Reader + var jsonData []byte // Marshal request body if provided if body != nil { - jsonData, err := json.Marshal(body) + var err error + jsonData, err = json.Marshal(body) if err != nil { 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 if t.logger != nil { - t.logger.Printf("HTTP %s %s", method, url) - t.logger.Printf("BODY %s", reqBody) + t.logger.Printf("=== HTTP REQUEST ===") + 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 @@ -139,7 +149,8 @@ func (t *Transport) Call(ctx context.Context, method, url string, body interface // Log response 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