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.
@@ -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
@@ -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: "deploy.yml", desc: "Configuration file path"
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: "deploy.yml", desc: "Configuration file path"
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: "deploy.yml", desc: "Configuration file path"
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: "deploy.yml", desc: "Configuration file path"
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
- attr_reader :config_file, :data
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
- raise ConfigurationError, "Environment '#{env_name}' is not a hash" unless env_config.is_a?(Hash)
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
- unless env_config["provider"]
62
- raise ConfigurationError,
63
- "Environment '#{env_name}' missing required 'provider' field"
64
- end
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: #{valid_providers.join(", ")}"
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
@@ -2,52 +2,181 @@
2
2
 
3
3
  require "logger"
4
4
  require_relative "configuration"
5
- require_relative "adapters/base"
6
- require_relative "adapters/aws"
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 Deployer Class
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" => Adapters::AWSAdapter,
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
- attr_reader :configuration, :logger
51
+ # @return [Configuration] The loaded deployment configuration
52
+ attr_reader :configuration
20
53
 
21
- def initialize(config_file = "deploy.yml")
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
- def deploy(environment, image)
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
- @logger.info "Deploying #{image} to #{environment} using #{adapter.class.name}"
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
- adapter.deploy(
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