edge-connect-client/internal/config/types.go

432 lines
11 KiB
Go
Raw Normal View History

// 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))
}