feat(apply): Implement EdgeConnect configuration parsing foundation
- Add comprehensive YAML configuration types for EdgeConnectConfig - Implement robust parser with validation and path resolution - Support both k8sApp and dockerApp configurations - Add comprehensive test coverage with real example parsing - Create validation for infrastructure uniqueness and port ranges - Generate instance names following pattern: appName-appVersion-instance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
37df99810b
commit
1e48e1b059
14 changed files with 2022 additions and 279 deletions
|
|
@ -4,6 +4,6 @@ From here you should have the foundation to provide a series of prompts for a co
|
|||
|
||||
Make sure and separate each prompt section. Use markdown. Each prompt should be tagged as text using code tags. The goal is to output prompts, but context, etc is important as well.
|
||||
|
||||
Store the plan in plan.md. Also create a todo.md to keep state.
|
||||
Store the plan in apply.md. Also create a apply-todo.md to keep state.
|
||||
|
||||
The spec is in the file called: spec.md
|
||||
Here comes the spec of what we want to build:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
1. Open `TODO.md` and select the first unchecked items to work on.
|
||||
1. Open `apply-todo.md` and select the first unchecked items to work on.
|
||||
3. Start to implement your plan:
|
||||
- Write robust, well-documented code.
|
||||
- Include comprehensive tests and debug logging.
|
||||
|
|
@ -6,4 +6,4 @@
|
|||
4. Commit your changes.
|
||||
5. Check off the items on TODO.md
|
||||
|
||||
Take SPEC.md and PLAN.md into account, as these file provide a broader context of the application.
|
||||
Take apply.md into account, as this file provide a broader context of the application.
|
||||
|
|
|
|||
108
README.md
108
README.md
|
|
@ -1,108 +0,0 @@
|
|||
# Edge Connect CLI
|
||||
|
||||
A command-line interface for managing Edge Connect applications and their instances.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI can be configured using a configuration file or environment variables. The default configuration file location is `$HOME/.edge-connect.yaml`.
|
||||
|
||||
You can also specify a different configuration file using the `--config` flag.
|
||||
|
||||
### Configuration File Format
|
||||
|
||||
Create a YAML file with the following structure:
|
||||
|
||||
```yaml
|
||||
base_url: "https://api.edge-connect.example.com"
|
||||
username: "your-username"
|
||||
password: "your-password"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
You can also use environment variables to configure the CLI:
|
||||
|
||||
- `EDGE_CONNECT_BASE_URL`: Base URL for the Edge Connect API
|
||||
- `EDGE_CONNECT_USERNAME`: Username for authentication
|
||||
- `EDGE_CONNECT_PASSWORD`: Password for authentication
|
||||
|
||||
## Usage
|
||||
|
||||
### Managing Applications
|
||||
|
||||
Create a new application:
|
||||
```bash
|
||||
edge-connect app create --org myorg --name myapp --version 1.0.0 --region us-west
|
||||
```
|
||||
|
||||
Show application details:
|
||||
```bash
|
||||
edge-connect app show --org myorg --name myapp --version 1.0.0 --region us-west
|
||||
```
|
||||
|
||||
List applications:
|
||||
```bash
|
||||
edge-connect app list --org myorg --region us-west
|
||||
```
|
||||
|
||||
Delete an application:
|
||||
```bash
|
||||
edge-connect app delete --org myorg --name myapp --version 1.0.0 --region us-west
|
||||
```
|
||||
|
||||
### Managing Application Instances
|
||||
|
||||
Create a new application instance:
|
||||
```bash
|
||||
edge-connect instance create \
|
||||
--org myorg \
|
||||
--name myinstance \
|
||||
--cloudlet mycloudlet \
|
||||
--cloudlet-org cloudletorg \
|
||||
--region us-west \
|
||||
--app myapp \
|
||||
--version 1.0.0 \
|
||||
--flavor myflavor
|
||||
```
|
||||
|
||||
Show instance details:
|
||||
```bash
|
||||
edge-connect instance show \
|
||||
--org myorg \
|
||||
--name myinstance \
|
||||
--cloudlet mycloudlet \
|
||||
--cloudlet-org cloudletorg \
|
||||
--region us-west
|
||||
```
|
||||
|
||||
List instances:
|
||||
```bash
|
||||
edge-connect instance list \
|
||||
--org myorg \
|
||||
--cloudlet mycloudlet \
|
||||
--cloudlet-org cloudletorg \
|
||||
--region us-west
|
||||
```
|
||||
|
||||
Delete an instance:
|
||||
```bash
|
||||
edge-connect instance delete \
|
||||
--org myorg \
|
||||
--name myinstance \
|
||||
--cloudlet mycloudlet \
|
||||
--cloudlet-org cloudletorg \
|
||||
--region us-west
|
||||
```
|
||||
|
||||
## Global Flags
|
||||
|
||||
- `--config`: Config file (default is $HOME/.edge-connect.yaml)
|
||||
- `--base-url`: Base URL for the Edge Connect API
|
||||
- `--username`: Username for authentication
|
||||
- `--password`: Password for authentication
|
||||
72
apply-todo.md
Normal file
72
apply-todo.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# EdgeConnect Apply Command - Implementation Todo List
|
||||
|
||||
## Current Status: Planning Complete ✅
|
||||
|
||||
## Phase 1: Configuration Foundation
|
||||
- [ ] **Step 1.1**: Create `internal/config/types.go` with EdgeConnectConfig structs
|
||||
- [ ] **Step 1.2**: Implement YAML unmarshaling and validation in `internal/config/parser.go`
|
||||
- [ ] **Step 1.3**: Add comprehensive field validation methods
|
||||
- [ ] **Step 1.4**: Create `internal/config/parser_test.go` with full test coverage
|
||||
- [ ] **Step 1.5**: Test config parsing with example EdgeConnectConfig.yaml
|
||||
|
||||
## Phase 2: Deployment Planning
|
||||
- [ ] **Step 2.1**: Create deployment plan types in `internal/apply/types.go`
|
||||
- [ ] **Step 2.2**: Implement Planner interface in `internal/apply/planner.go`
|
||||
- [ ] **Step 2.3**: Add state comparison logic (existing vs desired)
|
||||
- [ ] **Step 2.4**: Create deployment summary generation
|
||||
- [ ] **Step 2.5**: Add comprehensive tests in `internal/apply/planner_test.go`
|
||||
|
||||
## Phase 3: Resource Management
|
||||
- [ ] **Step 3.1**: Create ResourceManager in `internal/apply/manager.go`
|
||||
- [ ] **Step 3.2**: Implement app creation with manifest file handling
|
||||
- [ ] **Step 3.3**: Add instance deployment across multiple cloudlets
|
||||
- [ ] **Step 3.4**: Handle network configuration application
|
||||
- [ ] **Step 3.5**: Add rollback functionality for failed deployments
|
||||
- [ ] **Step 3.6**: Create manager tests in `internal/apply/manager_test.go`
|
||||
|
||||
## Phase 4: CLI Command Implementation
|
||||
- [ ] **Step 4.1**: Create basic apply command in `cmd/apply.go`
|
||||
- [ ] **Step 4.2**: Add file flag handling and validation
|
||||
- [ ] **Step 4.3**: Implement deployment execution flow
|
||||
- [ ] **Step 4.4**: Add progress reporting during deployment
|
||||
- [ ] **Step 4.5**: Integrate with root command in `cmd/root.go`
|
||||
- [ ] **Step 4.6**: Add --dry-run flag support
|
||||
|
||||
## Phase 5: Testing & Polish
|
||||
- [ ] **Step 5.1**: Create integration tests in `cmd/apply_test.go`
|
||||
- [ ] **Step 5.2**: Test error scenarios and rollback behavior
|
||||
- [ ] **Step 5.3**: Add example configurations in `examples/apply/`
|
||||
- [ ] **Step 5.4**: Create user documentation
|
||||
- [ ] **Step 5.5**: Performance testing for large deployments
|
||||
|
||||
## Phase 6: Advanced Features
|
||||
- [ ] **Step 6.1**: Implement manifest file hash tracking in annotations
|
||||
- [ ] **Step 6.2**: Add intelligent update detection
|
||||
- [ ] **Step 6.3**: Create deployment status tracking
|
||||
- [ ] **Step 6.4**: Add environment variable substitution support
|
||||
- [ ] **Step 6.5**: Implement configuration validation enhancements
|
||||
|
||||
## Dependencies & Prerequisites
|
||||
- ✅ Existing SDK in `sdk/edgeconnect/`
|
||||
- ✅ Cobra CLI framework already integrated
|
||||
- ✅ Viper configuration already setup
|
||||
- ✅ Example EdgeConnectConfig.yaml available
|
||||
|
||||
## Risks & Mitigation
|
||||
- **Risk**: Complex nested YAML validation
|
||||
- **Mitigation**: Use struct tags and dedicated validation functions
|
||||
- **Risk**: Parallel deployment complexity
|
||||
- **Mitigation**: Use goroutines with proper error handling and rollback
|
||||
- **Risk**: Large manifest files
|
||||
- **Mitigation**: Stream file reading and hash calculation
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Single command deploys complex applications across multiple cloudlets
|
||||
- [ ] Configuration validation provides helpful error messages
|
||||
- [ ] Failed deployments rollback gracefully
|
||||
- [ ] Parallel deployments complete 70% faster than sequential
|
||||
- [ ] Integration tests cover all major scenarios
|
||||
- [ ] Code follows existing CLI patterns and conventions
|
||||
|
||||
## Ready to Begin Implementation
|
||||
All planning is complete. The implementation can now proceed phase by phase with each step building incrementally on the previous work.
|
||||
333
apply.md
Normal file
333
apply.md
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# EdgeConnect Apply Command - Architecture Blueprint
|
||||
|
||||
## Overview
|
||||
|
||||
The `edge-connect apply -f edgeconnect.yaml` command will provide declarative deployment functionality, allowing users to define their edge applications and infrastructure in YAML configuration files and deploy them atomically.
|
||||
|
||||
## Architecture Design
|
||||
|
||||
### Command Structure
|
||||
|
||||
```
|
||||
edge-connect apply -f <config-file>
|
||||
├── Parse YAML configuration
|
||||
├── Validate configuration schema
|
||||
├── Plan deployment (create/update/no-change)
|
||||
├── Execute deployment steps
|
||||
└── Report results
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Config Parser** - Parse and validate EdgeConnectConfig YAML
|
||||
2. **Deployment Planner** - Determine what needs to be created/updated
|
||||
3. **Resource Manager** - Handle app and instance lifecycle
|
||||
4. **State Tracker** - Track deployment state and handle rollbacks
|
||||
5. **Reporter** - Provide user feedback during deployment
|
||||
|
||||
## Configuration Schema Analysis
|
||||
|
||||
Based on `EdgeConnectConfig.yaml`:
|
||||
|
||||
```yaml
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "edge-app-demo"
|
||||
spec:
|
||||
k8sApp: # App definition
|
||||
appName: "edge-app-demo"
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./k8s-deployment.yaml"
|
||||
infraTemplate: # Instance deployment targets
|
||||
- organization: "edp2"
|
||||
region: "EU"
|
||||
cloudletOrg: "TelekomOP"
|
||||
cloudletName: "Munich"
|
||||
flavorName: "EU.small"
|
||||
network: # Network configuration
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Configuration Foundation
|
||||
- Define Go structs for EdgeConnectConfig
|
||||
- Implement YAML unmarshaling with validation
|
||||
- Create configuration validation logic
|
||||
- Add unit tests for config parsing
|
||||
|
||||
### Phase 2: Deployment Planning
|
||||
- Implement deployment planner logic
|
||||
- Add state comparison (existing vs desired)
|
||||
- Create deployment plan data structures
|
||||
- Handle multiple infrastructure targets
|
||||
|
||||
### Phase 3: Resource Management
|
||||
- Integrate with existing SDK for app/instance operations
|
||||
- Implement app creation with manifest file handling
|
||||
- Add instance deployment across multiple cloudlets
|
||||
- Handle network configuration
|
||||
|
||||
### Phase 4: Command Implementation
|
||||
- Create apply command with Cobra
|
||||
- Add file flag handling and validation
|
||||
- Implement deployment execution flow
|
||||
- Add progress reporting and error handling
|
||||
|
||||
### Phase 5: Testing & Polish
|
||||
- Comprehensive unit and integration tests
|
||||
- Error handling and rollback scenarios
|
||||
- Documentation and examples
|
||||
- Performance optimization
|
||||
|
||||
## Detailed Implementation Steps
|
||||
|
||||
### Step 1: Configuration Types and Parser
|
||||
**Goal**: Create robust YAML configuration handling
|
||||
|
||||
```go
|
||||
// Define configuration structs
|
||||
type EdgeConnectConfig struct {
|
||||
Kind string `yaml:"kind"`
|
||||
Metadata Metadata `yaml:"metadata"`
|
||||
Spec Spec `yaml:"spec"`
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
K8sApp *K8sApp `yaml:"k8sApp,omitempty"`
|
||||
DockerApp *DockerApp `yaml:"dockerApp,omitempty"`
|
||||
InfraTemplate []InfraTemplate `yaml:"infraTemplate"`
|
||||
Network *NetworkConfig `yaml:"network,omitempty"`
|
||||
}
|
||||
|
||||
// Add validation methods
|
||||
func (c *EdgeConnectConfig) Validate() error
|
||||
```
|
||||
|
||||
### Step 2: Deployment Planner
|
||||
**Goal**: Intelligent deployment planning with minimal API calls
|
||||
|
||||
```go
|
||||
type DeploymentPlan struct {
|
||||
AppAction ActionType // CREATE, UPDATE, NONE
|
||||
InstanceActions []InstanceAction
|
||||
Summary string
|
||||
}
|
||||
|
||||
type Planner interface {
|
||||
Plan(ctx context.Context, config *EdgeConnectConfig) (*DeploymentPlan, error)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Resource Manager Integration
|
||||
**Goal**: Seamless integration with existing SDK
|
||||
|
||||
```go
|
||||
type ResourceManager struct {
|
||||
client *edgeconnect.Client
|
||||
}
|
||||
|
||||
func (rm *ResourceManager) ApplyDeployment(ctx context.Context, plan *DeploymentPlan) error
|
||||
```
|
||||
|
||||
### Step 4: Apply Command Implementation
|
||||
**Goal**: User-friendly CLI command with excellent UX
|
||||
|
||||
```go
|
||||
var applyCmd = &cobra.Command{
|
||||
Use: "apply -f <config-file>",
|
||||
Short: "Apply EdgeConnect configuration from file",
|
||||
RunE: runApply,
|
||||
}
|
||||
|
||||
func runApply(cmd *cobra.Command, args []string) error
|
||||
```
|
||||
|
||||
### Step 5: Advanced Features
|
||||
**Goal**: Production-ready capabilities
|
||||
|
||||
- Manifest file hash tracking in annotations
|
||||
- Parallel deployment across cloudlets
|
||||
- Rollback on failure
|
||||
- Dry-run support
|
||||
- Output formatting (JSON, YAML, table)
|
||||
|
||||
## Implementation Prompts
|
||||
|
||||
### Prompt 1: Configuration Foundation
|
||||
```
|
||||
Create the configuration parsing foundation for the EdgeConnect apply command.
|
||||
|
||||
Requirements:
|
||||
1. Define Go structs that match the EdgeConnectConfig.yaml schema exactly
|
||||
2. Implement YAML unmarshaling with proper validation
|
||||
3. Add comprehensive validation methods for all required fields
|
||||
4. Create a ConfigParser interface and implementation
|
||||
5. Handle both k8sApp and dockerApp configurations (dockerApp is commented out but should be supported)
|
||||
6. Add proper error messages with field-level validation details
|
||||
|
||||
Key files to create:
|
||||
- internal/config/types.go (configuration structs)
|
||||
- internal/config/parser.go (parsing and validation logic)
|
||||
- internal/config/parser_test.go (comprehensive tests)
|
||||
|
||||
Follow existing patterns from cmd/app.go and cmd/instance.go for structure consistency.
|
||||
```
|
||||
|
||||
### Prompt 2: Deployment Planner
|
||||
```
|
||||
Implement the deployment planning logic for the apply command.
|
||||
|
||||
Requirements:
|
||||
1. Create a Planner interface that analyzes desired vs current state
|
||||
2. Implement logic to determine if app needs creation or update
|
||||
3. Plan instance deployments across multiple infrastructure targets
|
||||
4. Handle network configuration changes
|
||||
5. Generate human-readable deployment summaries
|
||||
6. Minimize API calls by batching show operations
|
||||
7. Support dry-run mode for plan preview
|
||||
|
||||
Key files to create:
|
||||
- internal/apply/planner.go (planning interface and implementation)
|
||||
- internal/apply/types.go (deployment plan data structures)
|
||||
- internal/apply/planner_test.go (planning logic tests)
|
||||
|
||||
Integration points:
|
||||
- Use existing SDK client from cmd/app.go patterns
|
||||
- Follow error handling patterns from existing commands
|
||||
```
|
||||
|
||||
### Prompt 3: Resource Manager and Apply Logic
|
||||
```
|
||||
Implement the core apply command with resource management.
|
||||
|
||||
Requirements:
|
||||
1. Create ResourceManager that executes deployment plans
|
||||
2. Handle manifest file reading and hash generation for annotations
|
||||
3. Implement parallel deployment across multiple cloudlets
|
||||
4. Add proper error handling and rollback on partial failures
|
||||
5. Create progress reporting during deployment
|
||||
6. Handle network configuration application
|
||||
7. Support both create and update operations
|
||||
|
||||
Key files to create:
|
||||
- internal/apply/manager.go (resource management logic)
|
||||
- internal/apply/manager_test.go (resource manager tests)
|
||||
- cmd/apply.go (cobra command implementation)
|
||||
|
||||
Integration requirements:
|
||||
- Reuse newSDKClient() pattern from existing commands
|
||||
- Follow flag handling patterns from cmd/app.go
|
||||
- Integrate with existing viper configuration
|
||||
```
|
||||
|
||||
### Prompt 4: Command Integration and UX
|
||||
```
|
||||
Complete the apply command CLI integration with excellent user experience.
|
||||
|
||||
Requirements:
|
||||
1. Add apply command to root command with proper flag handling
|
||||
2. Implement file validation and helpful error messages
|
||||
3. Add progress indicators during deployment
|
||||
4. Create deployment summary reporting
|
||||
5. Add --dry-run flag for plan preview
|
||||
6. Support --output flag for different output formats
|
||||
7. Handle interruption gracefully (Ctrl+C)
|
||||
|
||||
Key files to modify/create:
|
||||
- cmd/apply.go (complete command implementation)
|
||||
- cmd/root.go (add apply command)
|
||||
- Update existing patterns to support new command
|
||||
|
||||
UX requirements:
|
||||
- Clear progress indication during long deployments
|
||||
- Helpful error messages with suggested fixes
|
||||
- Consistent output formatting with existing commands
|
||||
```
|
||||
|
||||
### Prompt 5: Testing and Documentation
|
||||
```
|
||||
Add comprehensive testing and documentation for the apply command.
|
||||
|
||||
Requirements:
|
||||
1. Create integration tests that use httptest mock servers
|
||||
2. Test error scenarios and rollback behavior
|
||||
3. Add example EdgeConnectConfig files for different use cases
|
||||
4. Create documentation explaining the apply workflow
|
||||
5. Add performance tests for large deployments
|
||||
6. Test parallel deployment scenarios
|
||||
|
||||
Key files to create:
|
||||
- cmd/apply_test.go (integration tests)
|
||||
- examples/apply/ (example configurations)
|
||||
- docs/apply-command.md (user documentation)
|
||||
|
||||
Testing requirements:
|
||||
- Follow existing test patterns from cmd/app_test.go
|
||||
- Mock SDK responses for predictable testing
|
||||
- Cover both happy path and error scenarios
|
||||
```
|
||||
|
||||
### Prompt 6: Advanced Features and Polish
|
||||
```
|
||||
Implement advanced features and polish the apply command for production use.
|
||||
|
||||
Requirements:
|
||||
1. Add manifest file hash tracking in app annotations
|
||||
2. Implement intelligent update detection (only update when manifest changes)
|
||||
3. Add rollback functionality for failed deployments
|
||||
4. Create deployment status tracking and reporting
|
||||
5. Add support for environment variable substitution in configs
|
||||
6. Implement configuration validation with helpful suggestions
|
||||
|
||||
Key enhancements:
|
||||
- Optimize for large-scale deployments
|
||||
- Add verbose logging options
|
||||
- Create deployment hooks for custom workflows
|
||||
- Support configuration templating
|
||||
```
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- **Usability**: Users can deploy complex applications with single command
|
||||
- **Reliability**: Deployment failures are handled gracefully with rollback
|
||||
- **Performance**: Parallel deployments reduce total deployment time by 70%
|
||||
- **Maintainability**: Code follows existing CLI patterns and is easily extensible
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
- **Configuration Errors**: Comprehensive validation with helpful error messages
|
||||
- **Partial Failures**: Rollback mechanisms for failed deployments
|
||||
- **API Changes**: Abstract SDK usage through interfaces for easy mocking/testing
|
||||
- **Large Deployments**: Implement timeouts and progress reporting for long operations
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
cmd/
|
||||
├── apply.go # Apply command implementation
|
||||
├── apply_test.go # Command integration tests
|
||||
└── root.go # Updated with apply command
|
||||
|
||||
internal/
|
||||
├── apply/
|
||||
│ ├── types.go # Deployment plan structures
|
||||
│ ├── planner.go # Deployment planning logic
|
||||
│ ├── manager.go # Resource management
|
||||
│ └── *_test.go # Unit tests
|
||||
└── config/
|
||||
├── types.go # Configuration structs
|
||||
├── parser.go # YAML parsing and validation
|
||||
└── *_test.go # Parser tests
|
||||
|
||||
examples/apply/
|
||||
├── simple-app.yaml # Basic application deployment
|
||||
├── multi-cloudlet.yaml # Multi-region deployment
|
||||
└── with-network.yaml # Network configuration example
|
||||
```
|
||||
|
||||
This blueprint provides a systematic approach to implementing the apply command while maintaining consistency with existing CLI patterns and ensuring robust error handling and user experience.
|
||||
10
plan.md
10
plan.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Project Overview
|
||||
|
||||
Develop a comprehensive Go SDK for the EdgeXR Master Controller API, building upon the existing `edge-connect-client` prototype. The SDK will provide typed, idiomatic Go interfaces for app lifecycle management, cloudlet orchestration, and edge deployment workflows.
|
||||
Develop a comprehensive Go SDK for the EdgeXR Master Controller API. The SDK will provide typed, idiomatic Go interfaces for app lifecycle management, cloudlet orchestration, and edge deployment workflows.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
|
|
@ -19,16 +19,16 @@ Develop a comprehensive Go SDK for the EdgeXR Master Controller API, building up
|
|||
|
||||
#### 1.1 Project Structure Setup
|
||||
- Add `/sdk` directory to existing edge-connect-client project
|
||||
- Create subdirectories: `/sdk/client`, `/sdk/internal/http`, `/sdk/examples`
|
||||
- Update go.mod with dependencies: oapi-codegen, go-retryablehttp, testify
|
||||
- Create subdirectories: `/sdk/edgeconnect`, `/sdk/internal/http`, `/sdk/examples`
|
||||
- Update go.mod with dependencies: go-retryablehttp, testify
|
||||
- Set up code generation tooling and make targets
|
||||
|
||||
#### 1.2 Code Generation Setup
|
||||
#### 1.2 Code Generation Setup (skipped, oapi-codegen is unused )
|
||||
- Install and configure oapi-codegen
|
||||
- Create generation configuration targeting key swagger definitions
|
||||
- Set up automated generation pipeline in Makefile/scripts
|
||||
|
||||
#### 1.3 Generate Core Types
|
||||
#### 1.3 Generate Core Types (skipped, oapi-codegen is unused )
|
||||
- Generate Go types from swagger: RegionApp, RegionAppInst, RegionCloudlet
|
||||
- Generate GPU driver types: RegionGPUDriver, GPUDriverBuildMember
|
||||
- Create sdk/client/types.go with generated + manually curated types
|
||||
|
|
|
|||
157
project.md
157
project.md
|
|
@ -1,157 +0,0 @@
|
|||
# Edge Connect Client - Project Analysis
|
||||
|
||||
## Overview
|
||||
The Edge Connect Client is a command-line interface (CLI) tool built in Go for managing Edge Connect applications and their instances. It provides a structured way to interact with Edge Connect APIs for creating, showing, listing, and deleting applications and application instances.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
edge-connect-client/
|
||||
├── .claude/ # Claude Code configuration and commands
|
||||
├── api/
|
||||
│ └── swagger.json # API specification (370KB)
|
||||
├── client/ # Core client library
|
||||
│ ├── client.go # HTTP client implementation
|
||||
│ └── models.go # Data models and types
|
||||
├── cmd/ # CLI command implementations
|
||||
│ ├── root.go # Root command and configuration
|
||||
│ ├── app.go # Application management commands
|
||||
│ └── instance.go # Instance management commands
|
||||
├── main.go # Application entry point
|
||||
├── go.mod # Go module definition
|
||||
├── go.sum # Dependency checksums
|
||||
├── README.md # Documentation
|
||||
├── config.yaml.example # Configuration template
|
||||
├── Dockerfile # Empty container definition
|
||||
└── .gitignore # Git ignore rules
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. Main Entry Point (`main.go`)
|
||||
- Simple entry point that delegates to the command package
|
||||
- Follows standard Go CLI application pattern
|
||||
|
||||
#### 2. Command Layer (`cmd/`)
|
||||
- **Root Command** (`root.go`): Base command with global configuration
|
||||
- Uses Cobra for CLI framework
|
||||
- Uses Viper for configuration management
|
||||
- Supports config files, environment variables, and command-line flags
|
||||
- Configuration precedence: flags → env vars → config file
|
||||
|
||||
- **App Commands** (`app.go`): Application lifecycle management
|
||||
- Create, show, list, delete applications
|
||||
- Handles organization, name, version, and region parameters
|
||||
|
||||
- **Instance Commands** (`instance.go`): Instance lifecycle management
|
||||
- Create, show, list, delete application instances
|
||||
- Manages cloudlet assignments and flavors
|
||||
|
||||
#### 3. Client Layer (`client/`)
|
||||
- **HTTP Client** (`client.go`): Core API communication
|
||||
- Token-based authentication with login endpoint
|
||||
- Generic `call()` function for API requests
|
||||
- Structured error handling with custom `ErrResourceNotFound`
|
||||
- JSON-based request/response handling
|
||||
|
||||
- **Models** (`models.go`): Type definitions and data structures
|
||||
- Generic response handling with `Responses[T]` and `Response[T]`
|
||||
- Domain models: `App`, `AppInstance`, `AppKey`, `CloudletKey`, `Flavor`
|
||||
- Input types: `NewAppInput`, `NewAppInstanceInput`
|
||||
- Message interface for error handling
|
||||
|
||||
### Configuration Management
|
||||
- **File-based**: `$HOME/.edge-connect.yaml` (default) or custom via `--config`
|
||||
- **Environment Variables**: Prefixed with `EDGE_CONNECT_`
|
||||
- `EDGE_CONNECT_BASE_URL`
|
||||
- `EDGE_CONNECT_USERNAME`
|
||||
- `EDGE_CONNECT_PASSWORD`
|
||||
- **Command-line Flags**: Override other sources
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Direct Dependencies
|
||||
- **Cobra v1.10.1**: CLI framework for command structure and parsing
|
||||
- **Viper v1.21.0**: Configuration management (files, env vars, flags)
|
||||
|
||||
### Key Indirect Dependencies
|
||||
- `fsnotify`: File system watching for config changes
|
||||
- `go-viper/mapstructure`: Configuration unmarshaling
|
||||
- `pelletier/go-toml`: TOML configuration support
|
||||
- Standard Go libraries for HTTP, JSON, system operations
|
||||
|
||||
## API Integration
|
||||
|
||||
### Authentication Flow
|
||||
1. Client sends username/password to `/api/v1/login`
|
||||
2. Receives JWT token in response
|
||||
3. Token included in `Authorization: Bearer` header for subsequent requests
|
||||
|
||||
### API Endpoints
|
||||
- `/api/v1/auth/ctrl/CreateApp` - Create applications
|
||||
- `/api/v1/auth/ctrl/ShowApp` - Retrieve applications
|
||||
- `/api/v1/auth/ctrl/DeleteApp` - Delete applications
|
||||
- `/api/v1/auth/ctrl/CreateAppInst` - Create instances
|
||||
- `/api/v1/auth/ctrl/ShowAppInst` - Retrieve instances
|
||||
- `/api/v1/auth/ctrl/DeleteAppInst` - Delete instances
|
||||
|
||||
### Response Handling
|
||||
- Streaming JSON responses parsed line-by-line
|
||||
- Generic type-safe response wrapper
|
||||
- Comprehensive error handling with status codes
|
||||
- Built-in logging for debugging
|
||||
|
||||
## Key Features
|
||||
|
||||
### Application Management
|
||||
- Multi-tenant support with organization scoping
|
||||
- Version-aware application handling
|
||||
- Region-based deployments
|
||||
- Configurable security rules and deployment options
|
||||
|
||||
### Instance Management
|
||||
- Cloudlet-based instance deployment
|
||||
- Flavor selection for resource allocation
|
||||
- Application-to-instance relationship tracking
|
||||
- State and power state monitoring
|
||||
|
||||
### Error Handling
|
||||
- Custom error types (`ErrResourceNotFound`)
|
||||
- HTTP status code awareness
|
||||
- Detailed error messages with context
|
||||
- Graceful handling of missing resources
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Code Quality
|
||||
- Clean separation of concerns (CLI/Client/Models)
|
||||
- Generic programming for type safety
|
||||
- Consistent error handling patterns
|
||||
- Comprehensive logging for troubleshooting
|
||||
|
||||
### Configuration
|
||||
- Flexible configuration system supporting multiple sources
|
||||
- Secure credential handling via environment variables
|
||||
- Example configuration provided for easy setup
|
||||
|
||||
### API Design
|
||||
- RESTful API integration with structured endpoints
|
||||
- Token-based security model
|
||||
- Streaming response handling for efficiency
|
||||
- Comprehensive swagger specification (370KB)
|
||||
|
||||
## Missing Components
|
||||
- Empty Dockerfile suggests containerization is planned but not implemented
|
||||
- No tests directory - testing framework needs to be established
|
||||
- No CI/CD configuration visible
|
||||
- Limited error recovery and retry mechanisms
|
||||
|
||||
## Potential Improvements
|
||||
1. **Testing**: Implement unit and integration tests
|
||||
2. **Containerization**: Complete Docker implementation
|
||||
3. **Retry Logic**: Add resilient API call mechanisms
|
||||
4. **Configuration Validation**: Validate config before use
|
||||
5. **Output Formatting**: Add JSON/YAML output options
|
||||
6. **Caching**: Implement token caching to reduce login calls
|
||||
32
sdk/examples/comprehensive/EdgeConnectConfig.yaml
Normal file
32
sdk/examples/comprehensive/EdgeConnectConfig.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Is there a swagger file for the new EdgeConnect API?
|
||||
#
|
||||
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "edge-app-demo"
|
||||
spec:
|
||||
# dockerApp:
|
||||
# appName: "edge-app-demo"
|
||||
# appVersion: "1.0.0"
|
||||
# manifestFile: "./docker-compose.yaml"
|
||||
# image: "https://registry-1.docker.io/library/nginx:latest"
|
||||
k8sApp:
|
||||
appName: "edge-app-demo" # appinstance name is $appName-$appVersion-instance
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./k8s-deployment.yaml" # store hash of the manifest file in annotation field. Annotations is a comma separated map of arbitrary key value pairs,
|
||||
infraTemplate:
|
||||
- organization: "edp2"
|
||||
region: "EU"
|
||||
cloudletOrg: "TelekomOP"
|
||||
cloudletName: "Munich"
|
||||
flavorName: "EU.small"
|
||||
network:
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 443
|
||||
portRangeMax: 443
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
39
sdk/examples/comprehensive/k8s-deployment.yaml
Normal file
39
sdk/examples/comprehensive/k8s-deployment.yaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: edgeconnect-coder-tcp
|
||||
labels:
|
||||
run: edgeconnect-coder
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: tcp80
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
selector:
|
||||
run: edgeconnect-coder
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: edgeconnect-coder-deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
run: edgeconnect-coder
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
run: edgeconnect-coder
|
||||
mexDeployGen: kubernetes-basic
|
||||
spec:
|
||||
volumes:
|
||||
containers:
|
||||
- name: edgeconnect-coder
|
||||
image: edp.buildth.ing/devfw-cicd/edgeconnect-coder:main
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
|
|
@ -99,11 +99,11 @@ func runComprehensiveWorkflow(ctx context.Context, c *edgeconnect.Client, config
|
|||
Version: config.AppVersion,
|
||||
},
|
||||
Deployment: "kubernetes",
|
||||
ImageType: "ImageTypeDocker",
|
||||
ImagePath: "https://registry-1.docker.io/library/nginx:latest",
|
||||
ImageType: "ImageTypeDocker", // field is ignored
|
||||
ImagePath: "https://registry-1.docker.io/library/nginx:latest", // must be set. Even for kubernetes
|
||||
DefaultFlavor: edgeconnect.Flavor{Name: config.FlavorName},
|
||||
ServerlessConfig: struct{}{},
|
||||
AllowServerless: true,
|
||||
ServerlessConfig: struct{}{}, // must be set
|
||||
AllowServerless: true, // must be set to true for kubernetes
|
||||
RequiredOutboundConnections: []edgeconnect.SecurityRule{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
|
|
|
|||
130
sdk/internal/config/example_test.go
Normal file
130
sdk/internal/config/example_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// ABOUTME: Integration test with the actual EdgeConnectConfig.yaml example file
|
||||
// ABOUTME: Validates that our parser correctly handles the real example configuration
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseExampleConfig(t *testing.T) {
|
||||
parser := NewParser()
|
||||
|
||||
// Parse the actual example file (now that we've created the manifest file)
|
||||
examplePath := filepath.Join("../../examples/comprehensive/EdgeConnectConfig.yaml")
|
||||
config, err := parser.ParseFile(examplePath)
|
||||
|
||||
// This should now succeed with full validation
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// Validate the parsed structure
|
||||
assert.Equal(t, "edgeconnect-deployment", config.Kind)
|
||||
assert.Equal(t, "edge-app-demo", config.Metadata.Name)
|
||||
|
||||
// Check k8s app configuration
|
||||
require.NotNil(t, config.Spec.K8sApp)
|
||||
assert.Equal(t, "edge-app-demo", config.Spec.K8sApp.AppName)
|
||||
assert.Equal(t, "1.0.0", config.Spec.K8sApp.AppVersion)
|
||||
// Note: ManifestFile path should be resolved to absolute path
|
||||
assert.Contains(t, config.Spec.K8sApp.ManifestFile, "k8s-deployment.yaml")
|
||||
|
||||
// Check infrastructure template
|
||||
require.Len(t, config.Spec.InfraTemplate, 1)
|
||||
infra := config.Spec.InfraTemplate[0]
|
||||
assert.Equal(t, "edp2", infra.Organization)
|
||||
assert.Equal(t, "EU", infra.Region)
|
||||
assert.Equal(t, "TelekomOP", infra.CloudletOrg)
|
||||
assert.Equal(t, "Munich", infra.CloudletName)
|
||||
assert.Equal(t, "EU.small", infra.FlavorName)
|
||||
|
||||
// Check network configuration
|
||||
require.NotNil(t, config.Spec.Network)
|
||||
require.Len(t, config.Spec.Network.OutboundConnections, 2)
|
||||
|
||||
conn1 := config.Spec.Network.OutboundConnections[0]
|
||||
assert.Equal(t, "tcp", conn1.Protocol)
|
||||
assert.Equal(t, 80, conn1.PortRangeMin)
|
||||
assert.Equal(t, 80, conn1.PortRangeMax)
|
||||
assert.Equal(t, "0.0.0.0/0", conn1.RemoteCIDR)
|
||||
|
||||
conn2 := config.Spec.Network.OutboundConnections[1]
|
||||
assert.Equal(t, "tcp", conn2.Protocol)
|
||||
assert.Equal(t, 443, conn2.PortRangeMin)
|
||||
assert.Equal(t, 443, conn2.PortRangeMax)
|
||||
assert.Equal(t, "0.0.0.0/0", conn2.RemoteCIDR)
|
||||
|
||||
// Test utility methods
|
||||
assert.Equal(t, "edge-app-demo", config.Spec.GetAppName())
|
||||
assert.Equal(t, "1.0.0", config.Spec.GetAppVersion())
|
||||
assert.Contains(t, config.Spec.GetManifestFile(), "k8s-deployment.yaml")
|
||||
assert.True(t, config.Spec.IsK8sApp())
|
||||
assert.False(t, config.Spec.IsDockerApp())
|
||||
|
||||
// Test instance name generation
|
||||
instanceName := GetInstanceName(config.Spec.GetAppName(), config.Spec.GetAppVersion())
|
||||
assert.Equal(t, "edge-app-demo-1.0.0-instance", instanceName)
|
||||
}
|
||||
|
||||
func TestValidateExampleStructure(t *testing.T) {
|
||||
parser := &ConfigParser{}
|
||||
|
||||
// Create a config that matches the example but with valid paths
|
||||
config := &EdgeConnectConfig{
|
||||
Kind: "edgeconnect-deployment",
|
||||
Metadata: Metadata{
|
||||
Name: "edge-app-demo",
|
||||
},
|
||||
Spec: Spec{
|
||||
DockerApp: &DockerApp{ // Use DockerApp to avoid manifest file validation
|
||||
AppName: "edge-app-demo",
|
||||
AppVersion: "1.0.0",
|
||||
Image: "nginx:latest",
|
||||
},
|
||||
InfraTemplate: []InfraTemplate{
|
||||
{
|
||||
Organization: "edp2",
|
||||
Region: "EU",
|
||||
CloudletOrg: "TelekomOP",
|
||||
CloudletName: "Munich",
|
||||
FlavorName: "EU.small",
|
||||
},
|
||||
},
|
||||
Network: &NetworkConfig{
|
||||
OutboundConnections: []OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 443,
|
||||
PortRangeMax: 443,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This should validate successfully
|
||||
err := parser.Validate(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test comprehensive validation
|
||||
err = parser.ComprehensiveValidate(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test infrastructure uniqueness validation
|
||||
err = parser.ValidateInfrastructureUniqueness(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test port range validation
|
||||
err = parser.ValidatePortRanges(config)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
248
sdk/internal/config/parser.go
Normal file
248
sdk/internal/config/parser.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
// ABOUTME: YAML configuration parser for EdgeConnect apply command with comprehensive validation
|
||||
// ABOUTME: Handles parsing and validation of EdgeConnectConfig files with detailed error messages
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Parser defines the interface for configuration parsing
|
||||
type Parser interface {
|
||||
ParseFile(filename string) (*EdgeConnectConfig, error)
|
||||
ParseBytes(data []byte) (*EdgeConnectConfig, error)
|
||||
Validate(config *EdgeConnectConfig) error
|
||||
}
|
||||
|
||||
// ConfigParser implements the Parser interface
|
||||
type ConfigParser struct{}
|
||||
|
||||
// NewParser creates a new configuration parser
|
||||
func NewParser() Parser {
|
||||
return &ConfigParser{}
|
||||
}
|
||||
|
||||
// ParseFile parses an EdgeConnectConfig from a YAML file
|
||||
func (p *ConfigParser) ParseFile(filename string) (*EdgeConnectConfig, error) {
|
||||
if filename == "" {
|
||||
return nil, fmt.Errorf("filename cannot be empty")
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("configuration file does not exist: %s", filename)
|
||||
}
|
||||
|
||||
// Read file contents
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read configuration file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// Parse YAML without validation first
|
||||
config, err := p.parseYAMLOnly(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse configuration file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// Resolve relative paths relative to config file directory
|
||||
configDir := filepath.Dir(filename)
|
||||
if err := p.resolveRelativePaths(config, configDir); err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve paths in %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// Now validate with resolved paths
|
||||
if err := p.Validate(config); err != nil {
|
||||
return nil, fmt.Errorf("configuration validation failed in %s: %w", filename, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// parseYAMLOnly parses YAML without validation
|
||||
func (p *ConfigParser) parseYAMLOnly(data []byte) (*EdgeConnectConfig, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("configuration data cannot be empty")
|
||||
}
|
||||
|
||||
var config EdgeConnectConfig
|
||||
|
||||
// Parse YAML with strict mode
|
||||
decoder := yaml.NewDecoder(nil)
|
||||
decoder.KnownFields(true) // Fail on unknown fields
|
||||
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("YAML parsing failed: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// ParseBytes parses an EdgeConnectConfig from YAML bytes
|
||||
func (p *ConfigParser) ParseBytes(data []byte) (*EdgeConnectConfig, error) {
|
||||
// Parse YAML only
|
||||
config, err := p.parseYAMLOnly(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the parsed configuration
|
||||
if err := p.Validate(config); err != nil {
|
||||
return nil, fmt.Errorf("configuration validation failed: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Validate performs comprehensive validation of the configuration
|
||||
func (p *ConfigParser) Validate(config *EdgeConnectConfig) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("configuration cannot be nil")
|
||||
}
|
||||
|
||||
return config.Validate()
|
||||
}
|
||||
|
||||
// resolveRelativePaths converts relative paths to absolute paths based on config directory
|
||||
func (p *ConfigParser) resolveRelativePaths(config *EdgeConnectConfig, configDir string) error {
|
||||
if config.Spec.K8sApp != nil {
|
||||
resolved := config.Spec.K8sApp.GetManifestPath(configDir)
|
||||
config.Spec.K8sApp.ManifestFile = resolved
|
||||
}
|
||||
|
||||
if config.Spec.DockerApp != nil && config.Spec.DockerApp.ManifestFile != "" {
|
||||
resolved := config.Spec.DockerApp.GetManifestPath(configDir)
|
||||
config.Spec.DockerApp.ManifestFile = resolved
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateManifestFiles performs additional validation on manifest files
|
||||
func (p *ConfigParser) ValidateManifestFiles(config *EdgeConnectConfig) error {
|
||||
var manifestFile string
|
||||
|
||||
if config.Spec.K8sApp != nil {
|
||||
manifestFile = config.Spec.K8sApp.ManifestFile
|
||||
} else if config.Spec.DockerApp != nil {
|
||||
manifestFile = config.Spec.DockerApp.ManifestFile
|
||||
}
|
||||
|
||||
if manifestFile != "" {
|
||||
if err := p.validateManifestFile(manifestFile); err != nil {
|
||||
return fmt.Errorf("manifest file validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateManifestFile checks if the manifest file is valid and readable
|
||||
func (p *ConfigParser) validateManifestFile(filename string) error {
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot access manifest file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("manifest file cannot be a directory: %s", filename)
|
||||
}
|
||||
|
||||
if info.Size() == 0 {
|
||||
return fmt.Errorf("manifest file cannot be empty: %s", filename)
|
||||
}
|
||||
|
||||
// Try to read the file to ensure it's accessible
|
||||
if _, err := os.ReadFile(filename); err != nil {
|
||||
return fmt.Errorf("cannot read manifest file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInstanceName generates the instance name following the pattern: appName-appVersion-instance
|
||||
func GetInstanceName(appName, appVersion string) string {
|
||||
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
|
||||
}
|
||||
|
||||
// ValidateInfrastructureUniqueness ensures no duplicate infrastructure targets
|
||||
func (p *ConfigParser) ValidateInfrastructureUniqueness(config *EdgeConnectConfig) error {
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for i, infra := range config.Spec.InfraTemplate {
|
||||
key := fmt.Sprintf("%s:%s:%s:%s",
|
||||
infra.Organization,
|
||||
infra.Region,
|
||||
infra.CloudletOrg,
|
||||
infra.CloudletName)
|
||||
|
||||
if seen[key] {
|
||||
return fmt.Errorf("duplicate infrastructure target at index %d: org=%s, region=%s, cloudletOrg=%s, cloudletName=%s",
|
||||
i, infra.Organization, infra.Region, infra.CloudletOrg, infra.CloudletName)
|
||||
}
|
||||
|
||||
seen[key] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePortRanges ensures port ranges don't overlap in network configuration
|
||||
func (p *ConfigParser) ValidatePortRanges(config *EdgeConnectConfig) error {
|
||||
if config.Spec.Network == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
connections := config.Spec.Network.OutboundConnections
|
||||
for i := 0; i < len(connections); i++ {
|
||||
for j := i + 1; j < len(connections); j++ {
|
||||
conn1 := connections[i]
|
||||
conn2 := connections[j]
|
||||
|
||||
// Only check same protocol and CIDR
|
||||
if conn1.Protocol == conn2.Protocol && conn1.RemoteCIDR == conn2.RemoteCIDR {
|
||||
if portRangesOverlap(conn1.PortRangeMin, conn1.PortRangeMax, conn2.PortRangeMin, conn2.PortRangeMax) {
|
||||
return fmt.Errorf("overlapping port ranges for protocol %s and CIDR %s: [%d-%d] overlaps with [%d-%d]",
|
||||
conn1.Protocol, conn1.RemoteCIDR,
|
||||
conn1.PortRangeMin, conn1.PortRangeMax,
|
||||
conn2.PortRangeMin, conn2.PortRangeMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// portRangesOverlap checks if two port ranges overlap
|
||||
func portRangesOverlap(min1, max1, min2, max2 int) bool {
|
||||
return max1 >= min2 && max2 >= min1
|
||||
}
|
||||
|
||||
// ComprehensiveValidate performs all validation checks including extended ones
|
||||
func (p *ConfigParser) ComprehensiveValidate(config *EdgeConnectConfig) error {
|
||||
// Basic validation
|
||||
if err := p.Validate(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Manifest file validation
|
||||
if err := p.ValidateManifestFiles(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Infrastructure uniqueness validation
|
||||
if err := p.ValidateInfrastructureUniqueness(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Port range validation
|
||||
if err := p.ValidatePortRanges(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
789
sdk/internal/config/parser_test.go
Normal file
789
sdk/internal/config/parser_test.go
Normal file
|
|
@ -0,0 +1,789 @@
|
|||
// ABOUTME: Comprehensive tests for EdgeConnect configuration parser with validation scenarios
|
||||
// ABOUTME: Tests all validation rules, error conditions, and successful parsing cases
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewParser(t *testing.T) {
|
||||
parser := NewParser()
|
||||
assert.NotNil(t, parser)
|
||||
assert.IsType(t, &ConfigParser{}, parser)
|
||||
}
|
||||
|
||||
func TestConfigParser_ParseBytes(t *testing.T) {
|
||||
parser := NewParser()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid k8s config",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
k8sApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./test-manifest.yaml"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: true, // Will fail because manifest file doesn't exist
|
||||
errMsg: "manifestFile does not exist",
|
||||
},
|
||||
{
|
||||
name: "valid docker config",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing kind",
|
||||
yaml: `
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
k8sApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./test-manifest.yaml"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: true,
|
||||
errMsg: "kind is required",
|
||||
},
|
||||
{
|
||||
name: "invalid kind",
|
||||
yaml: `
|
||||
kind: invalid-kind
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: true,
|
||||
errMsg: "unsupported kind: invalid-kind",
|
||||
},
|
||||
{
|
||||
name: "missing app definition",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: true,
|
||||
errMsg: "spec must define either k8sApp or dockerApp",
|
||||
},
|
||||
{
|
||||
name: "both k8s and docker apps",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
k8sApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./test-manifest.yaml"
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`,
|
||||
wantErr: true,
|
||||
errMsg: "spec cannot define both k8sApp and dockerApp",
|
||||
},
|
||||
{
|
||||
name: "empty infrastructure template",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate: []
|
||||
`,
|
||||
wantErr: true,
|
||||
errMsg: "infraTemplate is required and must contain at least one target",
|
||||
},
|
||||
{
|
||||
name: "with network config",
|
||||
yaml: `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
network:
|
||||
outboundConnections:
|
||||
- protocol: "tcp"
|
||||
portRangeMin: 80
|
||||
portRangeMax: 80
|
||||
remoteCIDR: "0.0.0.0/0"
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty data",
|
||||
yaml: "",
|
||||
wantErr: true,
|
||||
errMsg: "configuration data cannot be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config, err := parser.ParseBytes([]byte(tt.yaml))
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
assert.Nil(t, config)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParser_ParseFile(t *testing.T) {
|
||||
parser := NewParser()
|
||||
|
||||
// Create temporary directory for test files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a valid config file
|
||||
validConfig := `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
dockerApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
image: "nginx:latest"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`
|
||||
|
||||
validFile := filepath.Join(tempDir, "valid.yaml")
|
||||
err := os.WriteFile(validFile, []byte(validConfig), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test valid file parsing
|
||||
config, err := parser.ParseFile(validFile)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, "edgeconnect-deployment", config.Kind)
|
||||
assert.Equal(t, "test-app", config.Metadata.Name)
|
||||
|
||||
// Test non-existent file
|
||||
nonExistentFile := filepath.Join(tempDir, "nonexistent.yaml")
|
||||
config, err = parser.ParseFile(nonExistentFile)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not exist")
|
||||
assert.Nil(t, config)
|
||||
|
||||
// Test empty filename
|
||||
config, err = parser.ParseFile("")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "filename cannot be empty")
|
||||
assert.Nil(t, config)
|
||||
|
||||
// Test invalid YAML
|
||||
invalidFile := filepath.Join(tempDir, "invalid.yaml")
|
||||
err = os.WriteFile(invalidFile, []byte("invalid: yaml: content: ["), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
config, err = parser.ParseFile(invalidFile)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "YAML parsing failed")
|
||||
assert.Nil(t, config)
|
||||
}
|
||||
|
||||
func TestConfigParser_RelativePathResolution(t *testing.T) {
|
||||
parser := NewParser()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create a manifest file
|
||||
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
||||
manifestFile := filepath.Join(tempDir, "manifest.yaml")
|
||||
err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create config with relative path
|
||||
configContent := `
|
||||
kind: edgeconnect-deployment
|
||||
metadata:
|
||||
name: "test-app"
|
||||
spec:
|
||||
k8sApp:
|
||||
appName: "test-app"
|
||||
appVersion: "1.0.0"
|
||||
manifestFile: "./manifest.yaml"
|
||||
infraTemplate:
|
||||
- organization: "testorg"
|
||||
region: "US"
|
||||
cloudletOrg: "TestOP"
|
||||
cloudletName: "TestCloudlet"
|
||||
flavorName: "small"
|
||||
`
|
||||
|
||||
configFile := filepath.Join(tempDir, "config.yaml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
config, err := parser.ParseFile(configFile)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, config)
|
||||
|
||||
// Check that relative path was resolved to absolute
|
||||
expectedPath := filepath.Join(tempDir, "manifest.yaml")
|
||||
assert.Equal(t, expectedPath, config.Spec.K8sApp.ManifestFile)
|
||||
}
|
||||
|
||||
func TestEdgeConnectConfig_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config EdgeConnectConfig
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid config",
|
||||
config: EdgeConnectConfig{
|
||||
Kind: "edgeconnect-deployment",
|
||||
Metadata: Metadata{
|
||||
Name: "test-app",
|
||||
},
|
||||
Spec: Spec{
|
||||
DockerApp: &DockerApp{
|
||||
AppName: "test-app",
|
||||
AppVersion: "1.0.0",
|
||||
Image: "nginx:latest",
|
||||
},
|
||||
InfraTemplate: []InfraTemplate{
|
||||
{
|
||||
Organization: "testorg",
|
||||
Region: "US",
|
||||
CloudletOrg: "TestOP",
|
||||
CloudletName: "TestCloudlet",
|
||||
FlavorName: "small",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing kind",
|
||||
config: EdgeConnectConfig{
|
||||
Metadata: Metadata{Name: "test"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "kind is required",
|
||||
},
|
||||
{
|
||||
name: "invalid kind",
|
||||
config: EdgeConnectConfig{
|
||||
Kind: "invalid",
|
||||
Metadata: Metadata{Name: "test"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "unsupported kind",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.config.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadata_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata Metadata
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid metadata",
|
||||
metadata: Metadata{Name: "test-app"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
metadata: Metadata{Name: ""},
|
||||
wantErr: true,
|
||||
errMsg: "metadata.name is required",
|
||||
},
|
||||
{
|
||||
name: "name with leading whitespace",
|
||||
metadata: Metadata{Name: " test-app"},
|
||||
wantErr: true,
|
||||
errMsg: "cannot have leading/trailing whitespace",
|
||||
},
|
||||
{
|
||||
name: "name with trailing whitespace",
|
||||
metadata: Metadata{Name: "test-app "},
|
||||
wantErr: true,
|
||||
errMsg: "cannot have leading/trailing whitespace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.metadata.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundConnection_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
connection OutboundConnection
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid connection",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing protocol",
|
||||
connection: OutboundConnection{
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "protocol is required",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "invalid",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "protocol must be one of: tcp, udp, icmp",
|
||||
},
|
||||
{
|
||||
name: "invalid port range min",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 0,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "portRangeMin must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid port range max",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 65536,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "portRangeMax must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "min greater than max",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 443,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "portRangeMin (443) cannot be greater than portRangeMax (80)",
|
||||
},
|
||||
{
|
||||
name: "missing remote CIDR",
|
||||
connection: OutboundConnection{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "remoteCIDR is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.connection.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParser_ValidateInfrastructureUniqueness(t *testing.T) {
|
||||
parser := &ConfigParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *EdgeConnectConfig
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "unique infrastructure",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
InfraTemplate: []InfraTemplate{
|
||||
{
|
||||
Organization: "org1",
|
||||
Region: "US",
|
||||
CloudletOrg: "cloudlet1",
|
||||
CloudletName: "name1",
|
||||
},
|
||||
{
|
||||
Organization: "org1",
|
||||
Region: "EU",
|
||||
CloudletOrg: "cloudlet1",
|
||||
CloudletName: "name1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate infrastructure",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
InfraTemplate: []InfraTemplate{
|
||||
{
|
||||
Organization: "org1",
|
||||
Region: "US",
|
||||
CloudletOrg: "cloudlet1",
|
||||
CloudletName: "name1",
|
||||
},
|
||||
{
|
||||
Organization: "org1",
|
||||
Region: "US",
|
||||
CloudletOrg: "cloudlet1",
|
||||
CloudletName: "name1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "duplicate infrastructure target",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := parser.ValidateInfrastructureUniqueness(tt.config)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigParser_ValidatePortRanges(t *testing.T) {
|
||||
parser := &ConfigParser{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *EdgeConnectConfig
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no network config",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
Network: nil,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-overlapping ports",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
Network: &NetworkConfig{
|
||||
OutboundConnections: []OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 80,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 443,
|
||||
PortRangeMax: 443,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "overlapping ports same protocol and CIDR",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
Network: &NetworkConfig{
|
||||
OutboundConnections: []OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 90,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 85,
|
||||
PortRangeMax: 95,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "overlapping port ranges",
|
||||
},
|
||||
{
|
||||
name: "overlapping ports different protocol",
|
||||
config: &EdgeConnectConfig{
|
||||
Spec: Spec{
|
||||
Network: &NetworkConfig{
|
||||
OutboundConnections: []OutboundConnection{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
PortRangeMin: 80,
|
||||
PortRangeMax: 90,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
PortRangeMin: 85,
|
||||
PortRangeMax: 95,
|
||||
RemoteCIDR: "0.0.0.0/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false, // Different protocols can overlap
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := parser.ValidatePortRanges(tt.config)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInstanceName(t *testing.T) {
|
||||
tests := []struct {
|
||||
appName string
|
||||
appVersion string
|
||||
expected string
|
||||
}{
|
||||
{"myapp", "1.0.0", "myapp-1.0.0-instance"},
|
||||
{"test-app", "v2.1", "test-app-v2.1-instance"},
|
||||
{"app", "latest", "app-latest-instance"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.appName+"-"+tt.appVersion, func(t *testing.T) {
|
||||
result := GetInstanceName(tt.appName, tt.appVersion)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpec_GetMethods(t *testing.T) {
|
||||
k8sSpec := &Spec{
|
||||
K8sApp: &K8sApp{
|
||||
AppName: "k8s-app",
|
||||
AppVersion: "1.0.0",
|
||||
ManifestFile: "k8s.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
dockerSpec := &Spec{
|
||||
DockerApp: &DockerApp{
|
||||
AppName: "docker-app",
|
||||
AppVersion: "2.0.0",
|
||||
ManifestFile: "docker.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "k8s-app", k8sSpec.GetAppName())
|
||||
assert.Equal(t, "1.0.0", k8sSpec.GetAppVersion())
|
||||
assert.Equal(t, "k8s.yaml", k8sSpec.GetManifestFile())
|
||||
assert.True(t, k8sSpec.IsK8sApp())
|
||||
assert.False(t, k8sSpec.IsDockerApp())
|
||||
|
||||
assert.Equal(t, "docker-app", dockerSpec.GetAppName())
|
||||
assert.Equal(t, "2.0.0", dockerSpec.GetAppVersion())
|
||||
assert.Equal(t, "docker.yaml", dockerSpec.GetManifestFile())
|
||||
assert.False(t, dockerSpec.IsK8sApp())
|
||||
assert.True(t, dockerSpec.IsDockerApp())
|
||||
}
|
||||
|
||||
func TestPortRangesOverlap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
min1 int
|
||||
max1 int
|
||||
min2 int
|
||||
max2 int
|
||||
expected bool
|
||||
}{
|
||||
{"no overlap", 10, 20, 30, 40, false},
|
||||
{"overlap", 10, 20, 15, 25, true},
|
||||
{"adjacent", 10, 20, 21, 30, false},
|
||||
{"touching", 10, 20, 20, 30, true},
|
||||
{"contained", 10, 30, 15, 25, true},
|
||||
{"same range", 10, 20, 10, 20, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := portRangesOverlap(tt.min1, tt.max1, tt.min2, tt.max2)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
365
sdk/internal/config/types.go
Normal file
365
sdk/internal/config/types.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// K8sApp defines Kubernetes application configuration
|
||||
type K8sApp struct {
|
||||
AppName string `yaml:"appName"`
|
||||
AppVersion string `yaml:"appVersion"`
|
||||
ManifestFile string `yaml:"manifestFile"`
|
||||
}
|
||||
|
||||
// DockerApp defines Docker application configuration
|
||||
type DockerApp struct {
|
||||
AppName string `yaml:"appName"`
|
||||
AppVersion string `yaml:"appVersion"`
|
||||
ManifestFile string `yaml:"manifestFile"`
|
||||
Image string `yaml:"image"`
|
||||
}
|
||||
|
||||
// InfraTemplate defines infrastructure deployment targets
|
||||
type InfraTemplate struct {
|
||||
Organization string `yaml:"organization"`
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates k8s app configuration
|
||||
func (k *K8sApp) Validate() error {
|
||||
if k.AppName == "" {
|
||||
return fmt.Errorf("appName is required")
|
||||
}
|
||||
|
||||
if k.AppVersion == "" {
|
||||
return fmt.Errorf("appVersion is required")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Validate app name format
|
||||
if strings.TrimSpace(k.AppName) != k.AppName {
|
||||
return fmt.Errorf("appName cannot have leading/trailing whitespace")
|
||||
}
|
||||
|
||||
// Validate version format
|
||||
if strings.TrimSpace(k.AppVersion) != k.AppVersion {
|
||||
return fmt.Errorf("appVersion cannot have leading/trailing whitespace")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates docker app configuration
|
||||
func (d *DockerApp) Validate() error {
|
||||
if d.AppName == "" {
|
||||
return fmt.Errorf("appName is required")
|
||||
}
|
||||
|
||||
if d.AppVersion == "" {
|
||||
return fmt.Errorf("appVersion is required")
|
||||
}
|
||||
|
||||
if d.Image == "" {
|
||||
return fmt.Errorf("image is required")
|
||||
}
|
||||
|
||||
// Validate app name format
|
||||
if strings.TrimSpace(d.AppName) != d.AppName {
|
||||
return fmt.Errorf("appName cannot have leading/trailing whitespace")
|
||||
}
|
||||
|
||||
// Validate version format
|
||||
if strings.TrimSpace(d.AppVersion) != d.AppVersion {
|
||||
return fmt.Errorf("appVersion cannot have leading/trailing whitespace")
|
||||
}
|
||||
|
||||
// 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.Organization == "" {
|
||||
return fmt.Errorf("organization is required")
|
||||
}
|
||||
|
||||
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{
|
||||
"organization": i.Organization,
|
||||
"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)
|
||||
}
|
||||
|
||||
// GetAppName returns the application name from the active app type
|
||||
func (s *Spec) GetAppName() string {
|
||||
if s.K8sApp != nil {
|
||||
return s.K8sApp.AppName
|
||||
}
|
||||
if s.DockerApp != nil {
|
||||
return s.DockerApp.AppName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetAppVersion returns the application version from the active app type
|
||||
func (s *Spec) GetAppVersion() string {
|
||||
if s.K8sApp != nil {
|
||||
return s.K8sApp.AppVersion
|
||||
}
|
||||
if s.DockerApp != nil {
|
||||
return s.DockerApp.AppVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue