terraspace 1.1.6 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/.cody/all/bin/build.sh +33 -0
  3. data/.cody/all/buildspec.yml +10 -0
  4. data/.cody/{aws/role.rb → all/iam_role.rb} +0 -0
  5. data/.cody/all/project.rb +6 -0
  6. data/.cody/aws/bin/build.sh +5 -0
  7. data/.cody/aws/iam_role.rb +6 -0
  8. data/.cody/aws/project.rb +6 -2
  9. data/.cody/azurerm/bin/build.sh +5 -0
  10. data/.cody/azurerm/{role.rb → iam_role.rb} +0 -0
  11. data/.cody/azurerm/project.rb +5 -3
  12. data/.cody/google/bin/build.sh +5 -0
  13. data/.cody/google/{role.rb → iam_role.rb} +0 -0
  14. data/.cody/google/project.rb +5 -3
  15. data/.cody/none/bin/build.sh +5 -0
  16. data/.cody/none/{role.rb → iam_role.rb} +0 -0
  17. data/.cody/none/project.rb +6 -2
  18. data/.cody/shared/script/install/terraform.sh +2 -1
  19. data/.cody/shared/script/update/gemfile.sh +2 -0
  20. data/.cody/unit/project.rb +2 -2
  21. data/.gitignore +2 -0
  22. data/.pipedream/pipeline.rb +4 -3
  23. data/CHANGELOG.md +30 -0
  24. data/exe/terraspace +0 -7
  25. data/lib/templates/base/project/config/app.rb +3 -0
  26. data/lib/templates/plugin/ci/%gem_name%.gemspec.tt +32 -0
  27. data/lib/templates/plugin/ci/.gitignore +12 -0
  28. data/lib/templates/plugin/{.rspec → ci/.rspec} +0 -0
  29. data/lib/templates/plugin/ci/.rubocop.yml +13 -0
  30. data/lib/templates/plugin/ci/CHANGELOG.md.tt +5 -0
  31. data/lib/templates/plugin/ci/Gemfile +10 -0
  32. data/lib/templates/plugin/ci/LICENSE.txt +21 -0
  33. data/lib/templates/plugin/ci/README.md.tt +19 -0
  34. data/lib/templates/plugin/ci/Rakefile +12 -0
  35. data/lib/templates/plugin/ci/lib/%gem_name%/autoloader.rb.tt +23 -0
  36. data/lib/templates/plugin/ci/lib/%gem_name%/interface.rb.tt +15 -0
  37. data/lib/templates/plugin/ci/lib/%gem_name%/pr.rb.tt +15 -0
  38. data/lib/templates/plugin/ci/lib/%gem_name%/vars.rb.tt +26 -0
  39. data/lib/templates/plugin/ci/lib/%gem_name%/version.rb.tt +5 -0
  40. data/lib/templates/plugin/ci/lib/%gem_name%.rb.tt +17 -0
  41. data/lib/templates/plugin/ci/spec/%gem_name%_spec.rb.tt +7 -0
  42. data/lib/templates/plugin/ci/spec/spec_helper.rb.tt +15 -0
  43. data/lib/templates/plugin/{.gitignore → core/.gitignore} +0 -0
  44. data/lib/templates/plugin/core/.rspec +3 -0
  45. data/lib/templates/plugin/{CHANGELOG.md → core/CHANGELOG.md} +0 -0
  46. data/lib/templates/plugin/{Gemfile → core/Gemfile} +0 -0
  47. data/lib/templates/plugin/{LICENSE.txt → core/LICENSE.txt} +0 -0
  48. data/lib/templates/plugin/{README.md.tt → core/README.md.tt} +0 -0
  49. data/lib/templates/plugin/{Rakefile → core/Rakefile} +0 -0
  50. data/lib/templates/plugin/{bin → core/bin}/console.tt +0 -0
  51. data/lib/templates/plugin/{bin → core/bin}/setup +0 -0
  52. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/module/main.tf +0 -0
  53. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/module/outputs.tf +0 -0
  54. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/module/variables.tf +0 -0
  55. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/project/config/terraform/backend.tf.tt +0 -0
  56. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/project/config/terraform/provider.tf +0 -0
  57. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/stack/main.tf +0 -0
  58. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/stack/outputs.tf +0 -0
  59. data/lib/templates/plugin/{lib → core/lib}/templates/hcl/stack/variables.tf +0 -0
  60. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/module/main.rb +0 -0
  61. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/module/outputs.rb +0 -0
  62. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/module/variables.rb +0 -0
  63. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/project/config/terraform/backend.rb.tt +0 -0
  64. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/project/config/terraform/provider.rb +0 -0
  65. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/stack/main.rb +0 -0
  66. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/stack/outputs.rb +0 -0
  67. data/lib/templates/plugin/{lib → core/lib}/templates/ruby/stack/variables.rb +0 -0
  68. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/.rspec +0 -0
  69. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/Gemfile +0 -0
  70. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/spec/fixtures/stack/main.tf +0 -0
  71. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/spec/fixtures/stack/outputs.tf +0 -0
  72. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/spec/fixtures/stack/variables.tf +0 -0
  73. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/spec/main_spec.rb +0 -0
  74. data/lib/templates/plugin/{lib → core/lib}/templates/test/rspec/module/test/spec/spec_helper.rb +0 -0
  75. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/autoloader.rb.tt +0 -0
  76. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/clients.rb.tt +0 -0
  77. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/interfaces/backend.rb.tt +0 -0
  78. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/interfaces/config.rb.tt +0 -0
  79. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/interfaces/expander.rb.tt +0 -0
  80. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/interfaces/layer.rb.tt +0 -0
  81. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%/version.rb.tt +0 -0
  82. data/lib/templates/plugin/{lib → core/lib}/terraspace_plugin_%name%.rb.tt +0 -0
  83. data/lib/templates/plugin/{spec → core/spec}/spec_helper.rb.tt +0 -0
  84. data/lib/templates/plugin/{spec → core/spec}/terraspace_provider_%name%_spec.rb.tt +0 -0
  85. data/lib/templates/plugin/{terraspace_plugin_%name%.gemspec.tt → core/terraspace_plugin_%name%.gemspec.tt} +0 -0
  86. data/lib/terraspace/all/runner.rb +4 -1
  87. data/lib/terraspace/all/summary.rb +2 -0
  88. data/lib/terraspace/app.rb +23 -4
  89. data/lib/terraspace/builder/children.rb +6 -7
  90. data/lib/terraspace/builder.rb +8 -2
  91. data/lib/terraspace/cli/concerns/plan_path.rb +8 -0
  92. data/lib/terraspace/cli/down.rb +4 -0
  93. data/lib/terraspace/cli/init.rb +1 -1
  94. data/lib/terraspace/cli/new/ci.rb +121 -0
  95. data/lib/terraspace/cli/new/example.rb +1 -1
  96. data/lib/terraspace/cli/new/helpers/plugin_gem.rb +1 -1
  97. data/lib/terraspace/cli/new/plugin/ci.rb +46 -0
  98. data/lib/terraspace/cli/new/plugin/core.rb +26 -0
  99. data/lib/terraspace/cli/new/plugin/helper.rb +4 -1
  100. data/lib/terraspace/cli/new/plugin.rb +8 -15
  101. data/lib/terraspace/cli/new/test.rb +1 -1
  102. data/lib/terraspace/cli/new.rb +17 -13
  103. data/lib/terraspace/cli/plan.rb +13 -0
  104. data/lib/terraspace/cli/setup/check.rb +8 -0
  105. data/lib/terraspace/cli/up.rb +8 -12
  106. data/lib/terraspace/cli.rb +2 -2
  107. data/lib/terraspace/cloud/api/cani.rb +30 -0
  108. data/lib/terraspace/cloud/api/concern/errors.rb +12 -0
  109. data/lib/terraspace/cloud/api/concern/record.rb +18 -0
  110. data/lib/terraspace/cloud/api/concern.rb +38 -0
  111. data/lib/terraspace/cloud/api/http_methods.rb +116 -0
  112. data/lib/terraspace/cloud/api/validate.rb +24 -0
  113. data/lib/terraspace/cloud/api.rb +33 -0
  114. data/lib/terraspace/cloud/base.rb +97 -0
  115. data/lib/terraspace/cloud/ci/generic.rb +25 -0
  116. data/lib/terraspace/cloud/ci/manual.rb +81 -0
  117. data/lib/terraspace/cloud/ci/vcs/base.rb +36 -0
  118. data/lib/terraspace/cloud/ci/vcs/bitbucket.rb +11 -0
  119. data/lib/terraspace/cloud/ci/vcs/github.rb +11 -0
  120. data/lib/terraspace/cloud/ci/vcs/gitlab.rb +11 -0
  121. data/lib/terraspace/cloud/ci/vcs.rb +18 -0
  122. data/lib/terraspace/cloud/ci.rb +56 -0
  123. data/lib/terraspace/cloud/context.rb +14 -0
  124. data/lib/terraspace/cloud/folder/base.rb +17 -0
  125. data/lib/terraspace/cloud/folder/package.rb +33 -0
  126. data/lib/terraspace/cloud/folder/tidy.rb +54 -0
  127. data/lib/terraspace/cloud/folder/uploader.rb +37 -0
  128. data/lib/terraspace/cloud/folder.rb +11 -0
  129. data/lib/terraspace/cloud/plan.rb +47 -0
  130. data/lib/terraspace/cloud/update.rb +37 -0
  131. data/lib/terraspace/command.rb +16 -1
  132. data/lib/terraspace/compiler/dsl/syntax/mod.rb +2 -2
  133. data/lib/terraspace/compiler/dsl/syntax/tfvar.rb +1 -1
  134. data/lib/terraspace/compiler/expander/backend.rb +1 -1
  135. data/lib/terraspace/compiler/expander.rb +1 -1
  136. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +56 -29
  137. data/lib/terraspace/core.rb +36 -3
  138. data/lib/terraspace/ext/core/module.rb +9 -4
  139. data/lib/terraspace/hooks/builder.rb +1 -1
  140. data/lib/terraspace/logger.rb +32 -5
  141. data/lib/terraspace/mod.rb +21 -9
  142. data/lib/terraspace/plugin/expander/interface.rb +19 -11
  143. data/lib/terraspace/plugin.rb +14 -5
  144. data/lib/terraspace/shell.rb +7 -2
  145. data/lib/terraspace/terraform/args/thor.rb +8 -2
  146. data/lib/terraspace/terraform/ihooks/after/apply.rb +8 -0
  147. data/lib/terraspace/terraform/ihooks/after/destroy.rb +8 -0
  148. data/lib/terraspace/terraform/ihooks/after/plan.rb +31 -2
  149. data/lib/terraspace/terraform/ihooks/base.rb +7 -3
  150. data/lib/terraspace/terraform/ihooks/before/apply.rb +8 -0
  151. data/lib/terraspace/terraform/ihooks/before/destroy.rb +8 -0
  152. data/lib/terraspace/terraform/ihooks/before/plan.rb +11 -3
  153. data/lib/terraspace/terraform/runner.rb +19 -5
  154. data/lib/terraspace/version.rb +1 -1
  155. data/lib/terraspace.rb +2 -0
  156. data/terraspace.gemspec +1 -0
  157. metadata +119 -51
  158. data/.pipedream/schedule.rb +0 -3
@@ -0,0 +1,30 @@
1
+ class Terraspace::Cloud::Api
2
+ class Cani
3
+ include Terraspace::Util::Logging
4
+
5
+ def initialize(result)
6
+ @result = result
7
+ end
8
+
9
+ # {"data":{"attributes":{"detail":"You are authorized to perform this action.","status":200,"title":"Authoriz
10
+ # {"errors":[{"detail":"You are not authorized to perform this action. Double check your token or check with your admin that you have permissions.","status":403,"title":"Forbidden"}]}
11
+ def handle
12
+ yes = false # assume do not have permission
13
+ detail = @result&.dig('data', 'attributes', 'detail')
14
+ if detail&.include?('You are authorized to perform this action')
15
+ yes = true # confirm have permission
16
+ end
17
+ return if yes
18
+
19
+ if @result.nil? # 400 Bad Request
20
+ logger.info "ERROR: It doesn't look like TS_TOKEN is valid".color(:red)
21
+ else
22
+ errors = @result.dig('errors')
23
+ detail = errors.first['detail']
24
+ # {"errors":[{"detail":"You are not authorized to perform this action. Double check your token or check with your admin that you have permissions.","status":403,"title":"Forbidden"}]}
25
+ logger.info "ERROR: #{detail}".color(:red)
26
+ end
27
+ exit 1
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module Terraspace::Cloud::Api::Concern
2
+ module Errors
3
+ def errors?(result)
4
+ result.is_a?(Hash) && result.key?("errors")
5
+ end
6
+
7
+ def error_message(result)
8
+ $stderr.puts "ERROR: #{result["errors"]}"
9
+ $stderr.puts "Your current settings. org: #{@org} project: #{@project}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Terraspace::Cloud::Api::Concern
2
+ module Record
3
+ def load_record(result)
4
+ record = {}
5
+ data = result['data']
6
+ record['id'] = data['id']
7
+ record.merge!(data['attributes'])
8
+ record
9
+ end
10
+
11
+ def load_records(result)
12
+ result['data'].map do |item|
13
+ record = { id: item['id'] }
14
+ record.merge(item['attributes'])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ class Terraspace::Cloud::Api
2
+ module Concern
3
+ extend Memoist
4
+ include Errors
5
+ include Record
6
+ include Validate
7
+
8
+ def api
9
+ validate("stack", cloud_stack_name)
10
+ @options = @options.merge(
11
+ app: Terraspace.app,
12
+ role: Terraspace.role,
13
+ env: Terraspace.env,
14
+ extra: Terraspace.extra,
15
+ region: region,
16
+ name: cloud_stack_name,
17
+ )
18
+ @options.reject! { |k,v| v.nil? }
19
+ Terraspace::Cloud::Api.new(@options) # @options are CLI options
20
+ end
21
+
22
+ def cloud_stack_name
23
+ pattern = Terraspace.config.cloud.stack
24
+ expanded = expander.expansion(pattern) # pattern is a String that contains placeholders for substitutions
25
+ expanded.gsub(%r{-+},'-') # remove double dashes are more. IE: -- -> -
26
+ .sub(/^-+/,'').sub(/-+$/,'') # remove leading and trailing -
27
+ end
28
+
29
+ def region
30
+ expander.expansion(":REGION")
31
+ end
32
+
33
+ def expander
34
+ Terraspace::Compiler::Expander.autodetect(@mod)
35
+ end
36
+ memoize :expander
37
+ end
38
+ end
@@ -0,0 +1,116 @@
1
+ class Terraspace::Cloud::Api
2
+ module HttpMethods
3
+ extend Memoist
4
+ include Terraspace::Util
5
+
6
+ # Always translate raw json response to ruby Hash
7
+ def request(klass, path, data={})
8
+ url = url(path)
9
+ req = build_request(klass, url, data)
10
+ retries = 0
11
+ begin
12
+ resp = http.request(req) # send request
13
+ rescue Errno::ECONNREFUSED, Errno::EAFNOSUPPORT
14
+ delay = 2 ** retries
15
+ logger.info "Unable to connect to #{url}. Delay for #{delay}s and trying again."
16
+ sleep(delay)
17
+ retries += 1
18
+ # Final retry time: 2 * 4 = 16s
19
+ # Total retry time: 2 ** 4 + 2 ** 3 + 2 ** 2 + 2 ** 1 + 2 ** 0 = 31s
20
+ if retries < 5 # max_retries is 4
21
+ retry
22
+ else
23
+ logger.error "Error connecting to #{url}"
24
+ message = "#{$!.class}: #{$!.message}"
25
+ raise Terraspace::NetworkError.new(message)
26
+ end
27
+ end
28
+ result = load_json(url, resp)
29
+ Cani.new(result).handle if data[:cani]
30
+ result
31
+ end
32
+
33
+ def build_request(klass, url, data={})
34
+ req = klass.new(url) # url includes query string and uri.path does not, must used url
35
+ set_headers!(req)
36
+ if [Net::HTTP::Delete, Net::HTTP::Patch, Net::HTTP::Post, Net::HTTP::Put].include?(klass)
37
+ text = JSON.dump(data)
38
+ req.body = text
39
+ req.content_length = text.bytesize
40
+ end
41
+
42
+ logger.debug "API klass: #{klass}"
43
+ logger.debug "API url: #{url}"
44
+ logger.debug "API data: #{data}"
45
+
46
+ req
47
+ end
48
+
49
+ def set_headers!(req)
50
+ req['Authorization'] = "Bearer #{token}" if token
51
+ req['Content-Type'] = 'application/json'
52
+ end
53
+
54
+ def token
55
+ ENV['TS_TOKEN']
56
+ end
57
+
58
+ def load_json(url, resp)
59
+ uri = URI(url)
60
+
61
+ logger.debug "resp.code #{resp.code}"
62
+ logger.debug "resp.body #{resp.body}" # {"errors":[{"message":"403 Forbidden"}]}
63
+
64
+ if parseable?(resp.code)
65
+ JSON.load(resp.body)
66
+ else
67
+ logger.error "Error: #{url}"
68
+ logger.error "Error: Non-successful http response status code: #{resp.code}"
69
+ # logger.debug "Error: Non-successful http response body: #{resp.body}"
70
+ logger.error "headers: #{resp.each_header.to_h.inspect}"
71
+ logger.error "Terraspace Cloud API #{url}"
72
+ raise "Terraspace Cloud API called failed: #{uri.host}"
73
+ end
74
+ end
75
+
76
+ # Note: 422 is Unprocessable Entity. This means an invalid data payload was sent.
77
+ # We want that to error and raise
78
+ def parseable?(http_code)
79
+ http_code =~ /^20/ || http_code =~ /^40/
80
+ end
81
+
82
+ def http
83
+ uri = URI(endpoint)
84
+ http = Net::HTTP.new(uri.host, uri.port)
85
+ http.open_timeout = http.read_timeout = 30
86
+ http.use_ssl = true if uri.scheme == 'https'
87
+ http
88
+ end
89
+ memoize :http
90
+
91
+ # API does not include the /. IE: https://app.terraform.io/api/v2
92
+ def url(path)
93
+ "#{endpoint}/#{path}"
94
+ end
95
+
96
+ def get(path)
97
+ request(Net::HTTP::Get, path)
98
+ end
99
+
100
+ def post(path, data={})
101
+ request(Net::HTTP::Post, path, data)
102
+ end
103
+
104
+ def put(path, data={})
105
+ request(Net::HTTP::Put, path, data)
106
+ end
107
+
108
+ def patch(path, data={})
109
+ request(Net::HTTP::Patch, path, data)
110
+ end
111
+
112
+ def delete(path, data={})
113
+ request(Net::HTTP::Delete, path, data)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,24 @@
1
+ class Terraspace::Cloud::Api
2
+ module Validate
3
+ def validate(name, value)
4
+ unless value =~ /^[\w-]*$/
5
+ message = "ERROR: #{name}: only allows letters, numbers, dashes, and underscores"
6
+ end
7
+ if value =~ /^[-_]/ || value =~ /[-_]$/
8
+ message = "ERROR: #{name}: no leading or trailing underscore or dash allowed"
9
+ end
10
+ if message
11
+ puts message.color(:red)
12
+ puts <<~EOL
13
+ Please fix the configuration
14
+
15
+ config/app.rb
16
+
17
+ config.cloud.#{name} = '...'
18
+
19
+ EOL
20
+ exit 1
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Terraspace::Cloud
2
+ class Api
3
+ include Context
4
+ include HttpMethods
5
+
6
+ def initialize(options)
7
+ @options = options
8
+ setup_context(@options)
9
+ end
10
+
11
+ def endpoint
12
+ ENV['TS_API'].blank? ? 'https://api.terraspace.cloud/api/v1' : ENV['TS_API']
13
+ end
14
+
15
+ def stack_path
16
+ "orgs/#{@org}/projects/#{@project}/stacks/#{@stack}"
17
+ end
18
+
19
+ def create_upload
20
+ post("#{stack_path}/uploads", @options)
21
+ end
22
+
23
+ # record_attrs: {upload_id: "upload-nRPSpyWd65Ps6978", kind: "apply", stack_id: '...'}
24
+ def create_plan(data)
25
+ post("#{stack_path}/plans", data.merge(@options))
26
+ end
27
+
28
+ # data: {upload_id: "upload-nRPSpyWd65Ps6978", kind: "apply", stack_id: '...'}
29
+ def create_update(data)
30
+ post("#{stack_path}/updates", data.merge(@options))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,97 @@
1
+ module Terraspace::Cloud
2
+ class Base < Terraspace::CLI::Base
3
+ extend Memoist
4
+ include Api::Concern
5
+ include Context
6
+ include Terraspace::Util
7
+
8
+ def initialize(options={})
9
+ super
10
+ @cani = options[:cani]
11
+ @kind = options[:kind]
12
+ @success = options[:success]
13
+ setup_context(options)
14
+ end
15
+
16
+ def stage_attrs
17
+ status = @success ? "success" : "fail"
18
+ attrs = {
19
+ status: status,
20
+ kind: @kind,
21
+ terraspace_version: check.terraspace_version,
22
+ terraform_version: check.terraform_version,
23
+ }
24
+ attrs.merge!(ci_vars) if ci_vars
25
+ attrs
26
+ end
27
+
28
+ def check
29
+ Terraspace::CLI::Setup::Check.new
30
+ end
31
+ memoize :check
32
+
33
+ def pr_comment(url)
34
+ ci.comment(url) if ci.respond_to?(:comment)
35
+ end
36
+
37
+ def ci_vars
38
+ return unless ci
39
+ if ci.vars[:host]
40
+ vcs = Ci::Vcs.new(ci.vars)
41
+ vcs.merged_vars
42
+ else
43
+ ci.vars
44
+ end
45
+ end
46
+
47
+ def ci
48
+ Terraspace::Cloud::Ci.detect
49
+ end
50
+ memoize :ci
51
+
52
+ def sh(command, exit_on_fail: true)
53
+ logger.debug "=> #{command}"
54
+ system command
55
+ if $?.exitstatus != 0 && exit_on_fail
56
+ logger.info "ERROR RUNNING: #{command}"
57
+ exit $?.exitstatus
58
+ end
59
+ end
60
+
61
+ def clean_cache2_stage
62
+ # terraform plan can be a kind of apply or destroy
63
+ # terraform apply can be a kind of apply or destroy
64
+ kind = self.class.name.to_s.split('::').last.underscore # IE: apply or destroy
65
+ dir = "#{@mod.cache_dir}/.terraspace-cache/_cache2/#{kind}"
66
+ FileUtils.rm_rf(dir)
67
+ FileUtils.mkdir_p(dir)
68
+ end
69
+
70
+ def record?
71
+ changes? && !cancelled? || Terraspace.config.cloud.record == "all"
72
+ end
73
+
74
+ def changes?
75
+ no_changes = Terraspace::Logger.buffer.detect do |line|
76
+ line.include?('No changes')
77
+ end
78
+ zero_destroyed = Terraspace::Logger.buffer.detect do |line|
79
+ line.include?('Destroy complete! Resources: 0 destroyed')
80
+ end
81
+ !no_changes && !zero_destroyed
82
+ end
83
+
84
+ def cancelled?
85
+ !!Terraspace::Logger.buffer.detect do |line|
86
+ line.include?(' cancelled')
87
+ end
88
+ end
89
+
90
+ def terraspace_cloud_info(result)
91
+ data = result['data']
92
+ url = data['attributes']['url']
93
+ logger.info "Terraspace Cloud #{url}"
94
+ url
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,25 @@
1
+ class Terraspace::Cloud::Ci
2
+ class Generic
3
+ def vars
4
+ {
5
+ build_system: "generic",
6
+ host: ENV['TS_CI_HOST'] || ENV['TS_VCS_HOST'],
7
+ full_repo: ENV['TS_CI_REPO'],
8
+ branch_name: ENV['TS_CI_BRANCH'],
9
+ # urls
10
+ commit_url: ENV['TS_CI_COMMIT_URL'],
11
+ branch_url: ENV['TS_CI_BRANCH_URL'],
12
+ pr_url: ENV['TS_CI_PR_URL'],
13
+ build_url: ENV['TS_CI_BUILD_URL'],
14
+ # additional properties
15
+ build_type: ENV['TS_CI_BUILD_TYPE'],
16
+ pr_number: ENV['TS_CI_PR_NUMBER'],
17
+ sha: ENV['TS_CI_SHA'],
18
+ # additional properties
19
+ commit_message: ENV['TS_CI_COMMIT_MESSAGE'],
20
+ build_id: ENV['TS_CI_BUILD_ID'],
21
+ build_number: ENV['TS_CI_BUILD_NUMBER'],
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,81 @@
1
+ class Terraspace::Cloud::Ci
2
+ class Manual
3
+ extend Memoist
4
+ include Terraspace::Util::Logging
5
+
6
+ def vars
7
+ if git_repo? && git_installed?
8
+ vars_data
9
+ else
10
+ { build_system: "manual" }
11
+ end
12
+ end
13
+
14
+ def vars_data
15
+ {
16
+ build_system: "manual", # required
17
+ host: host,
18
+ full_repo: full_repo,
19
+ branch_name: branch_name,
20
+ sha: sha,
21
+ dirty: dirty?,
22
+ # commit_url: commit_url, # provided by core
23
+ # branch_url: branch_url, # provided by core
24
+ }
25
+ end
26
+
27
+ def host
28
+ return nil unless File.exist?('.git')
29
+ return nil if git_url.blank?
30
+ uri = URI(git_url)
31
+ "#{uri.scheme}://#{uri.host}"
32
+ end
33
+
34
+ def full_repo
35
+ uri = URI(git_url)
36
+ uri.path.sub(/^\//,'')
37
+ end
38
+
39
+ def dirty?
40
+ out = git "status --porcelain"
41
+ !out.blank?
42
+ end
43
+
44
+ # Works for
45
+ # git@github.com: => https://github.com/
46
+ # git@bitbucket.org: => https://bitbucket.org/
47
+ # git@gitlab.com: => https://gitlab.com/
48
+ def git_url
49
+ out = git "config --get remote.origin.url"
50
+ out.sub(/\.git/,'').sub(/^git@/,'https://').sub(/\.(.*):/,'.\1/')
51
+ end
52
+
53
+ def branch_name
54
+ out = git "rev-parse --abbrev-ref HEAD"
55
+ out unless out == "HEAD" # edge case: when branch has never been pushed
56
+ end
57
+
58
+ def sha
59
+ out = git "rev-parse HEAD"
60
+ out unless out == "HEAD" # edge case: when branch has never been pushed
61
+ end
62
+
63
+ def git(command)
64
+ return unless git_installed?
65
+ out = `git #{command}`
66
+ unless $?.success?
67
+ logger.debug "WARN Command Failed: git #{command}".color(:yellow)
68
+ end
69
+ out.strip
70
+ end
71
+ memoize :git
72
+
73
+ def git_installed?
74
+ system "type git > /dev/null 2>&1"
75
+ end
76
+
77
+ def git_repo?
78
+ File.exist?('.git')
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,36 @@
1
+ class Terraspace::Cloud::Ci::Vcs
2
+ class Base
3
+ extend Memoist
4
+
5
+ def initialize(vars)
6
+ @vars = vars
7
+ end
8
+
9
+ def vars
10
+ {
11
+ commit_url: commit_url, # implemented by subclass
12
+ branch_url: branch_url, # implemented by subclass
13
+ }
14
+ end
15
+
16
+ def merged_vars
17
+ @vars.merge(vars)
18
+ end
19
+
20
+ class << self
21
+ def vars_methods(*names)
22
+ names.each do |name|
23
+ vars_method(name)
24
+ end
25
+ end
26
+
27
+ def vars_method(name)
28
+ define_method name do
29
+ @vars[name]
30
+ end
31
+ end
32
+ end
33
+
34
+ vars_methods :host, :full_repo, :sha, :branch_name
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ class Terraspace::Cloud::Ci::Vcs
2
+ class Bitbucket < Base
3
+ def commit_url
4
+ "#{host}/#{full_repo}/commits/#{sha}" if sha
5
+ end
6
+
7
+ def branch_url
8
+ "#{host}/#{full_repo}/branch/#{branch_name}" if branch_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class Terraspace::Cloud::Ci::Vcs
2
+ class Github < Base
3
+ def commit_url
4
+ "#{host}/#{full_repo}/commits/#{sha}" if sha
5
+ end
6
+
7
+ def branch_url
8
+ "#{host}/#{full_repo}/tree/#{branch_name}" if branch_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class Terraspace::Cloud::Ci::Vcs
2
+ class Gitlab < Base
3
+ def commit_url
4
+ "#{host}/#{full_repo}/-/commits/#{sha}" if sha
5
+ end
6
+
7
+ def branch_url
8
+ "#{host}/#{full_repo}/-/tree/#{branch_name}" if branch_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ class Terraspace::Cloud::Ci
2
+ class Vcs
3
+ extend Memoist
4
+
5
+ def initialize(vars)
6
+ @vars = vars
7
+ end
8
+
9
+ def merged_vars
10
+ vcs_class = case @vars[:host]
11
+ when /github/ then Github
12
+ when /gitlab/ then Gitlab
13
+ when /bitbucket/ then Bitbucket
14
+ end
15
+ vcs_class ? vcs_class.new(@vars).merged_vars : {}
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ module Terraspace::Cloud
2
+ class Ci
3
+ # Example meta:
4
+ #
5
+ # [
6
+ # {name: "github", interface_class: TerrapaceCiGithub::Interface},
7
+ # {name: "gitlab", interface_class: TerrapaceCiGitlab::Interface},
8
+ # ]
9
+ #
10
+ class_attribute :meta # not shared with child classes
11
+ self.meta = []
12
+
13
+ class << self
14
+ def register(data)
15
+ self.meta << data unless meta.find do |m|
16
+ m[:name] == data[:name]
17
+ end
18
+ end
19
+
20
+ def detect
21
+ return Generic if ENV['TS_CI_REPO']
22
+
23
+ detected = meta.find do |data|
24
+ env_key = data[:env_key] # IE: ENV['GITHUB_ACTIONS']
25
+ env_value = data[:env_value] # IE: "string" or /pattern/
26
+ if env_value
27
+ v = ENV[env_key]
28
+ v && match?(v, env_value)
29
+ else
30
+ ENV[env_key] # only env_key
31
+ end
32
+ end
33
+ klass = if detected
34
+ interface_class(detected)
35
+ else
36
+ Manual
37
+ end
38
+ klass.new
39
+ end
40
+
41
+ # IE: TerraspaceCiGithub::Interface
42
+ def interface_class(meta)
43
+ "terraspace_ci_#{meta[:name]}::Interface".classify.constantize
44
+ end
45
+
46
+ def match?(v, env_value)
47
+ case v
48
+ when String
49
+ v == env_value
50
+ when Regexp
51
+ v.match(env_value)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ module Terraspace::Cloud
2
+ module Context
3
+ include Terraspace::Cloud::Api::Validate
4
+
5
+ def setup_context(options)
6
+ cloud = Terraspace.config.cloud
7
+ @org = cloud.org
8
+ @project = cloud.project
9
+ @stack = options[:stack]
10
+ validate("org", @org)
11
+ validate("project", @project)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ class Terraspace::Cloud::Folder
2
+ class Base < Terraspace::Cloud::Base
3
+ def initialize(options={})
4
+ super
5
+ @type = options[:type]
6
+ end
7
+
8
+ # final zip dest
9
+ def zip_path
10
+ "#{artifacts_path}.zip"
11
+ end
12
+
13
+ def artifacts_path
14
+ "#{@mod.cache_dir}/.terraspace-cache/_cache2/artifacts/#{@type}"
15
+ end
16
+ end
17
+ end