gjallarhorn 0.1.0.alpha → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +12 -0
- data/README.md +116 -9
- data/examples/zero-downtime-deployment.rb +104 -0
- data/lib/gjallarhorn/adapter/aws.rb +652 -0
- data/lib/gjallarhorn/adapter/base.rb +125 -0
- data/lib/gjallarhorn/cli.rb +90 -4
- data/lib/gjallarhorn/configuration.rb +64 -8
- data/lib/gjallarhorn/deployer.rb +179 -9
- data/lib/gjallarhorn/deployment/basic.rb +171 -0
- data/lib/gjallarhorn/deployment/legacy.rb +40 -0
- data/lib/gjallarhorn/deployment/strategy.rb +189 -0
- data/lib/gjallarhorn/deployment/zero_downtime.rb +276 -0
- data/lib/gjallarhorn/history.rb +164 -0
- data/lib/gjallarhorn/proxy/kamal_proxy_manager.rb +36 -0
- data/lib/gjallarhorn/proxy/manager.rb +186 -0
- data/lib/gjallarhorn/proxy/nginx_manager.rb +362 -0
- data/lib/gjallarhorn/proxy/traefik_manager.rb +36 -0
- data/lib/gjallarhorn/version.rb +1 -1
- data/lib/gjallarhorn.rb +16 -0
- metadata +101 -6
- data/lib/gjallarhorn/adapters/aws.rb +0 -96
- data/lib/gjallarhorn/adapters/base.rb +0 -56
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
# Base adapter interface for cloud provider implementations
|
|
7
|
+
#
|
|
8
|
+
# The Base class defines the common interface that all cloud provider adapters
|
|
9
|
+
# must implement. It provides the foundation for deploying, managing, and monitoring
|
|
10
|
+
# containerized applications across different cloud platforms.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass and override {#deploy}, {#rollback}, {#status}, {#health_check},
|
|
13
|
+
# {#scale}, and {#logs} to implement a cloud provider adapter.
|
|
14
|
+
#
|
|
15
|
+
# @example Implementing a custom adapter
|
|
16
|
+
# class MyCloudAdapter < Gjallarhorn::Adapter::Base
|
|
17
|
+
# def deploy(image:, environment:, services: [])
|
|
18
|
+
# # Implementation specific to MyCloud
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @since 0.1.0
|
|
23
|
+
module Gjallarhorn
|
|
24
|
+
module Adapter
|
|
25
|
+
# Abstract base class for all cloud provider adapters
|
|
26
|
+
class Base
|
|
27
|
+
# @return [Hash] Configuration hash for this adapter
|
|
28
|
+
attr_reader :config
|
|
29
|
+
|
|
30
|
+
# @return [Logger] Logger instance for adapter operations
|
|
31
|
+
attr_reader :logger
|
|
32
|
+
|
|
33
|
+
# Initialize a new adapter instance
|
|
34
|
+
#
|
|
35
|
+
# @param config [Hash] Configuration hash containing provider-specific settings
|
|
36
|
+
def initialize(config)
|
|
37
|
+
@config = config
|
|
38
|
+
@logger = Logger.new($stdout)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Deploy a container image with the specified services
|
|
42
|
+
#
|
|
43
|
+
# @param image [String] Container image tag to deploy
|
|
44
|
+
# @param environment [String] Target environment name
|
|
45
|
+
# @param services [Array<Hash>] List of service configurations to deploy
|
|
46
|
+
# @abstract Subclasses must implement this method
|
|
47
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
48
|
+
# @return [void]
|
|
49
|
+
def deploy(image:, environment:, services: [])
|
|
50
|
+
raise NotImplementedError, "Subclasses must implement deploy"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Rollback services to a previous version
|
|
54
|
+
#
|
|
55
|
+
# @param version [String] Version to rollback to
|
|
56
|
+
# @abstract Subclasses must implement this method
|
|
57
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
58
|
+
# @return [void]
|
|
59
|
+
def rollback(version:)
|
|
60
|
+
raise NotImplementedError, "Subclasses must implement rollback"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the current status of all services
|
|
64
|
+
#
|
|
65
|
+
# @abstract Subclasses must implement this method
|
|
66
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
67
|
+
# @return [Array<String>] Status information for all services
|
|
68
|
+
def status
|
|
69
|
+
raise NotImplementedError, "Subclasses must implement status"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check the health status of a specific service
|
|
73
|
+
#
|
|
74
|
+
# @param service [String] Service name to check
|
|
75
|
+
# @abstract Subclasses must implement this method
|
|
76
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
77
|
+
# @return [Boolean] True if service is healthy, false otherwise
|
|
78
|
+
def health_check(service:)
|
|
79
|
+
raise NotImplementedError, "Subclasses must implement health_check"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Scale a service to the specified number of replicas
|
|
83
|
+
#
|
|
84
|
+
# @param service [String] Service name to scale
|
|
85
|
+
# @param replicas [Integer] Target number of replicas
|
|
86
|
+
# @abstract Subclasses must implement this method
|
|
87
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
88
|
+
# @return [void]
|
|
89
|
+
def scale(service:, replicas:)
|
|
90
|
+
raise NotImplementedError, "Subclasses must implement scale"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Retrieve logs from a specific service
|
|
94
|
+
#
|
|
95
|
+
# @param service [String] Service name to get logs from
|
|
96
|
+
# @param lines [Integer] Number of log lines to retrieve (default: 100)
|
|
97
|
+
# @abstract Subclasses must implement this method
|
|
98
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
99
|
+
# @return [String] Service logs
|
|
100
|
+
def logs(service:, lines: 100)
|
|
101
|
+
raise NotImplementedError, "Subclasses must implement logs"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
protected
|
|
105
|
+
|
|
106
|
+
# Wait for a service to become healthy with timeout
|
|
107
|
+
#
|
|
108
|
+
# @param service [String] Service name to wait for
|
|
109
|
+
# @param timeout [Integer] Maximum time to wait in seconds (default: 300)
|
|
110
|
+
# @raise [RuntimeError] If the service doesn't become healthy within timeout
|
|
111
|
+
# @return [Boolean] True when service becomes healthy
|
|
112
|
+
# @api private
|
|
113
|
+
def wait_for_health(service, timeout = 300)
|
|
114
|
+
start_time = Time.now
|
|
115
|
+
loop do
|
|
116
|
+
return true if health_check(service: service)
|
|
117
|
+
|
|
118
|
+
raise "Health check timeout for #{service}" if Time.now - start_time > timeout
|
|
119
|
+
|
|
120
|
+
sleep 5
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/gjallarhorn/cli.rb
CHANGED
|
@@ -3,11 +3,35 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "deployer"
|
|
5
5
|
require_relative "configuration"
|
|
6
|
+
require_relative "history"
|
|
6
7
|
|
|
7
8
|
module Gjallarhorn
|
|
9
|
+
# Command-line interface for Gjallarhorn deployment operations
|
|
10
|
+
#
|
|
11
|
+
# The CLI class provides a Thor-based command-line interface for all deployment
|
|
12
|
+
# operations including deploy, status checks, rollbacks, and configuration management.
|
|
13
|
+
# All methods include comprehensive error handling and user-friendly output.
|
|
14
|
+
#
|
|
15
|
+
# @example Deploy to production
|
|
16
|
+
# gjallarhorn deploy production myapp:v1.2.3
|
|
17
|
+
#
|
|
18
|
+
# @example Check status with custom config
|
|
19
|
+
# gjallarhorn status staging --config staging-deploy.yml
|
|
20
|
+
#
|
|
21
|
+
# @example Rollback to previous version
|
|
22
|
+
# gjallarhorn rollback production v1.2.2
|
|
23
|
+
#
|
|
24
|
+
# @since 0.1.0
|
|
8
25
|
class CLI < Thor
|
|
26
|
+
# Default configuration file path
|
|
27
|
+
DEFAULT_CONFIG_FILE = "config/deploy.yml"
|
|
28
|
+
|
|
9
29
|
desc "deploy ENVIRONMENT IMAGE", "Deploy an image to the specified environment"
|
|
10
|
-
option :config, aliases: "-c", default:
|
|
30
|
+
option :config, aliases: "-c", default: DEFAULT_CONFIG_FILE, desc: "Configuration file path"
|
|
31
|
+
# Deploy a container image to the specified environment
|
|
32
|
+
#
|
|
33
|
+
# @param environment [String] Target environment name
|
|
34
|
+
# @param image [String] Container image tag to deploy
|
|
11
35
|
def deploy(environment, image)
|
|
12
36
|
deployer = Deployer.new(options[:config])
|
|
13
37
|
deployer.deploy(environment, image)
|
|
@@ -17,7 +41,10 @@ module Gjallarhorn
|
|
|
17
41
|
end
|
|
18
42
|
|
|
19
43
|
desc "status ENVIRONMENT", "Check deployment status for an environment"
|
|
20
|
-
option :config, aliases: "-c", default:
|
|
44
|
+
option :config, aliases: "-c", default: DEFAULT_CONFIG_FILE, desc: "Configuration file path"
|
|
45
|
+
# Check the deployment status for services in an environment
|
|
46
|
+
#
|
|
47
|
+
# @param environment [String] Target environment name
|
|
21
48
|
def status(environment)
|
|
22
49
|
deployer = Deployer.new(options[:config])
|
|
23
50
|
result = deployer.status(environment)
|
|
@@ -32,7 +59,11 @@ module Gjallarhorn
|
|
|
32
59
|
end
|
|
33
60
|
|
|
34
61
|
desc "rollback ENVIRONMENT VERSION", "Rollback to a previous version"
|
|
35
|
-
option :config, aliases: "-c", default:
|
|
62
|
+
option :config, aliases: "-c", default: DEFAULT_CONFIG_FILE, desc: "Configuration file path"
|
|
63
|
+
# Rollback services in an environment to a previous version
|
|
64
|
+
#
|
|
65
|
+
# @param environment [String] Target environment name
|
|
66
|
+
# @param version [String] Version to rollback to
|
|
36
67
|
def rollback(environment, version)
|
|
37
68
|
deployer = Deployer.new(options[:config])
|
|
38
69
|
deployer.rollback(environment, version)
|
|
@@ -42,7 +73,8 @@ module Gjallarhorn
|
|
|
42
73
|
end
|
|
43
74
|
|
|
44
75
|
desc "config", "Show current configuration"
|
|
45
|
-
option :config, aliases: "-c", default:
|
|
76
|
+
option :config, aliases: "-c", default: DEFAULT_CONFIG_FILE, desc: "Configuration file path"
|
|
77
|
+
# Display the current configuration in YAML format
|
|
46
78
|
def config
|
|
47
79
|
configuration = Configuration.new(options[:config])
|
|
48
80
|
puts configuration.to_yaml
|
|
@@ -51,9 +83,63 @@ module Gjallarhorn
|
|
|
51
83
|
exit 1
|
|
52
84
|
end
|
|
53
85
|
|
|
86
|
+
desc "history ENVIRONMENT", "Show deployment history for an environment"
|
|
87
|
+
option :config, aliases: "-c", default: DEFAULT_CONFIG_FILE, desc: "Configuration file path"
|
|
88
|
+
option :limit, aliases: "-l", type: :numeric, default: 20, desc: "Maximum number of records to show"
|
|
89
|
+
# Display deployment history for the specified environment
|
|
90
|
+
#
|
|
91
|
+
# @param environment [String] Target environment name
|
|
92
|
+
def history(environment)
|
|
93
|
+
history_manager = History.new
|
|
94
|
+
records = history_manager.get_history(environment: environment, limit: options[:limit])
|
|
95
|
+
|
|
96
|
+
if records.empty?
|
|
97
|
+
puts "No deployment history found for #{environment}"
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts "Deployment history for #{environment}:"
|
|
102
|
+
puts "=" * 60
|
|
103
|
+
|
|
104
|
+
records.each do |record|
|
|
105
|
+
display_deployment_record(record)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Show statistics
|
|
109
|
+
stats = history_manager.statistics(environment: environment)
|
|
110
|
+
puts "Statistics:"
|
|
111
|
+
puts " Total deployments: #{stats[:total_deployments]}"
|
|
112
|
+
puts " Success rate: #{stats[:success_rate]}% (#{stats[:successful_deployments]}/#{stats[:total_deployments]})"
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
puts "Failed to retrieve history: #{e.message}"
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
|
|
54
118
|
desc "version", "Show Gjallarhorn version"
|
|
119
|
+
# Display the current Gjallarhorn version
|
|
55
120
|
def version
|
|
56
121
|
puts Gjallarhorn::VERSION
|
|
57
122
|
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Display a single deployment record with formatting
|
|
127
|
+
#
|
|
128
|
+
# @param record [Hash] Deployment record
|
|
129
|
+
# @return [void]
|
|
130
|
+
def display_deployment_record(record)
|
|
131
|
+
timestamp = Time.parse(record["timestamp"]).strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
132
|
+
status_indicator = case record["status"]
|
|
133
|
+
when "success" then "✅"
|
|
134
|
+
when "failed" then "❌"
|
|
135
|
+
else "🔄"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
puts "#{status_indicator} #{timestamp} - #{record["image"]}"
|
|
139
|
+
puts " Status: #{record["status"]}"
|
|
140
|
+
puts " Strategy: #{record["strategy"]}" if record["strategy"]
|
|
141
|
+
puts " Error: #{record["error"]}" if record["error"]
|
|
142
|
+
puts
|
|
143
|
+
end
|
|
58
144
|
end
|
|
59
145
|
end
|
|
@@ -3,14 +3,43 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
module Gjallarhorn
|
|
6
|
+
# Configuration management class for loading and validating deployment configurations
|
|
7
|
+
#
|
|
8
|
+
# The Configuration class handles loading YAML configuration files that define
|
|
9
|
+
# deployment environments, their providers, and associated services. It provides
|
|
10
|
+
# validation to ensure all required fields are present and valid.
|
|
11
|
+
#
|
|
12
|
+
# @example Loading configuration
|
|
13
|
+
# config = Gjallarhorn::Configuration.new('deploy.yml')
|
|
14
|
+
# environments = config.environments
|
|
15
|
+
# production_config = config.environment('production')
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing environment details
|
|
18
|
+
# provider = config.provider_for('staging')
|
|
19
|
+
# services = config.services_for('production')
|
|
20
|
+
#
|
|
21
|
+
# @since 0.1.0
|
|
6
22
|
class Configuration
|
|
7
|
-
|
|
23
|
+
# @return [String] Path to the configuration file
|
|
24
|
+
attr_reader :config_file
|
|
8
25
|
|
|
26
|
+
# @return [Hash] Loaded configuration data
|
|
27
|
+
attr_reader :data
|
|
28
|
+
|
|
29
|
+
# Initialize a new Configuration instance
|
|
30
|
+
#
|
|
31
|
+
# @param config_file [String] Path to the YAML configuration file
|
|
32
|
+
# @raise [ConfigurationError] If the file doesn't exist or contains invalid YAML
|
|
9
33
|
def initialize(config_file = "deploy.yml")
|
|
10
34
|
@config_file = config_file
|
|
11
35
|
load_configuration
|
|
12
36
|
end
|
|
13
37
|
|
|
38
|
+
# Get configuration for a specific environment
|
|
39
|
+
#
|
|
40
|
+
# @param name [String, Symbol] Environment name
|
|
41
|
+
# @raise [ConfigurationError] If the environment is not found
|
|
42
|
+
# @return [Hash] Environment configuration hash
|
|
14
43
|
def environment(name)
|
|
15
44
|
env_config = @data[name.to_s]
|
|
16
45
|
raise ConfigurationError, "Environment '#{name}' not found in #{config_file}" unless env_config
|
|
@@ -18,20 +47,34 @@ module Gjallarhorn
|
|
|
18
47
|
env_config
|
|
19
48
|
end
|
|
20
49
|
|
|
50
|
+
# Get all available environment names
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<String>] List of environment names
|
|
21
53
|
def environments
|
|
22
54
|
@data.keys
|
|
23
55
|
end
|
|
24
56
|
|
|
57
|
+
# Get the provider for a specific environment
|
|
58
|
+
#
|
|
59
|
+
# @param environment [String, Symbol] Environment name
|
|
60
|
+
# @return [String] Provider name (e.g., 'aws', 'gcp')
|
|
25
61
|
def provider_for(environment)
|
|
26
62
|
env_config = environment(environment)
|
|
27
63
|
env_config["provider"]
|
|
28
64
|
end
|
|
29
65
|
|
|
66
|
+
# Get the services configuration for a specific environment
|
|
67
|
+
#
|
|
68
|
+
# @param environment [String, Symbol] Environment name
|
|
69
|
+
# @return [Array<Hash>] List of service configurations
|
|
30
70
|
def services_for(environment)
|
|
31
71
|
env_config = environment(environment)
|
|
32
72
|
env_config["services"] || []
|
|
33
73
|
end
|
|
34
74
|
|
|
75
|
+
# Convert configuration to YAML string
|
|
76
|
+
#
|
|
77
|
+
# @return [String] YAML representation of the configuration
|
|
35
78
|
def to_yaml
|
|
36
79
|
@data.to_yaml
|
|
37
80
|
end
|
|
@@ -56,21 +99,34 @@ module Gjallarhorn
|
|
|
56
99
|
end
|
|
57
100
|
|
|
58
101
|
def validate_environment(env_name, env_config)
|
|
59
|
-
|
|
102
|
+
validate_environment_structure(env_name, env_config)
|
|
103
|
+
validate_provider_field(env_name, env_config)
|
|
104
|
+
validate_provider_value(env_name, env_config["provider"])
|
|
105
|
+
end
|
|
60
106
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
def validate_environment_structure(env_name, env_config)
|
|
108
|
+
return if env_config.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
raise ConfigurationError, "Environment '#{env_name}' is not a hash"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def validate_provider_field(env_name, env_config)
|
|
114
|
+
return if env_config["provider"]
|
|
115
|
+
|
|
116
|
+
raise ConfigurationError,
|
|
117
|
+
"Environment '#{env_name}' missing required 'provider' field"
|
|
118
|
+
end
|
|
65
119
|
|
|
120
|
+
def validate_provider_value(env_name, provider)
|
|
66
121
|
valid_providers = %w[aws gcp azure docker kubernetes]
|
|
67
|
-
provider = env_config["provider"]
|
|
68
122
|
return if valid_providers.include?(provider)
|
|
69
123
|
|
|
124
|
+
valid_list = valid_providers.join(", ")
|
|
70
125
|
raise ConfigurationError,
|
|
71
|
-
"Invalid provider '#{provider}' for environment '#{env_name}'. Must be one of: #{
|
|
126
|
+
"Invalid provider '#{provider}' for environment '#{env_name}'. Must be one of: #{valid_list}"
|
|
72
127
|
end
|
|
73
128
|
end
|
|
74
129
|
|
|
130
|
+
# Raised when configuration loading or validation fails
|
|
75
131
|
class ConfigurationError < Error; end
|
|
76
132
|
end
|
data/lib/gjallarhorn/deployer.rb
CHANGED
|
@@ -2,52 +2,181 @@
|
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
4
|
require_relative "configuration"
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
5
|
+
require_relative "adapter/base"
|
|
6
|
+
require_relative "adapter/aws"
|
|
7
|
+
require_relative "deployment/strategy"
|
|
8
|
+
require_relative "deployment/zero_downtime"
|
|
9
|
+
require_relative "deployment/basic"
|
|
10
|
+
require_relative "deployment/legacy"
|
|
11
|
+
require_relative "proxy/manager"
|
|
12
|
+
require_relative "proxy/nginx_manager"
|
|
13
|
+
require_relative "proxy/traefik_manager"
|
|
14
|
+
require_relative "proxy/kamal_proxy_manager"
|
|
15
|
+
require_relative "history"
|
|
7
16
|
|
|
8
|
-
# Main
|
|
17
|
+
# Main deployment orchestrator that handles deployments across different cloud providers
|
|
18
|
+
#
|
|
19
|
+
# The Deployer class acts as the central coordinator for all deployment operations,
|
|
20
|
+
# managing configuration loading, adapter selection, and deployment execution across
|
|
21
|
+
# different cloud environments.
|
|
22
|
+
#
|
|
23
|
+
# @example Basic deployment
|
|
24
|
+
# deployer = Gjallarhorn::Deployer.new('config/deploy.yml')
|
|
25
|
+
# deployer.deploy('production', 'myapp:v1.2.3')
|
|
26
|
+
#
|
|
27
|
+
# @example Check service status
|
|
28
|
+
# status = deployer.status('staging')
|
|
29
|
+
# puts status.inspect
|
|
30
|
+
#
|
|
31
|
+
# @example Rollback to previous version
|
|
32
|
+
# deployer.rollback('production', 'v1.2.2')
|
|
33
|
+
#
|
|
34
|
+
# @since 0.1.0
|
|
9
35
|
module Gjallarhorn
|
|
36
|
+
# Main deployment orchestrator class
|
|
10
37
|
class Deployer
|
|
38
|
+
# Default configuration file path
|
|
39
|
+
DEFAULT_CONFIG_FILE = "config/deploy.yml"
|
|
40
|
+
|
|
41
|
+
# Mapping of provider names to their corresponding adapter classes
|
|
42
|
+
# @api private
|
|
11
43
|
ADAPTERS = {
|
|
12
|
-
"aws" =>
|
|
44
|
+
"aws" => Adapter::AWSAdapter,
|
|
13
45
|
"gcp" => nil, # TODO: Implement in Phase 2
|
|
14
46
|
"azure" => nil, # TODO: Implement in Phase 2
|
|
15
47
|
"docker" => nil, # TODO: Implement in Phase 2
|
|
16
48
|
"kubernetes" => nil # TODO: Implement in Phase 3
|
|
17
49
|
}.freeze
|
|
18
50
|
|
|
19
|
-
|
|
51
|
+
# @return [Configuration] The loaded deployment configuration
|
|
52
|
+
attr_reader :configuration
|
|
20
53
|
|
|
21
|
-
|
|
54
|
+
# @return [Logger] Logger instance for deployment operations
|
|
55
|
+
attr_reader :logger
|
|
56
|
+
|
|
57
|
+
# Initialize a new Deployer instance
|
|
58
|
+
#
|
|
59
|
+
# @param config_file [String] Path to the YAML configuration file
|
|
60
|
+
# @raise [ConfigurationError] If the configuration file is invalid
|
|
61
|
+
def initialize(config_file = DEFAULT_CONFIG_FILE)
|
|
22
62
|
@configuration = Configuration.new(config_file)
|
|
23
63
|
@logger = Logger.new($stdout)
|
|
64
|
+
@history = History.new
|
|
24
65
|
end
|
|
25
66
|
|
|
26
|
-
|
|
67
|
+
# Deploy a container image to the specified environment
|
|
68
|
+
#
|
|
69
|
+
# @param environment [String] Target environment name (e.g., 'production', 'staging')
|
|
70
|
+
# @param image [String] Container image tag to deploy (e.g., 'myapp:v1.2.3')
|
|
71
|
+
# @param strategy [String] Deployment strategy to use ('zero_downtime', 'rolling', 'basic')
|
|
72
|
+
# @raise [DeploymentError] If the deployment fails or provider is not supported
|
|
73
|
+
# @return [void]
|
|
74
|
+
def deploy(environment, image, strategy: "zero_downtime")
|
|
75
|
+
# Record deployment start
|
|
76
|
+
@history.record_deployment(
|
|
77
|
+
environment: environment,
|
|
78
|
+
image: image,
|
|
79
|
+
status: "started",
|
|
80
|
+
strategy: strategy
|
|
81
|
+
)
|
|
82
|
+
|
|
27
83
|
adapter = create_adapter(environment)
|
|
28
|
-
|
|
84
|
+
deployment_strategy = create_deployment_strategy(strategy, adapter, environment)
|
|
85
|
+
|
|
86
|
+
@logger.info "Deploying #{image} to #{environment} using #{adapter.class.name} with #{strategy} strategy"
|
|
29
87
|
|
|
30
|
-
|
|
88
|
+
deployment_strategy.deploy(
|
|
31
89
|
image: image,
|
|
32
90
|
environment: environment,
|
|
33
91
|
services: @configuration.services_for(environment)
|
|
34
92
|
)
|
|
35
93
|
|
|
36
94
|
@logger.info "Deployment completed successfully"
|
|
95
|
+
|
|
96
|
+
# Record successful deployment
|
|
97
|
+
@history.record_deployment(
|
|
98
|
+
environment: environment,
|
|
99
|
+
image: image,
|
|
100
|
+
status: "success",
|
|
101
|
+
strategy: strategy
|
|
102
|
+
)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
# Record failed deployment
|
|
105
|
+
@history.record_deployment(
|
|
106
|
+
environment: environment,
|
|
107
|
+
image: image,
|
|
108
|
+
status: "failed",
|
|
109
|
+
strategy: strategy,
|
|
110
|
+
error: e.message
|
|
111
|
+
)
|
|
112
|
+
raise
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Deploy with specific strategy (convenience method)
|
|
116
|
+
#
|
|
117
|
+
# @param environment [String] Target environment name
|
|
118
|
+
# @param image [String] Container image tag to deploy
|
|
119
|
+
# @param strategy [String] Deployment strategy to use
|
|
120
|
+
# @return [void]
|
|
121
|
+
def deploy_with_strategy(environment, image, strategy)
|
|
122
|
+
deploy(environment, image, strategy: strategy)
|
|
37
123
|
end
|
|
38
124
|
|
|
125
|
+
# Get the current status of services in the specified environment
|
|
126
|
+
#
|
|
127
|
+
# @param environment [String] Target environment name
|
|
128
|
+
# @raise [DeploymentError] If the provider is not supported
|
|
129
|
+
# @return [Hash] Status information for all services in the environment
|
|
39
130
|
def status(environment)
|
|
40
131
|
adapter = create_adapter(environment)
|
|
41
132
|
adapter.status
|
|
42
133
|
end
|
|
43
134
|
|
|
135
|
+
# Rollback services in the environment to a previous version
|
|
136
|
+
#
|
|
137
|
+
# @param environment [String] Target environment name
|
|
138
|
+
# @param version [String] Version to rollback to (e.g., 'v1.2.2')
|
|
139
|
+
# @raise [DeploymentError] If the rollback fails or provider is not supported
|
|
140
|
+
# @return [void]
|
|
44
141
|
def rollback(environment, version)
|
|
142
|
+
# Record rollback start
|
|
143
|
+
@history.record_deployment(
|
|
144
|
+
environment: environment,
|
|
145
|
+
image: version,
|
|
146
|
+
status: "started",
|
|
147
|
+
strategy: "rollback"
|
|
148
|
+
)
|
|
149
|
+
|
|
45
150
|
adapter = create_adapter(environment)
|
|
46
151
|
adapter.rollback(version: version)
|
|
152
|
+
|
|
153
|
+
# Record successful rollback
|
|
154
|
+
@history.record_deployment(
|
|
155
|
+
environment: environment,
|
|
156
|
+
image: version,
|
|
157
|
+
status: "success",
|
|
158
|
+
strategy: "rollback"
|
|
159
|
+
)
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
# Record failed rollback
|
|
162
|
+
@history.record_deployment(
|
|
163
|
+
environment: environment,
|
|
164
|
+
image: version,
|
|
165
|
+
status: "failed",
|
|
166
|
+
strategy: "rollback",
|
|
167
|
+
error: e.message
|
|
168
|
+
)
|
|
169
|
+
raise
|
|
47
170
|
end
|
|
48
171
|
|
|
49
172
|
private
|
|
50
173
|
|
|
174
|
+
# Create an adapter instance for the specified environment
|
|
175
|
+
#
|
|
176
|
+
# @param environment [String] Target environment name
|
|
177
|
+
# @raise [DeploymentError] If the provider is not supported
|
|
178
|
+
# @return [Adapter::Base] Configured adapter instance
|
|
179
|
+
# @api private
|
|
51
180
|
def create_adapter(environment)
|
|
52
181
|
env_config = @configuration.environment(environment)
|
|
53
182
|
provider = env_config["provider"]
|
|
@@ -57,7 +186,48 @@ module Gjallarhorn
|
|
|
57
186
|
|
|
58
187
|
adapter_class.new(env_config)
|
|
59
188
|
end
|
|
189
|
+
|
|
190
|
+
# Create a deployment strategy instance
|
|
191
|
+
#
|
|
192
|
+
# @param strategy_name [String] Strategy name ('zero_downtime', 'rolling', 'basic')
|
|
193
|
+
# @param adapter [Adapter::Base] Adapter instance
|
|
194
|
+
# @param environment [String] Target environment name
|
|
195
|
+
# @raise [DeploymentError] If the strategy is not supported
|
|
196
|
+
# @return [Deployment::Strategy] Deployment strategy instance
|
|
197
|
+
# @api private
|
|
198
|
+
def create_deployment_strategy(strategy_name, adapter, environment)
|
|
199
|
+
proxy_manager = create_proxy_manager(environment) if strategy_name == "zero_downtime"
|
|
200
|
+
|
|
201
|
+
case strategy_name
|
|
202
|
+
when "zero_downtime"
|
|
203
|
+
Deployment::ZeroDowntime.new(adapter, proxy_manager, @logger)
|
|
204
|
+
when "basic"
|
|
205
|
+
Deployment::Basic.new(adapter, proxy_manager, @logger)
|
|
206
|
+
when "legacy"
|
|
207
|
+
Deployment::Legacy.new(adapter, proxy_manager, @logger)
|
|
208
|
+
else
|
|
209
|
+
raise DeploymentError, "Deployment strategy '#{strategy_name}' not yet implemented"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Create a proxy manager instance for zero-downtime deployments
|
|
214
|
+
#
|
|
215
|
+
# @param environment [String] Target environment name
|
|
216
|
+
# @return [Proxy::Manager, nil] Proxy manager instance or nil if not configured
|
|
217
|
+
# @api private
|
|
218
|
+
def create_proxy_manager(environment)
|
|
219
|
+
env_config = @configuration.environment(environment)
|
|
220
|
+
proxy_config = env_config["proxy"]
|
|
221
|
+
|
|
222
|
+
return nil unless proxy_config
|
|
223
|
+
|
|
224
|
+
Proxy::Manager.create(proxy_config.transform_keys(&:to_sym), @logger)
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
@logger.warn "Failed to create proxy manager: #{e.message}"
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
60
229
|
end
|
|
61
230
|
|
|
231
|
+
# Raised when deployment operations fail
|
|
62
232
|
class DeploymentError < Error; end
|
|
63
233
|
end
|