-
Notifications
You must be signed in to change notification settings - Fork 233
/
environment_variable.rb
169 lines (142 loc) · 5.99 KB
/
environment_variable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# frozen_string_literal: true
require 'validates_lengths_from_database'
require 'aws-sdk-s3'
class EnvironmentVariable < ActiveRecord::Base
FAILED_LOOKUP_MARK = ' X' # SpaceX
PARENT_PRIORITY = ["Deploy", "Stage", "Project", "EnvironmentVariableGroup"].freeze
include GroupScope
extend Inlinable
audited
belongs_to :parent, polymorphic: true # Resource they are set on
validates :name, presence: true
validates :parent_type, inclusion: PARENT_PRIORITY
include ValidatesLengthsFromDatabase
validates_lengths_from_database only: :value
allow_inline delegate :name, to: :parent, prefix: true, allow_nil: true
allow_inline delegate :name, to: :scope, prefix: true, allow_nil: true
class << self
# preview parameter can be used to not raise an error,
# but return a value with a helpful message
# also used by an external plugin
def env(deploy, deploy_group, preview: false, resolve_secrets: true, project_specific: nil)
env = {}
if deploy_group && deploy.project.config_service?
env.merge! env_vars_from_config_service(deploy, deploy_group)
end
if deploy_group && (env_repo_name = ENV["DEPLOYMENT_ENV_REPO"]) && deploy.project.use_env_repo
env.merge! env_vars_from_repo(env_repo_name, deploy.project, deploy_group)
end
env.merge! env_vars_from_db(deploy, deploy_group, project_specific: project_specific)
resolve_dollar_variables(env)
resolve_secrets(deploy.project, deploy_group, env, preview: preview) if resolve_secrets
env
end
# scopes is given as argument since it needs to be cached
def sort_by_scopes(variables, scopes)
variables.sort_by { |x| [x.name, scopes.index { |_, s| s == x.scope_type_and_id } || 999] }
end
# env_scopes is given as argument since it needs to be cached
def serialize(variables, env_scopes)
sorted = EnvironmentVariable.sort_by_scopes(variables, env_scopes)
sorted.map do |var|
"#{var.name}=#{var.value.inspect} # #{var.scope&.name || "All"}"
end.join("\n")
end
# bucket and key for reading OR url for display
# NOTE: `deploy_group` already signals if it is for `display`, but I want it to be explicit
def config_service_location(project, deploy_group, display:)
prefix = "samson/#{project.permalink}"
if display
return unless bucket = ENV["CONFIG_SERVICE_BUCKET"]
"s3://#{bucket}/#{prefix}"
else
bucket = ENV.fetch "CONFIG_SERVICE_BUCKET"
[bucket, "#{prefix}/#{deploy_group.permalink}.yml"]
end
end
private
def env_vars_from_db(deploy, deploy_group, project_specific: nil)
# Project Specific:
# nil => project env + groups env
# true => project env
# false => groups env
project_envs =
if project_specific.to_s == "true"
deploy.project.environment_variables
elsif project_specific.to_s == "false"
deploy.project.environment_variable_groups.flat_map(&:environment_variables)
else
deploy.project.nested_environment_variables
end
variables =
deploy.environment_variables +
(deploy.stage&.environment_variables || []) +
project_envs
variables.sort_by!(&:priority)
variables.each_with_object({}) do |ev, all|
all[ev.name] = ev.value if !all[ev.name] && ev.matches_scope?(deploy_group)
end
end
def env_vars_from_repo(env_repo_name, project, deploy_group)
path = "generated/#{project.permalink}/#{deploy_group.permalink}.env"
content = GITHUB.contents(env_repo_name, path: path, headers: {Accept: 'applications/vnd.github.v3.raw'})
Dotenv::Parser.call(content)
rescue StandardError => e
raise Samson::Hooks::UserError, "Cannot download env file #{path} from #{env_repo_name} (#{e.message})"
end
# TODO: versioned lookup
def env_vars_from_config_service(deploy, deploy_group)
_, key = config_service_location(deploy.project, deploy_group, display: false)
response = config_service_read_with_failover(key)
YAML.safe_load(response)
rescue StandardError => e
raise Samson::Hooks::UserError, "Error reading env vars from config service: #{e.message}"
end
def config_service_read_with_failover(key)
bucket = ENV.fetch 'CONFIG_SERVICE_BUCKET'
region = ENV.fetch 'CONFIG_SERVICE_REGION'
dr_bucket = ENV.fetch 'CONFIG_SERVICE_DR_BUCKET'
dr_region = ENV.fetch 'CONFIG_SERVICE_DR_REGION'
Samson::Retry.with_retries(Aws::S3::Errors::ServiceError, 3) do
begin
config_service_s3_client = Aws::S3::Client.new(region: region)
config_service_s3_client.get_object(bucket: bucket, key: key).body.read
rescue Aws::S3::Errors::NoSuchKey
raise "key \"#{key}\" does not exist in bucket #{bucket}!"
rescue Aws::S3::Errors::ServiceError
config_service_s3_client = Aws::S3::Client.new(region: dr_region)
config_service_s3_client.get_object(bucket: dr_bucket, key: key).body.read
end
end
end
def resolve_dollar_variables(env)
env.each do |k, value|
env[k] = value.gsub(/\$\{(\w+)\}|\$(\w+)/) { |original| env[$1 || $2] || original }
end
end
def resolve_secrets(project, deploy_group, env, preview:)
resolver = Samson::Secrets::KeyResolver.new(project, Array(deploy_group))
env.each do |key, value|
next unless secret_key = value.dup.sub!(/^#{Regexp.escape TerminalExecutor::SECRET_PREFIX}/, '')
found = resolver.read(secret_key)
resolved =
if preview
path = resolver.expand_key(secret_key)
path ? "#{TerminalExecutor::SECRET_PREFIX}#{path}" : "#{value}#{FAILED_LOOKUP_MARK}"
else
found.to_s
end
env[key] = resolved
end
resolver.verify! unless preview
end
end
def priority
[PARENT_PRIORITY.index(parent_type) || 999, super]
end
private
# callback for audited
def auditing_enabled
parent_type != "Deploy" && super
end
end