afterlife 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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