lex-onboard 0.1.1 → 0.2.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: 74c904f4f9278bbb31dfeacf303ec989e91d85cc0947935d85c1119f7ab98200
4
- data.tar.gz: 6e1f50f1fa14308abfdd4e4bf41233c993dafeaac6ef11afdb3f6d602bbc78a4
3
+ metadata.gz: 945d2252b6dec50ef82d5ff441ef39099c146b0a1dbd7300e1eb9a10238f9bc3
4
+ data.tar.gz: 4213f845c3c1211175427ab0f98381c11832640da13556fbc30dcf7264723867
5
5
  SHA512:
6
- metadata.gz: 29b4496a85b322f17349909214054ef4c9ab5e55ac857d01a88d1dbc234e219e84f17e5b8252edf7501dd80321bd5e842e5b6ca21b35d44cdae667c768bd2106
7
- data.tar.gz: 65fbd780e1adf5c12c8eaee6cfd712f9052c9e3eadc23e21c9f064c3a25abfe2614f9fb3dd9b845ac444522b0026b9e6e6eaa4331b3aadb3f2f0331fd94373f4
6
+ metadata.gz: 966c8d189ef9f44a6c77c44b65eedcf582f4ac9dfca8dd2a6fc61223d48d29c7057aad5633095e08ad5d1bd94d5a3657dc225b5c13110ef36ae247eca3937946
7
+ data.tar.gz: 63bece2311db70ff269547be69007c130d302f5c415e1792aeea6e55ef905d77108bd1a2af880ae4b5bda5e67c7b92e1595c5662a0e64ea0eb6c17facd0b46f6
@@ -5,55 +5,125 @@ module Legion
5
5
  module Onboard
6
6
  module Runners
7
7
  module Provision
8
+ include Validator
9
+
10
+ ROLLBACK_ACTIONS = {
11
+ vault_namespace: ->(client, askid) { client.delete_namespace(name: askid) },
12
+ consul_partition: ->(client, askid) { client.delete_partition(name: askid) },
13
+ tfe_project: ->(client, askid) { client.delete_project(name: askid) }
14
+ }.freeze
15
+
8
16
  def provision(askid:, tfe_organization: 'terraform.uhg.com', requester_slack_webhook: nil, **)
17
+ validation = validate_askid(askid: askid)
18
+ return { status: 'rejected', askid: askid, reason: validation[:reason] } unless validation[:valid]
19
+
20
+ conflicts = check_conflicts(askid: askid)
21
+ unless conflicts[:conflicts].empty?
22
+ return { status: 'rejected', askid: askid, reason: "conflict in: #{conflicts[:conflicts].join(', ')}" }
23
+ end
24
+
25
+ completed_steps = []
9
26
  steps = []
10
27
 
11
- steps << run_step('vault_namespace') { create_vault_namespace(name: askid) }
12
- steps << run_step('consul_partition') { create_consul_partition(name: askid) }
13
- steps << run_step('tfe_project') { create_tfe_project(name: askid, organization: tfe_organization) }
14
- steps << run_step('notify') { notify_requester(askid: askid, webhook: requester_slack_webhook) }
28
+ %i[vault_namespace consul_partition tfe_project].each do |step_name|
29
+ result = run_step(step_name, askid: askid, tfe_organization: tfe_organization)
30
+ steps << result
31
+
32
+ if result[:status] == 'error'
33
+ rollback_results = rollback(completed_steps, askid: askid)
34
+ return { status: 'failed', askid: askid, steps: steps, rollback: rollback_results }
35
+ end
15
36
 
16
- failed = steps.any? { |s| s[:status] == 'error' }
37
+ completed_steps << step_name if result[:status] == 'completed'
38
+ end
17
39
 
18
- {
19
- status: failed ? 'failed' : 'completed',
20
- askid: askid,
21
- steps: steps
22
- }
40
+ notify_requester(askid: askid, webhook: requester_slack_webhook) if requester_slack_webhook
41
+ { status: 'completed', askid: askid, steps: steps, rollback: [] }
23
42
  end
24
43
 
25
44
  private
26
45
 
27
- def run_step(name)
28
- yield
29
- { name: name, status: 'ok' }
46
+ def run_step(step_name, askid:, tfe_organization:)
47
+ case step_name
48
+ when :vault_namespace then vault_namespace(askid: askid)
49
+ when :consul_partition then consul_partition(askid: askid)
50
+ when :tfe_project then tfe_project(askid: askid, organization: tfe_organization)
51
+ end
52
+ end
53
+
54
+ def rollback(completed_steps, askid:)
55
+ completed_steps.reverse.map do |step_name|
56
+ action = ROLLBACK_ACTIONS[step_name]
57
+ next unless action
58
+
59
+ action.call(client_for_step(step_name), askid)
60
+ { step: step_name, status: 'rolled_back' }
61
+ rescue StandardError => e
62
+ { step: step_name, status: 'rollback_failed', error: e.message }
63
+ end.compact
64
+ end
65
+
66
+ def client_for_step(step_name)
67
+ case step_name
68
+ when :vault_namespace then vault_client
69
+ when :consul_partition then consul_client
70
+ when :tfe_project then tfe_client
71
+ end
72
+ end
73
+
74
+ def vault_namespace(askid:)
75
+ unless defined?(Legion::Extensions::Vault::Client)
76
+ return { step: :vault_namespace, status: 'skipped', reason: 'vault unavailable' }
77
+ end
78
+ return { step: :vault_namespace, status: 'skipped', reason: 'already exists' } if vault_exists?(askid)
79
+
80
+ vault_client.create_namespace(name: askid)
81
+ { step: :vault_namespace, status: 'completed', askid: askid }
30
82
  rescue StandardError => e
31
- { name: name, status: 'error', error: e.message }
83
+ { step: :vault_namespace, status: 'error', error: e.message }
32
84
  end
33
85
 
34
- def create_vault_namespace(name:)
35
- return { result: false } unless defined?(Legion::Extensions::Vault::Client)
86
+ def consul_partition(askid:)
87
+ unless defined?(Legion::Extensions::Consul::Client)
88
+ return { step: :consul_partition, status: 'skipped', reason: 'consul unavailable' }
89
+ end
90
+ return { step: :consul_partition, status: 'skipped', reason: 'already exists' } if consul_exists?(askid)
36
91
 
37
- Legion::Extensions::Vault::Client.new.create_namespace(name: name)
92
+ consul_client.create_partition(name: askid)
93
+ { step: :consul_partition, status: 'completed', askid: askid }
94
+ rescue StandardError => e
95
+ { step: :consul_partition, status: 'error', error: e.message }
38
96
  end
39
97
 
40
- def create_consul_partition(name:)
41
- return { result: false } unless defined?(Legion::Extensions::Consul::Client)
98
+ def tfe_project(askid:, organization:)
99
+ unless defined?(Legion::Extensions::Tfe::Client)
100
+ return { step: :tfe_project, status: 'skipped', reason: 'tfe unavailable' }
101
+ end
102
+ return { step: :tfe_project, status: 'skipped', reason: 'already exists' } if tfe_exists?(askid)
42
103
 
43
- Legion::Extensions::Consul::Client.new.create_partition(name: name)
104
+ tfe_client.create_project(organization: organization, name: askid)
105
+ { step: :tfe_project, status: 'completed', askid: askid }
106
+ rescue StandardError => e
107
+ { step: :tfe_project, status: 'error', error: e.message }
108
+ end
109
+
110
+ def vault_client
111
+ Legion::Extensions::Vault::Client.new
44
112
  end
45
113
 
46
- def create_tfe_project(name:, organization:)
47
- return { result: false } unless defined?(Legion::Extensions::Tfe::Client)
114
+ def consul_client
115
+ Legion::Extensions::Consul::Client.new
116
+ end
48
117
 
49
- Legion::Extensions::Tfe::Client.new.create_project(organization: organization, name: name)
118
+ def tfe_client
119
+ Legion::Extensions::Tfe::Client.new
50
120
  end
51
121
 
52
122
  def notify_requester(askid:, webhook: nil)
53
123
  return true unless webhook && defined?(Legion::Extensions::Slack::Client)
54
124
 
55
125
  Legion::Extensions::Slack::Client.new.send_webhook(
56
- webhook: webhook, text: "Onboarding complete for #{askid}"
126
+ webhook: webhook, message: "Onboarding complete for #{askid}"
57
127
  )
58
128
  rescue StandardError
59
129
  true
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Onboard
6
+ module Runners
7
+ module Validator
8
+ ASKID_PATTERN = /\A[a-z0-9]([a-z0-9-]*[a-z0-9])?\z/
9
+ MAX_ASKID_LENGTH = 63
10
+
11
+ def validate_askid(askid:)
12
+ return { valid: false, reason: 'askid is empty' } if askid.nil? || askid.empty?
13
+ return { valid: false, reason: "askid exceeds #{MAX_ASKID_LENGTH} characters" } if askid.length > MAX_ASKID_LENGTH
14
+
15
+ unless askid.match?(ASKID_PATTERN)
16
+ return { valid: false,
17
+ reason: 'askid format invalid — must be lowercase alphanumeric with hyphens' }
18
+ end
19
+
20
+ { valid: true }
21
+ end
22
+
23
+ def check_conflicts(askid:)
24
+ conflicts = []
25
+ conflicts << :vault if vault_exists?(askid)
26
+ conflicts << :consul if consul_exists?(askid)
27
+ conflicts << :tfe if tfe_exists?(askid)
28
+
29
+ { conflicts: conflicts, askid: askid }
30
+ end
31
+
32
+ private
33
+
34
+ def vault_exists?(askid)
35
+ return false unless defined?(Legion::Extensions::Vault::Client)
36
+
37
+ Legion::Extensions::Vault::Client.new.list_namespaces[:namespaces]&.include?(askid)
38
+ rescue StandardError
39
+ false
40
+ end
41
+
42
+ def consul_exists?(askid)
43
+ return false unless defined?(Legion::Extensions::Consul::Client)
44
+
45
+ Legion::Extensions::Consul::Client.new.list_partitions[:partitions]&.any? { |p| p[:name] == askid }
46
+ rescue StandardError
47
+ false
48
+ end
49
+
50
+ def tfe_exists?(askid)
51
+ return false unless defined?(Legion::Extensions::Tfe::Client)
52
+
53
+ Legion::Extensions::Tfe::Client.new.list_projects[:projects]&.any? { |p| p[:name] == askid }
54
+ rescue StandardError
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Onboard
6
- VERSION = '0.1.1'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'onboard/version'
4
+ require_relative 'onboard/runners/validator'
4
5
  require_relative 'onboard/runners/provision'
5
6
 
6
7
  module Legion
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-onboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -118,6 +118,7 @@ files:
118
118
  - lib/legion/extensions/onboard.rb
119
119
  - lib/legion/extensions/onboard/actors/provision.rb
120
120
  - lib/legion/extensions/onboard/runners/provision.rb
121
+ - lib/legion/extensions/onboard/runners/validator.rb
121
122
  - lib/legion/extensions/onboard/version.rb
122
123
  homepage: https://github.com/LegionIO
123
124
  licenses: