afterlife 1.3.0 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 785bb4c634a1b73338a56cb64b6119389ca07904457159b14af3a00ad48869f0
4
- data.tar.gz: 0f24b2547eb32ab72ae1807f2c5bcb813a5cc09f9dee589f6343bbd047c32c26
3
+ metadata.gz: e03a1499e1af905014e5fb48d8865601a0bb354a856d56dc0a629a3b3c08a99b
4
+ data.tar.gz: 345db972b71109a69d3117e3ef5e3a8fc8cc4aec094c230f9b49ea46e2c206e3
5
5
  SHA512:
6
- metadata.gz: 3ff42032daec81547cc0075ab8ed71d250b2cec8ac677383f6a9702e75124ebb27d173c085c8e5d155cfd9c0b63f49e79c3a7f35630de88424127816f3de4593
7
- data.tar.gz: 48d016ace44ecc8f285eee917e6493766bc914adc324cc582f0e7a7c495fdc9ede0548f5c3529fb6ac296a2e529ed8b83a0208c5f9ae87e5d6d43eafad540afc
6
+ metadata.gz: 2a4b14afb3f286756663da5d5c6c039cd0c327cf8a9ad60a3c20b04d994fdba73b2270892b209cbf9ece8f424e17e2e6eab9798f93416c254aa4549b486a41fc
7
+ data.tar.gz: 89e9ec3e6d10345f8baeea198d0dde968d96b438cc5312a7d2f545176b4755bd6953b0ad0ad60663015c33b8ac477f5bacb3aa1948bc320a1495cfd1602396ef
data/lib/afterlife/cli.rb CHANGED
@@ -31,12 +31,14 @@ module Afterlife
31
31
  desc 'deploy <stage>', 'Deploy current repo'
32
32
  option 'no-build', type: :boolean
33
33
  option 'no-install', type: :boolean
34
+ option 'no-auth', type: :boolean
35
+ option 'no-apply', type: :boolean
34
36
  option 'dry-run', type: :boolean
35
37
  option 'skip-after-hooks', type: :boolean
36
38
  option :yes, type: :boolean
37
39
  def deploy(stage)
38
40
  invoke Deploy::Cli, :call, [stage], options
39
- rescue Deploy::Error
41
+ rescue Error
40
42
  fatal!(e.message)
41
43
  rescue Interrupt
42
44
  log_interrupted
@@ -11,14 +11,14 @@ module Afterlife
11
11
  desc 'deploy <stage>', 'Deploy current repo'
12
12
  def call(stage)
13
13
  Afterlife.cli = self
14
- Deploy.call(stage) do |deployment|
14
+ Deploy.call(stage, options) do |deployment|
15
15
  setup_deploy(deployment)
16
16
  say_status 'Deploying', deployment.initial_message
17
17
  deployment.run
18
18
  say_status 'Deployed', deployment.output
19
19
  run_after_hooks
20
20
  end
21
- rescue Deploy::Error, Afterlife::Error => e
21
+ rescue Afterlife::Error => e
22
22
  fatal!(e.message)
23
23
  end
24
24
 
@@ -3,6 +3,13 @@
3
3
  module Afterlife
4
4
  module Deploy
5
5
  class Deployment
6
+
7
+ attr_reader :options
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
6
13
  def run
7
14
  fail NotImplementedError
8
15
  end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Afterlife
4
+ module Deploy
5
+ class KubernetesDeployment < Deployment
6
+ def setup
7
+ Afterlife.current_repo.env.set('PLATFORM' => 'linux/amd64') if deploy_stage?
8
+
9
+ Afterlife.current_repo.env.set!(
10
+ 'REGISTRY' => registry,
11
+ 'TAG' => repo.current_revision,
12
+ )
13
+ end
14
+
15
+ def run
16
+ Exec.run(commands)
17
+ end
18
+
19
+ def confirmation_message
20
+ 'You are about to deploy the current directory'
21
+ end
22
+
23
+ private
24
+
25
+ def commands
26
+ [
27
+ authenticate_command,
28
+ build_command,
29
+ set_image_command,
30
+ apply_kubernetes_settings,
31
+ ].flatten.compact
32
+ end
33
+
34
+ # commands
35
+
36
+ def authenticate_command
37
+ return if options['no-auth'] || local_stage?
38
+
39
+ AwsAuth.new(registry).commands
40
+ end
41
+
42
+ def build_command
43
+ return if options['no-build']
44
+
45
+ <<-BASH
46
+ docker buildx bake -f docker-bake.hcl #{targets.join(' ')} #{local_stage? ? '--load' : '--push'}
47
+ BASH
48
+ end
49
+
50
+ def set_image_command
51
+ <<-BASH
52
+ cd #{kubelocation} &&
53
+ #{
54
+ targets.map do |target|
55
+ "kustomize edit set image #{full_image_name(target)}:latest=#{registry_image_name(target)}"
56
+ end.join(' && ')
57
+ }
58
+ BASH
59
+ end
60
+
61
+ def targets
62
+ repo.conf.dig(:deploy, :targets) || %w[app]
63
+ end
64
+
65
+ def registry_image_name(target)
66
+ "#{registry}/#{full_image_name(target)}:#{repo.current_revision}"
67
+ end
68
+
69
+ def full_image_name(target)
70
+ return image_name if target == 'app'
71
+
72
+ "#{image_name}-#{target}"
73
+ end
74
+
75
+ def apply_kubernetes_settings
76
+ return if options['no-apply']
77
+
78
+ <<-BASH
79
+ kubectl apply -k #{kubelocation}
80
+ BASH
81
+ end
82
+
83
+ # utils
84
+
85
+ def local_stage?
86
+ Afterlife.current_stage.name.to_sym == :local
87
+ end
88
+
89
+ def deploy_stage?
90
+ %i[qa production staging].include?(Afterlife.current_stage.name.to_sym)
91
+ end
92
+
93
+ def kubelocation
94
+ ".afterlife/#{Afterlife.current_stage.name}"
95
+ end
96
+
97
+ def image_name
98
+ @image_name ||= repo.conf.dig(:deploy, :image_name).tap do |result|
99
+ fail Error, 'deploy.image_name for kubernetes deployments' unless result
100
+ end
101
+ end
102
+
103
+ # Priority:
104
+ # 1. Uses AFTERLIFE_DEPLOY_REGISTRY from the ENV if it's defined. It can be
105
+ # either in real system ENV, or defined in the env section of the repo
106
+ # .afterlife.yml config
107
+ # 2. Uses repo .afterlife.yml config at deploy.registry
108
+ # 3. Uses stages.$current_stage.registry if exists in ~/.afterlife/config.yml
109
+ def registry
110
+ @registry ||= Afterlife.current_repo.variable('deploy.registry') || Afterlife.current_stage.registry
111
+ end
112
+
113
+ class AwsAuth
114
+ attr_reader :registry
115
+
116
+ def initialize(registry)
117
+ @registry = registry
118
+ end
119
+
120
+ def commands
121
+ [docker_login]
122
+ end
123
+
124
+ def docker_login
125
+ <<-BASH
126
+ echo "#{aws_ecr_token}" | docker login --username AWS --password-stdin #{registry}
127
+ BASH
128
+ end
129
+
130
+ def aws_ecr_token
131
+ @aws_ecr_token ||= Exec.result("aws ecr get-login-password --region #{region}")
132
+ end
133
+
134
+ def region
135
+ @region ||= registry.gsub(/[^.]+\.dkr\.ecr\.([^.]+)\.amazonaws\.com/, '\1')
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -4,11 +4,9 @@ module Afterlife
4
4
  module Deploy
5
5
  module_function
6
6
 
7
- Error = Class.new StandardError
8
-
9
- def call(stage)
10
- deployment = klass.new
11
- Afterlife.current_stage = find_stage(stage)
7
+ def call(stage, options)
8
+ deployment = klass.new(options)
9
+ Afterlife.current_stage = stage
12
10
  fill_env
13
11
  deployment.setup
14
12
  yield deployment
@@ -28,16 +26,6 @@ module Afterlife
28
26
  end
29
27
  end
30
28
 
31
- def find_stage(stage)
32
- return Stage.build(stage) unless Afterlife.config.file_exist?
33
-
34
- unless Afterlife.config.stages.key?(stage.to_sym)
35
- fail Error, "invalid stage '#{stage}'. Possible values: #{Afterlife.config.stages.keys.map(&:to_s)}"
36
- end
37
-
38
- Afterlife.config.stages[stage.to_sym]
39
- end
40
-
41
29
  def fill_env
42
30
  # in nested deployments we need the current definition, not the parent's
43
31
  Afterlife.current_repo.env.set!(
@@ -21,21 +21,19 @@ module Afterlife
21
21
  from_stage_envs
22
22
  end
23
23
 
24
- def env_hash_by_branch
25
- @env_hash_by_branch ||= env_hash&.dig(:by_branch, repo.current_branch.to_sym)
26
- end
27
-
28
- def env_hash_by_stage
29
- @env_hash_by_stage ||= env_hash&.dig(:by_stage, Afterlife.current_stage.name.to_sym)
30
- end
24
+ def [](key)
25
+ key = key.gsub('.', '_').upcase
26
+ env_name = "AFTERLIFE_#{key}"
27
+ return ENV[env_name] if ENV.key?(env_name)
31
28
 
32
- def env_hash
33
- @env_hash ||= repo.conf.dig(:deploy, :env)
29
+ @env[env_name]
34
30
  end
35
31
 
36
32
  # soft set, do not override existent ENVs
37
33
  def set(arg)
38
34
  arg.each do |k, v|
35
+ fail ArgumentError, "key '#{k}' must be a string" unless k.is_a?(String)
36
+
39
37
  @env[k] ||= ENV.fetch(k, v)
40
38
  end
41
39
  end
@@ -43,6 +41,8 @@ module Afterlife
43
41
  # forces to be exactly dthe value
44
42
  def set!(arg)
45
43
  arg.each do |k, v|
44
+ fail ArgumentError, "key '#{k}' must be a string" unless k.is_a?(String)
45
+
46
46
  @env[k] = v
47
47
  end
48
48
  end
@@ -50,9 +50,9 @@ module Afterlife
50
50
  private
51
51
 
52
52
  def from_flat_envs
53
- return unless env_hash.is_a?(Hash)
53
+ return unless repo_env_config.is_a?(Hash)
54
54
 
55
- env_hash.each do |key, value|
55
+ repo_env_config.each do |key, value|
56
56
  next if key == :by_branch
57
57
  next if key == :by_stage
58
58
 
@@ -68,6 +68,10 @@ module Afterlife
68
68
  end
69
69
  end
70
70
 
71
+ def env_hash_by_branch
72
+ @env_hash_by_branch ||= repo_env_config&.dig(:by_branch, repo.current_branch.to_sym)
73
+ end
74
+
71
75
  def from_stage_envs
72
76
  return unless env_hash_by_stage.is_a?(Hash)
73
77
 
@@ -75,5 +79,15 @@ module Afterlife
75
79
  @env[key.to_s] = value.to_s
76
80
  end
77
81
  end
82
+
83
+ def env_hash_by_stage
84
+ return unless Afterlife.current_stage
85
+
86
+ @env_hash_by_stage ||= repo_env_config&.dig(:by_stage, Afterlife.current_stage.name.to_sym)
87
+ end
88
+
89
+ def repo_env_config
90
+ @repo_env_config ||= repo.conf.dig(:deploy, :env) || {}
91
+ end
78
92
  end
79
93
  end
@@ -5,7 +5,11 @@ require 'active_support/core_ext/string'
5
5
 
6
6
  module Afterlife
7
7
  class Exec
8
- Error = Class.new StandardError
8
+ def self.result(arg)
9
+ fail Error, 'Exec.result only accepts strings' unless arg.is_a?(String)
10
+
11
+ new(arg).run(result: true).first
12
+ end
9
13
 
10
14
  def self.run(arg)
11
15
  new(arg).run
@@ -23,11 +27,14 @@ module Afterlife
23
27
  @commands = Array(arg)
24
28
  end
25
29
 
26
- def run
27
- parsed_commands.each do |command|
30
+ def run(result: false)
31
+ parsed_commands.map do |command|
28
32
  Afterlife.cli.log_info(command) if Afterlife.cli.options['verbose']
29
- system(env_hash, command.squish, exception: true) unless Afterlife.cli.options['dry-run']
30
- end
33
+ next if Afterlife.cli.options['dry-run']
34
+ next `#{command}`.squish if result
35
+
36
+ system(env_hash, command.squish, exception: true)
37
+ end.compact
31
38
  rescue RuntimeError => e
32
39
  raise Error, e
33
40
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Afterlife
4
4
  class Repo
5
+ # Reads configuration from the repo,
6
+ # either from YML or from the repo instance methods
5
7
  class Config
6
8
  Error = Class.new StandardError
7
9
 
@@ -12,7 +14,7 @@ module Afterlife
12
14
  end
13
15
 
14
16
  def initialize(path)
15
- @path = path.split('.').map(&:to_sym)
17
+ @path = path
16
18
  end
17
19
 
18
20
  def read(&block) # rubocop:disable Metrics/MethodLength
@@ -24,7 +26,7 @@ module Afterlife
24
26
  when String, Symbol
25
27
  yield(from_yml)
26
28
  else
27
- fail Error, "No config with path '#{path.join('.')}'" unless repo_method?
29
+ fail Error, "No config with path '#{path}'" unless repo_method?
28
30
 
29
31
  yield(repo.public_send(repo_method))
30
32
  end
@@ -33,7 +35,7 @@ module Afterlife
33
35
  private
34
36
 
35
37
  def repo_method?
36
- path.count == 1 && repo.public_methods(false).include?(repo_method)
38
+ path.count('.').zero? && repo.public_methods(false).include?(repo_method)
37
39
  end
38
40
 
39
41
  def repo_method
@@ -47,7 +49,7 @@ module Afterlife
47
49
  def from_yml
48
50
  return @from_yml if defined?(@from_yml)
49
51
 
50
- @from_yml ||= repo.conf.dig(*path)
52
+ @from_yml ||= repo.variable(path)
51
53
  end
52
54
  end
53
55
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Afterlife
4
4
  class Repo
5
+ # Reads (and builds) the .afterlife.yml file in the repo
5
6
  class Provider
6
7
  CONFIG_NAME = '.afterlife.yml'
7
8
 
@@ -46,6 +46,14 @@ module Afterlife
46
46
  full_path.join(conf.dig(:cdn, :dist) || DEFAULT_DIST)
47
47
  end
48
48
 
49
+ # 1. Searchs for the AFTERLIFE_#{name} env var
50
+ # 2. Searchs the path in the repo .afterlife.yml config
51
+ def variable(name)
52
+ return env[name] if env[name]
53
+
54
+ conf.dig(*name.split('.').map(&:to_sym))
55
+ end
56
+
49
57
  def env
50
58
  @env ||= Environment.from(self)
51
59
  end
@@ -109,10 +117,6 @@ module Afterlife
109
117
  @package_json ||= JSON.parse(File.read(@pkg_path), symbolize_names: true)
110
118
  end
111
119
 
112
- def branch_conf
113
- conf.dig(:deploy, :env, :by_branch, current_branch.to_sym)
114
- end
115
-
116
120
  def conf
117
121
  @conf ||= Provider.new(full_path).config
118
122
  end
@@ -6,6 +6,7 @@ module Afterlife
6
6
  :bucket,
7
7
  :region,
8
8
  :cdn_url,
9
+ :registry,
9
10
  keyword_init: true,
10
11
  ) do
11
12
  def self.build(name = nil)
@@ -14,7 +15,18 @@ module Afterlife
14
15
  bucket: ENV.fetch('AWS_BUCKET', nil),
15
16
  region: ENV.fetch('AWS_REGION'),
16
17
  cdn_url: ENV.fetch('AWS_BUCKET', nil),
18
+ registry: ENV.fetch('AFTERLIFE_DEPLOY_REGISTRY', nil),
17
19
  )
18
20
  end
21
+
22
+ def self.find(stage)
23
+ return build(stage) unless Afterlife.config.file_exist?
24
+
25
+ unless Afterlife.config.stages.key?(stage.to_sym)
26
+ fail Error, "invalid stage '#{stage}'. Possible values: #{Afterlife.config.stages.keys.map(&:to_s)}"
27
+ end
28
+
29
+ Afterlife.config.stages[stage.to_sym]
30
+ end
19
31
  end
20
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Afterlife
4
- VERSION = '1.3.0'
4
+ VERSION = '1.5.0'
5
5
  end
data/lib/afterlife.rb CHANGED
@@ -33,7 +33,7 @@ module Afterlife
33
33
  end
34
34
 
35
35
  def self.current_stage=(other)
36
- @current_stage = other
36
+ @current_stage = Stage.find(other)
37
37
  end
38
38
 
39
39
  def self.current_stage
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afterlife
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Genaro Madrid
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-21 00:00:00.000000000 Z
11
+ date: 2024-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -130,6 +130,7 @@ files:
130
130
  - lib/afterlife/deploy/cli.rb
131
131
  - lib/afterlife/deploy/custom_deployment.rb
132
132
  - lib/afterlife/deploy/deployment.rb
133
+ - lib/afterlife/deploy/kubernetes_deployment.rb
133
134
  - lib/afterlife/environment.rb
134
135
  - lib/afterlife/exec.rb
135
136
  - lib/afterlife/release.rb