Skip to content

Commit 54470a4

Browse files
grokifyclaude
andcommitted
feat(deploy): add deployment provider system
Add core deployment provider system with: - Provider interface for cloud deployment backends - Capabilities struct for provider feature detection - DeployConfig with YAML loading and env var overrides - Thread-safe provider registry with priority support - DeploymentStatus and Resource types for tracking state - Custom error types (ProviderError, ConfigError, DeploymentError) Supports environment variable configuration via: - AGENTKIT_DEPLOY_PROVIDER for provider selection - PULUMI_BACKEND_URL for state storage - AGENTKIT_DEPLOY_DRY_RUN and AGENTKIT_DEPLOY_AUTO_APPROVE Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d6df8de commit 54470a4

File tree

5 files changed

+847
-0
lines changed

5 files changed

+847
-0
lines changed

deploy/config.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package deploy
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strconv"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
// Environment variable names for configuration.
12+
const (
13+
// EnvDeployProvider is the environment variable for selecting the provider.
14+
EnvDeployProvider = "AGENTKIT_DEPLOY_PROVIDER"
15+
16+
// EnvPulumiBackend is the environment variable for the Pulumi backend URL.
17+
EnvPulumiBackend = "PULUMI_BACKEND_URL"
18+
19+
// EnvPulumiAccessToken is the environment variable for Pulumi Cloud access.
20+
EnvPulumiAccessToken = "PULUMI_ACCESS_TOKEN"
21+
22+
// EnvDryRun is the environment variable to enable dry-run mode.
23+
EnvDryRun = "AGENTKIT_DEPLOY_DRY_RUN"
24+
25+
// EnvAutoApprove is the environment variable to auto-approve deployments.
26+
EnvAutoApprove = "AGENTKIT_DEPLOY_AUTO_APPROVE"
27+
)
28+
29+
// DefaultProvider is the default deployment provider when none is specified.
30+
const DefaultProvider = "lightsail"
31+
32+
// DeployConfig holds the configuration for a deployment.
33+
type DeployConfig struct {
34+
// Stack contains the core stack configuration.
35+
Stack StackConfig `yaml:"stack" json:"stack"`
36+
37+
// Provider is the deployment provider name ("lightsail", "ecs", "cloudrun", "docker").
38+
Provider string `yaml:"provider" json:"provider"`
39+
40+
// ProviderConfig contains provider-specific configuration.
41+
// Use type assertion to access (e.g., cfg.ProviderConfig.(*LightsailConfig)).
42+
ProviderConfig any `yaml:"provider_config" json:"provider_config"`
43+
44+
// PulumiBackend is the Pulumi state backend URL.
45+
// Examples: "file://~/.pulumi", "s3://my-bucket/pulumi", "https://api.pulumi.com"
46+
PulumiBackend string `yaml:"pulumi_backend" json:"pulumi_backend"`
47+
48+
// DryRun performs a preview without making changes.
49+
DryRun bool `yaml:"dry_run" json:"dry_run"`
50+
51+
// AutoApprove skips confirmation prompts.
52+
AutoApprove bool `yaml:"auto_approve" json:"auto_approve"`
53+
}
54+
55+
// StackConfig defines the core deployment stack configuration.
56+
type StackConfig struct {
57+
// Name is the stack name (e.g., "myapp-prod", "invest-advisor-staging").
58+
Name string `yaml:"name" json:"name"`
59+
60+
// Project is the Pulumi project name.
61+
Project string `yaml:"project" json:"project"`
62+
63+
// Description provides a human-readable description.
64+
Description string `yaml:"description" json:"description"`
65+
66+
// Region is the cloud region for deployment.
67+
Region string `yaml:"region" json:"region"`
68+
69+
// Tags are resource tags applied to all resources.
70+
Tags map[string]string `yaml:"tags" json:"tags"`
71+
72+
// Image is the container image configuration.
73+
Image ImageConfig `yaml:"image" json:"image"`
74+
75+
// Resources defines compute resources.
76+
Resources ResourceConfig `yaml:"resources" json:"resources"`
77+
78+
// Environment contains environment variables.
79+
Environment map[string]string `yaml:"environment" json:"environment"`
80+
81+
// Secrets contains secret references.
82+
Secrets map[string]SecretRef `yaml:"secrets" json:"secrets"`
83+
84+
// HealthCheck defines the health check configuration.
85+
HealthCheck *HealthCheckConfig `yaml:"health_check" json:"health_check"`
86+
87+
// Scaling defines the scaling configuration.
88+
Scaling *ScalingConfig `yaml:"scaling" json:"scaling"`
89+
}
90+
91+
// ImageConfig defines container image settings.
92+
type ImageConfig struct {
93+
// Repository is the container image repository.
94+
Repository string `yaml:"repository" json:"repository"`
95+
96+
// Tag is the image tag (default: "latest").
97+
Tag string `yaml:"tag" json:"tag"`
98+
99+
// Digest is the image digest for immutable deployments.
100+
Digest string `yaml:"digest" json:"digest"`
101+
102+
// Build contains build configuration if building from source.
103+
Build *BuildConfig `yaml:"build" json:"build"`
104+
}
105+
106+
// BuildConfig defines how to build the container image.
107+
type BuildConfig struct {
108+
// Context is the build context path.
109+
Context string `yaml:"context" json:"context"`
110+
111+
// Dockerfile is the path to the Dockerfile.
112+
Dockerfile string `yaml:"dockerfile" json:"dockerfile"`
113+
114+
// Args are build arguments.
115+
Args map[string]string `yaml:"args" json:"args"`
116+
}
117+
118+
// ResourceConfig defines compute resources.
119+
type ResourceConfig struct {
120+
// CPU is the CPU allocation (e.g., "0.25", "1", "2").
121+
CPU string `yaml:"cpu" json:"cpu"`
122+
123+
// Memory is the memory allocation (e.g., "512", "1024", "2048" in MB).
124+
Memory string `yaml:"memory" json:"memory"`
125+
126+
// Port is the container port to expose.
127+
Port int `yaml:"port" json:"port"`
128+
}
129+
130+
// SecretRef references a secret from a secrets manager.
131+
type SecretRef struct {
132+
// Source is the secret source ("env", "ssm", "secrets-manager", "vault").
133+
Source string `yaml:"source" json:"source"`
134+
135+
// Key is the secret key or path.
136+
Key string `yaml:"key" json:"key"`
137+
138+
// Version is the secret version (optional).
139+
Version string `yaml:"version" json:"version"`
140+
}
141+
142+
// HealthCheckConfig defines health check settings.
143+
type HealthCheckConfig struct {
144+
// Path is the HTTP health check path.
145+
Path string `yaml:"path" json:"path"`
146+
147+
// IntervalSeconds is the check interval.
148+
IntervalSeconds int `yaml:"interval_seconds" json:"interval_seconds"`
149+
150+
// TimeoutSeconds is the check timeout.
151+
TimeoutSeconds int `yaml:"timeout_seconds" json:"timeout_seconds"`
152+
153+
// HealthyThreshold is the number of successful checks required.
154+
HealthyThreshold int `yaml:"healthy_threshold" json:"healthy_threshold"`
155+
156+
// UnhealthyThreshold is the number of failed checks before unhealthy.
157+
UnhealthyThreshold int `yaml:"unhealthy_threshold" json:"unhealthy_threshold"`
158+
}
159+
160+
// ScalingConfig defines scaling settings.
161+
type ScalingConfig struct {
162+
// MinInstances is the minimum number of instances.
163+
MinInstances int `yaml:"min_instances" json:"min_instances"`
164+
165+
// MaxInstances is the maximum number of instances.
166+
MaxInstances int `yaml:"max_instances" json:"max_instances"`
167+
168+
// TargetCPUPercent is the target CPU utilization for scaling.
169+
TargetCPUPercent int `yaml:"target_cpu_percent" json:"target_cpu_percent"`
170+
}
171+
172+
// LoadDeployConfig loads a deployment configuration from a YAML file.
173+
// Environment variables override file values (precedence: env > file > defaults).
174+
func LoadDeployConfig(path string) (*DeployConfig, error) {
175+
data, err := os.ReadFile(path)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to read config file: %w", err)
178+
}
179+
180+
cfg := &DeployConfig{}
181+
if err := yaml.Unmarshal(data, cfg); err != nil {
182+
return nil, fmt.Errorf("failed to parse config file: %w", err)
183+
}
184+
185+
// Apply environment variable overrides
186+
applyEnvOverrides(cfg)
187+
188+
// Apply defaults
189+
applyDefaults(cfg)
190+
191+
return cfg, nil
192+
}
193+
194+
// NewDeployConfig creates a new DeployConfig with defaults applied.
195+
func NewDeployConfig() *DeployConfig {
196+
cfg := &DeployConfig{}
197+
applyDefaults(cfg)
198+
return cfg
199+
}
200+
201+
// applyEnvOverrides applies environment variable overrides to the config.
202+
func applyEnvOverrides(cfg *DeployConfig) {
203+
// Provider override
204+
if provider := os.Getenv(EnvDeployProvider); provider != "" {
205+
cfg.Provider = provider
206+
}
207+
208+
// Pulumi backend override
209+
if backend := os.Getenv(EnvPulumiBackend); backend != "" {
210+
cfg.PulumiBackend = backend
211+
}
212+
213+
// Dry run override
214+
if dryRun := os.Getenv(EnvDryRun); dryRun != "" {
215+
cfg.DryRun = parseBool(dryRun)
216+
}
217+
218+
// Auto approve override
219+
if autoApprove := os.Getenv(EnvAutoApprove); autoApprove != "" {
220+
cfg.AutoApprove = parseBool(autoApprove)
221+
}
222+
}
223+
224+
// applyDefaults applies default values to the config.
225+
func applyDefaults(cfg *DeployConfig) {
226+
if cfg.Provider == "" {
227+
cfg.Provider = DefaultProvider
228+
}
229+
230+
if cfg.Stack.Image.Tag == "" {
231+
cfg.Stack.Image.Tag = "latest"
232+
}
233+
234+
if cfg.Stack.Resources.Port == 0 {
235+
cfg.Stack.Resources.Port = 8080
236+
}
237+
238+
if cfg.Stack.Resources.CPU == "" {
239+
cfg.Stack.Resources.CPU = "0.25"
240+
}
241+
242+
if cfg.Stack.Resources.Memory == "" {
243+
cfg.Stack.Resources.Memory = "512"
244+
}
245+
}
246+
247+
// Validate checks that the configuration is valid.
248+
func (c *DeployConfig) Validate() error {
249+
if c.Stack.Name == "" {
250+
return NewConfigError("stack.name", ErrInvalidConfig)
251+
}
252+
253+
if c.Stack.Project == "" {
254+
return NewConfigError("stack.project", ErrInvalidConfig)
255+
}
256+
257+
if c.Stack.Image.Repository == "" && c.Stack.Image.Build == nil {
258+
return NewConfigError("stack.image", fmt.Errorf("%w: repository or build required", ErrInvalidConfig))
259+
}
260+
261+
return nil
262+
}
263+
264+
// GetProviderName returns the effective provider name, considering env overrides.
265+
func (c *DeployConfig) GetProviderName() string {
266+
if provider := os.Getenv(EnvDeployProvider); provider != "" {
267+
return provider
268+
}
269+
if c.Provider != "" {
270+
return c.Provider
271+
}
272+
return DefaultProvider
273+
}
274+
275+
// parseBool parses a boolean string (case-insensitive).
276+
func parseBool(s string) bool {
277+
b, err := strconv.ParseBool(s)
278+
if err != nil {
279+
return false
280+
}
281+
return b
282+
}

0 commit comments

Comments
 (0)