// ABOUTME: Configuration types for EdgeConnect apply command YAML parsing // ABOUTME: Defines structs that match EdgeConnectConfig.yaml schema exactly package config import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // EdgeConnectConfig represents the top-level configuration structure type EdgeConnectConfig struct { Kind string `yaml:"kind"` Metadata Metadata `yaml:"metadata"` Spec Spec `yaml:"spec"` } // Metadata contains configuration metadata type Metadata struct { Name string `yaml:"name"` AppVersion string `yaml:"appVersion"` Organization string `yaml:"organization"` } // Spec defines the application and infrastructure specification type Spec struct { K8sApp *K8sApp `yaml:"k8sApp,omitempty"` DockerApp *DockerApp `yaml:"dockerApp,omitempty"` InfraTemplate []InfraTemplate `yaml:"infraTemplate"` Network *NetworkConfig `yaml:"network,omitempty"` DeploymentStrategy string `yaml:"deploymentStrategy,omitempty"` } // K8sApp defines Kubernetes application configuration type K8sApp struct { ManifestFile string `yaml:"manifestFile"` } // DockerApp defines Docker application configuration type DockerApp struct { ManifestFile string `yaml:"manifestFile"` Image string `yaml:"image"` } // InfraTemplate defines infrastructure deployment targets type InfraTemplate struct { Region string `yaml:"region"` CloudletOrg string `yaml:"cloudletOrg"` CloudletName string `yaml:"cloudletName"` FlavorName string `yaml:"flavorName"` } // NetworkConfig defines network configuration type NetworkConfig struct { OutboundConnections []OutboundConnection `yaml:"outboundConnections"` } // OutboundConnection defines an outbound network connection type OutboundConnection struct { Protocol string `yaml:"protocol"` PortRangeMin int `yaml:"portRangeMin"` PortRangeMax int `yaml:"portRangeMax"` RemoteCIDR string `yaml:"remoteCIDR"` } // Validate performs comprehensive validation of the configuration func (c *EdgeConnectConfig) Validate() error { if c.Kind == "" { return fmt.Errorf("kind is required") } if c.Kind != "edgeconnect-deployment" { return fmt.Errorf("unsupported kind: %s, expected 'edgeconnect-deployment'", c.Kind) } if err := c.Metadata.Validate(); err != nil { return fmt.Errorf("metadata validation failed: %w", err) } if err := c.Spec.Validate(); err != nil { return fmt.Errorf("spec validation failed: %w", err) } return nil } // getDeploymentType determines the deployment type from config func (c *EdgeConnectConfig) GetDeploymentType() string { if c.Spec.IsK8sApp() { return "kubernetes" } return "docker" } // getImagePath gets the image path for the application func (c *EdgeConnectConfig) GetImagePath() string { if c.Spec.IsDockerApp() && c.Spec.DockerApp.Image != "" { return c.Spec.DockerApp.Image } // 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 == "" { return fmt.Errorf("metadata.name is required") } if strings.TrimSpace(m.Name) != m.Name { return fmt.Errorf("metadata.name cannot have leading/trailing whitespace") } if m.AppVersion == "" { return fmt.Errorf("metadata.appVersion is required") } if strings.TrimSpace(m.AppVersion) != m.AppVersion { return fmt.Errorf("metadata.appVersion cannot have leading/trailing whitespace") } if m.Organization == "" { return fmt.Errorf("metadata.organization is required") } if strings.TrimSpace(m.Organization) != m.Organization { return fmt.Errorf("metadata.Organization cannot have leading/trailing whitespace") } return nil } // Validate validates spec configuration func (s *Spec) Validate() error { // Must have either k8sApp or dockerApp, but not both if s.K8sApp == nil && s.DockerApp == nil { return fmt.Errorf("spec must define either k8sApp or dockerApp") } if s.K8sApp != nil && s.DockerApp != nil { return fmt.Errorf("spec cannot define both k8sApp and dockerApp") } // Validate app configuration if s.K8sApp != nil { if err := s.K8sApp.Validate(); err != nil { return fmt.Errorf("k8sApp validation failed: %w", err) } } if s.DockerApp != nil { if err := s.DockerApp.Validate(); err != nil { return fmt.Errorf("dockerApp validation failed: %w", err) } } // Infrastructure template is required if len(s.InfraTemplate) == 0 { return fmt.Errorf("infraTemplate is required and must contain at least one target") } // Validate each infrastructure template for i, infra := range s.InfraTemplate { if err := infra.Validate(); err != nil { return fmt.Errorf("infraTemplate[%d] validation failed: %w", i, err) } } // Validate network configuration if present if s.Network != nil { if err := s.Network.Validate(); err != nil { return fmt.Errorf("network validation failed: %w", err) } } // Validate deployment strategy if present if s.DeploymentStrategy != "" { if err := s.ValidateDeploymentStrategy(); err != nil { return fmt.Errorf("deploymentStrategy validation failed: %w", err) } } return nil } // Validate validates k8s app configuration func (k *K8sApp) Validate() error { if k.ManifestFile == "" { return fmt.Errorf("manifestFile is required") } // Check if manifest file exists if _, err := os.Stat(k.ManifestFile); os.IsNotExist(err) { return fmt.Errorf("manifestFile does not exist: %s", k.ManifestFile) } return nil } // Validate validates docker app configuration func (d *DockerApp) Validate() error { if d.Image == "" { return fmt.Errorf("image is required") } // Check if manifest file exists if specified if d.ManifestFile != "" { if _, err := os.Stat(d.ManifestFile); os.IsNotExist(err) { return fmt.Errorf("manifestFile does not exist: %s", d.ManifestFile) } } return nil } // Validate validates infrastructure template configuration func (i *InfraTemplate) Validate() error { if i.Region == "" { return fmt.Errorf("region is required") } if i.CloudletOrg == "" { return fmt.Errorf("cloudletOrg is required") } if i.CloudletName == "" { return fmt.Errorf("cloudletName is required") } if i.FlavorName == "" { return fmt.Errorf("flavorName is required") } // Validate no leading/trailing whitespace fields := map[string]string{ "region": i.Region, "cloudletOrg": i.CloudletOrg, "cloudletName": i.CloudletName, "flavorName": i.FlavorName, } for field, value := range fields { if strings.TrimSpace(value) != value { return fmt.Errorf("%s cannot have leading/trailing whitespace", field) } } return nil } // Validate validates network configuration func (n *NetworkConfig) Validate() error { if len(n.OutboundConnections) == 0 { return fmt.Errorf("outboundConnections is required when network is specified") } for i, conn := range n.OutboundConnections { if err := conn.Validate(); err != nil { return fmt.Errorf("outboundConnections[%d] validation failed: %w", i, err) } } return nil } // Validate validates outbound connection configuration func (o *OutboundConnection) Validate() error { if o.Protocol == "" { return fmt.Errorf("protocol is required") } validProtocols := map[string]bool{ "tcp": true, "udp": true, "icmp": true, } if !validProtocols[strings.ToLower(o.Protocol)] { return fmt.Errorf("protocol must be one of: tcp, udp, icmp") } if o.PortRangeMin <= 0 || o.PortRangeMin > 65535 { return fmt.Errorf("portRangeMin must be between 1 and 65535") } if o.PortRangeMax <= 0 || o.PortRangeMax > 65535 { return fmt.Errorf("portRangeMax must be between 1 and 65535") } if o.PortRangeMin > o.PortRangeMax { return fmt.Errorf("portRangeMin (%d) cannot be greater than portRangeMax (%d)", o.PortRangeMin, o.PortRangeMax) } if o.RemoteCIDR == "" { return fmt.Errorf("remoteCIDR is required") } return nil } // GetManifestPath returns the absolute path to the manifest file func (k *K8sApp) GetManifestPath(configDir string) string { if filepath.IsAbs(k.ManifestFile) { return k.ManifestFile } return filepath.Join(configDir, k.ManifestFile) } // GetManifestPath returns the absolute path to the manifest file func (d *DockerApp) GetManifestPath(configDir string) string { if d.ManifestFile == "" { return "" } if filepath.IsAbs(d.ManifestFile) { return d.ManifestFile } return filepath.Join(configDir, d.ManifestFile) } // GetManifestFile returns the manifest file path from the active app type func (s *Spec) GetManifestFile() string { if s.K8sApp != nil { return s.K8sApp.ManifestFile } if s.DockerApp != nil { return s.DockerApp.ManifestFile } return "" } // IsK8sApp returns true if this is a Kubernetes application func (s *Spec) IsK8sApp() bool { return s.K8sApp != nil } // IsDockerApp returns true if this is a Docker application func (s *Spec) IsDockerApp() bool { return s.DockerApp != nil } // ValidateDeploymentStrategy validates the deployment strategy value func (s *Spec) ValidateDeploymentStrategy() error { validStrategies := map[string]bool{ "recreate": true, "blue-green": true, // Future implementation "rolling": true, // Future implementation } strategy := strings.ToLower(strings.TrimSpace(s.DeploymentStrategy)) if !validStrategies[strategy] { return fmt.Errorf("deploymentStrategy must be one of: recreate, blue-green, rolling") } return nil } // GetDeploymentStrategy returns the deployment strategy, defaulting to "recreate" func (s *Spec) GetDeploymentStrategy() string { if s.DeploymentStrategy == "" { return "recreate" } return strings.ToLower(strings.TrimSpace(s.DeploymentStrategy)) }