terraspace 2.0.3 → 2.1.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.cody/README.md +1 -1
  3. data/.cody/all/project.rb +4 -0
  4. data/.cody/aws/project.rb +4 -0
  5. data/.cody/azurerm/project.rb +4 -0
  6. data/.cody/google/project.rb +4 -0
  7. data/.cody/none/project.rb +4 -0
  8. data/.cody/shared/script/install/infracost.sh +6 -0
  9. data/.cody/shared/script/install/terraform.sh +2 -2
  10. data/.cody/shared/script/install.sh +1 -0
  11. data/.pipedream/README.md +1 -1
  12. data/.pipedream/pipeline.rb +15 -8
  13. data/CHANGELOG.md +8 -0
  14. data/lib/templates/base/project/Gemfile.tt +5 -0
  15. data/lib/templates/base/project/config/app.rb +6 -2
  16. data/lib/templates/plugin/ci/CHANGELOG.md.tt +1 -1
  17. data/lib/templates/plugin/ci/lib/%gem_name%/vars.rb.tt +1 -1
  18. data/lib/terraspace/app.rb +16 -0
  19. data/lib/terraspace/cli/base.rb +1 -0
  20. data/lib/terraspace/cli/commander.rb +2 -1
  21. data/lib/terraspace/cli/down.rb +42 -6
  22. data/lib/terraspace/cli/new/plugin/ci.rb +1 -4
  23. data/lib/terraspace/cli/plan.rb +66 -2
  24. data/lib/terraspace/cli/up.rb +65 -11
  25. data/lib/terraspace/cloud/api/cani.rb +15 -9
  26. data/lib/terraspace/cloud/api/concern.rb +0 -1
  27. data/lib/terraspace/cloud/api/http_methods.rb +7 -2
  28. data/lib/terraspace/cloud/api.rb +29 -5
  29. data/lib/terraspace/cloud/base.rb +15 -28
  30. data/lib/terraspace/cloud/ci.rb +1 -8
  31. data/lib/terraspace/cloud/comment.rb +28 -0
  32. data/lib/terraspace/cloud/context.rb +1 -0
  33. data/lib/terraspace/cloud/cost/infracost.rb +80 -0
  34. data/lib/terraspace/cloud/cost.rb +68 -0
  35. data/lib/terraspace/cloud/git.rb +0 -0
  36. data/lib/terraspace/cloud/plan.rb +33 -15
  37. data/lib/terraspace/cloud/stream.rb +113 -0
  38. data/lib/terraspace/cloud/streamer.rb +9 -0
  39. data/lib/terraspace/cloud/update.rb +19 -19
  40. data/lib/terraspace/cloud/{folder → upload}/base.rb +1 -1
  41. data/lib/terraspace/cloud/{folder → upload}/package.rb +1 -1
  42. data/lib/terraspace/cloud/{folder → upload}/tidy.rb +1 -1
  43. data/lib/terraspace/cloud/upload.rb +53 -0
  44. data/lib/terraspace/cloud/vcs/base.rb +6 -0
  45. data/lib/terraspace/cloud/vcs/ci_env.rb +15 -0
  46. data/lib/terraspace/cloud/vcs/commenter.rb +75 -0
  47. data/lib/terraspace/cloud/vcs/interface.rb +14 -0
  48. data/lib/terraspace/cloud/vcs/local_env.rb +25 -0
  49. data/lib/terraspace/cloud/{ci/vcs → vcs/local_git}/base.rb +6 -3
  50. data/lib/terraspace/cloud/vcs/local_git/bitbucket.rb +17 -0
  51. data/lib/terraspace/cloud/vcs/local_git/github.rb +17 -0
  52. data/lib/terraspace/cloud/vcs/local_git/gitlab.rb +17 -0
  53. data/lib/terraspace/cloud/{ci/manual.rb → vcs/local_git.rb} +18 -10
  54. data/lib/terraspace/cloud/vcs.rb +21 -0
  55. data/lib/terraspace/logger.rb +1 -1
  56. data/lib/terraspace/shell/error.rb +1 -1
  57. data/lib/terraspace/terraform/runner.rb +4 -22
  58. data/lib/terraspace/util/popen.rb +67 -0
  59. data/lib/terraspace/version.rb +1 -1
  60. metadata +25 -23
  61. data/lib/templates/plugin/ci/lib/%gem_name%/pr.rb.tt +0 -15
  62. data/lib/terraspace/cloud/api/concern/record.rb +0 -18
  63. data/lib/terraspace/cloud/ci/generic.rb +0 -25
  64. data/lib/terraspace/cloud/ci/vcs/bitbucket.rb +0 -11
  65. data/lib/terraspace/cloud/ci/vcs/github.rb +0 -11
  66. data/lib/terraspace/cloud/ci/vcs/gitlab.rb +0 -11
  67. data/lib/terraspace/cloud/ci/vcs.rb +0 -18
  68. data/lib/terraspace/cloud/folder/uploader.rb +0 -37
  69. data/lib/terraspace/cloud/folder.rb +0 -11
  70. data/lib/terraspace/terraform/ihooks/after/apply.rb +0 -8
  71. data/lib/terraspace/terraform/ihooks/after/destroy.rb +0 -8
  72. data/lib/terraspace/terraform/ihooks/after/plan.rb +0 -46
  73. data/lib/terraspace/terraform/ihooks/base.rb +0 -17
  74. data/lib/terraspace/terraform/ihooks/before/apply.rb +0 -8
  75. data/lib/terraspace/terraform/ihooks/before/destroy.rb +0 -8
  76. data/lib/terraspace/terraform/ihooks/before/plan.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fec1ba076567ee419169922ed9dd183f13bd998983776f603bf36b051d7a6a7
4
- data.tar.gz: a2c710787cedca1d62b788c761a9cb3bb40f43511d04ccf51fdfe44a0cdff391
3
+ metadata.gz: 98afb84e95b715872759e1e8c1ded1304cf7fd53688ed990ba2875079933a4e7
4
+ data.tar.gz: 94716c0e911013aa51d433164ead45535b424a7e7b3847681f9b5ff11aeb2f38
5
5
  SHA512:
6
- metadata.gz: 3318d458250b2e33892740f9fd1fa47d363b2bcffb53a8c8dc8ac081e282bb3cd13be18b230841aa9c125d3fe5e832b53cf2294a51010c123ec96c831666d76d
7
- data.tar.gz: 4acc67e5c612e584d96c2fd752a1b967c5fe9ce34069ec3149b5f1f6329dff56075f26a220106d20e22155d37279d50cb2e02db10bec873c7803fcadddc1e890
6
+ metadata.gz: fd23a9a876b03a71f0ec1689db46f342c166e4ad817d9bc671a3fadc8be493f1f5c31a199133876c6574884271672773d2efcdcc779c5fdb2187edc8ce56e366
7
+ data.tar.gz: e6e0ba42d6e2091da24925ca7dabd88bd67ef704b606b3e1642cecb3f6b73d3db76cef2edbd04e6da71d693d550c9f3f12e14244bdb55ddf361eacb53efb29ab
data/.cody/README.md CHANGED
@@ -10,7 +10,7 @@ This installs the `cody` command to manage the AWS CodeBuild project.
10
10
 
11
11
  ## Update Project
12
12
 
13
- cody deploy --type aws
13
+ cody up --type aws
14
14
 
15
15
  ## Start a Deploy
16
16
 
data/.cody/all/project.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  github("boltops-tools/terraspace")
2
2
  image("aws/codebuild/amazonlinux2-x86_64-standard:3.0")
3
3
  env_vars(
4
+ INFRACOST_API_KEY: "ssm:/#{Cody.env}/INFRACOST_API_KEY",
5
+ TS_API: "ssm:/#{Cody.env}/TS_API",
6
+ TS_COST: true,
7
+ TS_LOG_LEVEL: "info",
4
8
  TS_ORG: "qa",
5
9
  TS_TOKEN: "ssm:/#{Cody.env}/TS_TOKEN",
6
10
  )
data/.cody/aws/project.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  github("boltops-tools/terraspace")
2
2
  image("aws/codebuild/amazonlinux2-x86_64-standard:3.0")
3
3
  env_vars(
4
+ INFRACOST_API_KEY: "ssm:/#{Cody.env}/INFRACOST_API_KEY",
5
+ TS_API: "ssm:/#{Cody.env}/TS_API",
6
+ TS_COST: true, # not ssm so can see in the codebuild logs
7
+ TS_LOG_LEVEL: "info", # not ssm so can see in the codebuild logs
4
8
  TS_ORG: "qa",
5
9
  TS_TOKEN: "ssm:/#{Cody.env}/TS_TOKEN",
6
10
  )
@@ -3,6 +3,10 @@ image("aws/codebuild/amazonlinux2-x86_64-standard:3.0")
3
3
  env_vars(
4
4
  # Used by .cody/azurerm/bin/az/configure.sh
5
5
  AZURE_APP_CLIENT_JSON: ssm("/terraspace/#{Cody.env}/azure_app_client_json"),
6
+ INFRACOST_API_KEY: "ssm:/#{Cody.env}/INFRACOST_API_KEY",
7
+ TS_API: "ssm:/#{Cody.env}/TS_API",
8
+ TS_COST: true,
9
+ TS_LOG_LEVEL: "info",
6
10
  TS_ORG: "qa",
7
11
  TS_TOKEN: "ssm:/#{Cody.env}/TS_TOKEN",
8
12
  )
@@ -3,6 +3,10 @@ image("aws/codebuild/amazonlinux2-x86_64-standard:3.0")
3
3
  env_vars(
4
4
  # Used by .cody/google/bin/gcloud/configure.sh
5
5
  GOOGLE_CREDS_JSON: ssm("/terraspace/#{Cody.env}/google_creds_json"),
6
+ INFRACOST_API_KEY: "ssm:/#{Cody.env}/INFRACOST_API_KEY",
7
+ TS_API: "ssm:/#{Cody.env}/TS_API",
8
+ TS_COST: true,
9
+ TS_LOG_LEVEL: "info",
6
10
  TS_ORG: "qa",
7
11
  TS_TOKEN: "ssm:/#{Cody.env}/TS_TOKEN",
8
12
  )
@@ -1,6 +1,10 @@
1
1
  github("boltops-tools/terraspace")
2
2
  image("aws/codebuild/amazonlinux2-x86_64-standard:3.0")
3
3
  env_vars(
4
+ INFRACOST_API_KEY: "ssm:/#{Cody.env}/INFRACOST_API_KEY",
5
+ TS_API: "ssm:/#{Cody.env}/TS_API",
6
+ TS_LOG_LEVEL: "info",
7
+ TS_COST: true,
4
8
  TS_ORG: "qa",
5
9
  TS_TOKEN: "ssm:/#{Cody.env}/TS_TOKEN",
6
10
  )
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ set -eu
4
+ # install infracost
5
+ # https://www.infracost.io/docs/
6
+ curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh
@@ -2,8 +2,8 @@
2
2
 
3
3
  set -eu
4
4
 
5
- #TERRAFORM_VERSION=latest
6
- TERRAFORM_VERSION=1.1.9
5
+ # TERRAFORM_VERSION=latest
6
+ TERRAFORM_VERSION=1.2.4
7
7
 
8
8
  git clone https://github.com/tfutils/tfenv.git ~/.tfenv
9
9
  echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
@@ -4,3 +4,4 @@ set -eu
4
4
 
5
5
  .cody/shared/script/install/terraform.sh
6
6
  .cody/shared/script/install/terraspace.sh
7
+ .cody/shared/script/install/infracost.sh
data/.pipedream/README.md CHANGED
@@ -12,7 +12,7 @@ This installs both the `pipe` and `pipedream` commands. They do the same thing,
12
12
 
13
13
  To update the CodePipeline pipelines:
14
14
 
15
- pipedream deploy
15
+ pipedream up
16
16
 
17
17
  ## Start a Execution
18
18
 
@@ -7,13 +7,20 @@ stage "Source" do
7
7
  end
8
8
 
9
9
  stage "Build" do
10
- # in parallel
11
- codebuild(
12
- "terraspace-all",
13
- "terraspace-aws",
14
- "terraspace-azurerm",
15
- "terraspace-google",
16
- "terraspace-none",
17
- "terraspace-unit",
10
+ vars = env_vars(
11
+ INFRACOST_API_KEY: "ssm:/#{Pipedream.env}/INFRACOST_API_KEY",
12
+ TS_API: "ssm:/#{Pipedream.env}/TS_API",
13
+ TS_LOG_LEVEL: "info", # not ssm so can see in the codebuild logs
14
+ TS_COST: true,
15
+ TS_ORG: "qa",
16
+ TS_TOKEN: "ssm:/#{Pipedream.env}/TS_TOKEN",
18
17
  )
18
+ in_parallel do
19
+ codebuild(Name: "terraspace-all", EnvironmentVariables: vars)
20
+ codebuild(Name: "terraspace-aws", EnvironmentVariables: vars)
21
+ codebuild(Name: "terraspace-azurerm", EnvironmentVariables: vars)
22
+ codebuild(Name: "terraspace-google", EnvironmentVariables: vars)
23
+ codebuild(Name: "terraspace-none", EnvironmentVariables: vars)
24
+ codebuild(Name: "terraspace-unit") # does not have EnvironmentVariables
25
+ end
19
26
  end
data/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [2.1.0] - 2022-07-11
7
+ - [#247](https://github.com/boltops-tools/terraspace/pull/247) cost estimates and real-time stream logging
8
+ - Cloud Cost Estimation support
9
+ - Live Stream Logging support
10
+ - CI/CD plugin updates. Decoupled CI from PR plugins
11
+ - Improve git info. Get git info with or without CI
12
+ - Fix utf8 encoding edge cases
13
+
6
14
  ## [2.0.3] - 2022-07-04
7
15
  - [#237](https://github.com/boltops-tools/terraspace/pull/237) remove duplicate paths in layering
8
16
  - [#244](https://github.com/boltops-tools/terraspace/pull/244) rename _cache2 to .cache2
@@ -7,3 +7,8 @@ build_gemfile(
7
7
  plugin_gem_name,
8
8
  )
9
9
  %>
10
+
11
+ # Uncomment the ci and vcs provider you wish to use. Should use both ci and vcs gem
12
+ # Docs: https://terraspace.cloud/docs/ci/
13
+ # gem "terraspace_ci_github"
14
+ # gem "terraspace_vcs_github"
@@ -2,6 +2,10 @@
2
2
  Terraspace.configure do |config|
3
3
  config.logger.level = :info
4
4
 
5
- # config.cloud.org = "ORG" # replace with your org. only letters, numbers, underscore and dashes allowed
6
- # config.cloud.project = "main" # optional. main is the default project name. only letters, numbers, underscore and dashes allowed
5
+ # To enable Terraspace Cloud set config.cloud.org
6
+ # config.cloud.org = "ORG" # required: replace with your org. only letters, numbers, underscore and dashes allowed
7
+ # config.cloud.project = "main" # optional. main is the default project name. only letters, numbers, underscore and dashes allowed
8
+
9
+ # Uncomment to enable Cost Estimation. See: http://terraspace.cloud/docs/cloud/cost-estimation/
10
+ # config.cloud.cost.enabled = true
7
11
  end
@@ -1,5 +1,5 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - <%= Time.now.strftime("%Y-%m-%d") %>
3
+ ## [0.1.0] - Unreleased
4
4
 
5
5
  - Initial release
@@ -5,7 +5,7 @@ module <%= gem_class_name %>
5
5
  # Provide as many CI system as possible.
6
6
  def data
7
7
  {
8
- build_system: "REPLACE_ME",
8
+ build_system: "<%= name %>",
9
9
  host: ENV['REPLACE_ME'],
10
10
  full_repo: ENV['REPLACE_ME'],
11
11
  branch_name: ENV['REPLACE_ME'],
@@ -51,6 +51,10 @@ module Terraspace
51
51
  config.cloud.org = ENV['TS_ORG'] # required for Terraspace cloud
52
52
  config.cloud.record = "changes" # IE: changes or all
53
53
  config.cloud.stack = ":APP-:ROLE-:MOD_NAME-:ENV-:EXTRA-:REGION"
54
+ config.cloud.cost = ActiveSupport::OrderedOptions.new
55
+ config.cloud.cost.enabled = cast_value(ENV['TS_COST'])
56
+ config.cloud.vcs = ActiveSupport::OrderedOptions.new
57
+ config.cloud.vcs.name = nil # github, gitlab, bitbucket. Else default to registered terraspace_vcs_* plugin
54
58
 
55
59
  config.hooks = ActiveSupport::OrderedOptions.new
56
60
  config.hooks.show = true
@@ -95,6 +99,18 @@ module Terraspace
95
99
  config
96
100
  end
97
101
 
102
+ # https://stackoverflow.com/questions/36228873/ruby-how-to-convert-a-string-to-boolean
103
+ # https://github.com/rails/rails/blob/5-1-stable/activemodel/lib/active_model/type/boolean.rb
104
+ # so dont have to add activemodel as a dependency just for this method
105
+ FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
106
+ def cast_value(value)
107
+ if value == ""
108
+ nil
109
+ else
110
+ !FALSE_VALUES.include?(value)
111
+ end
112
+ end
113
+
98
114
  def ts_logger
99
115
  Logger.new(ENV['TS_LOG_PATH'] || $stderr)
100
116
  end
@@ -1,5 +1,6 @@
1
1
  class Terraspace::CLI
2
2
  class Base
3
+ extend Memoist
3
4
  include Terraspace::Util
4
5
 
5
6
  def initialize(options={})
@@ -8,7 +8,8 @@ class Terraspace::CLI
8
8
  def run
9
9
  Terraspace::Builder.new(@options).run unless @options[:build] # Up already ran build
10
10
  Init.new(@options).run
11
- Terraspace::Terraform::Runner.new(@name, @options).run
11
+ @runner = Terraspace::Terraform::Runner.new(@name, @options)
12
+ @runner.run
12
13
  end
13
14
  end
14
15
  end
@@ -1,11 +1,34 @@
1
1
  class Terraspace::CLI
2
2
  class Down < Base
3
- include TfcConcern
4
3
  include Concerns::PlanPath
4
+ include Terraspace::Cloud::Streamer
5
+ include Terraspace::Cloud::Vcs::Commenter
6
+ include TfcConcern
5
7
 
6
8
  def run
7
- plan if @options[:yes] && !tfc?
8
- destroy
9
+ cloud_update.cani?
10
+ @stream = cloud_stream.open("down")
11
+ success = perform
12
+ cloud_stream.close(success, @exception)
13
+ exit 1 unless success
14
+ end
15
+
16
+ def perform
17
+ success = nil
18
+ if @options[:yes] && !tfc?
19
+ success = plan
20
+ else
21
+ skip_plan = true
22
+ end
23
+ if success or skip_plan
24
+ success = destroy
25
+ end
26
+ success
27
+ rescue Exception => e
28
+ @exception = true
29
+ logger.info "Exception #{e.class}: #{e.message}".color(:red)
30
+ logger.info e.backtrace.join("\n")
31
+ false
9
32
  end
10
33
 
11
34
  private
@@ -13,12 +36,25 @@ class Terraspace::CLI
13
36
  if Terraspace.cloud? && !@options[:out]
14
37
  @options[:out] = plan_path
15
38
  end
16
- Commander.new("plan", @options.merge(destroy: true)).run
39
+ plan = Plan.new(@options.merge(destroy: true))
40
+ plan.plan_only # returns success: true or false
17
41
  end
18
42
 
19
43
  def destroy
20
- Commander.new("destroy", @options.merge(command: "down")).run
21
- Terraspace::Terraform::Tfc::Workspace.new(@options).destroy if @options[:destroy_workspace]
44
+ commander = Commander.new("destroy", @options.merge(command: "down"))
45
+ success = commander.run
46
+ update = cloud_update.create(success, @stream)
47
+
48
+ if success && @options[:destroy_workspace]
49
+ Terraspace::Terraform::Tfc::Workspace.new(@options).destroy
50
+ end
51
+
52
+ logger.info "Terraspace Cloud #{update['data']['attributes']['url']}" if update
53
+ end
54
+
55
+ def cloud_update
56
+ Terraspace::Cloud::Update.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
22
57
  end
58
+ memoize :cloud_update
23
59
  end
24
60
  end
@@ -7,7 +7,6 @@ class Terraspace::CLI::New::Plugin
7
7
  def self.options
8
8
  [
9
9
  [:force, aliases: %w[y], type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
10
- [:pr, type: :boolean, desc: "Generate pr code also. Most CI systems don't have PR support"],
11
10
  ]
12
11
  end
13
12
  options.each { |args| class_option(*args) }
@@ -15,8 +14,7 @@ class Terraspace::CLI::New::Plugin
15
14
  def create_plugin
16
15
  puts "=> Creating new ci plugin: #{name}"
17
16
  core_template_source("plugin/ci")
18
- exclude_pattern = "pr\.rb" unless options[:pr]
19
- directory ".", "terraspace_ci_#{name}", exclude_pattern: exclude_pattern
17
+ directory ".", "terraspace_ci_#{name}"
20
18
  end
21
19
 
22
20
  def finish_message
@@ -27,7 +25,6 @@ class Terraspace::CLI::New::Plugin
27
25
  "lib/#{gem_name}/interface.rb",
28
26
  "README.md",
29
27
  ]
30
- files << "lib/#{gem_name}/pr.rb" if @options[:pr]
31
28
  files.sort!
32
29
  list = files.map { |file| " #{file}" }.join("\n")
33
30
  puts <<~EOL
@@ -1,13 +1,77 @@
1
1
  class Terraspace::CLI
2
2
  class Plan < Base
3
- include TfcConcern
4
3
  include Concerns::PlanPath
4
+ include Terraspace::Cloud::Streamer
5
+ include Terraspace::Cloud::Vcs::Commenter
6
+ include TfcConcern
5
7
 
6
8
  def run
9
+ cloud_plan.cani?
10
+ @stream = cloud_stream.open("plan")
11
+ success = perform
12
+ cloud_stream.close(success, @exception)
13
+ exit 1 unless success
14
+ end
15
+
16
+ def perform
17
+ success = plan_only
18
+ plan = cloud_plan.create(success, @stream)
19
+ if success && plan # possible from no changes / recording is disabled
20
+ resp = cloud_cost.cani?(exit_on_error: false)
21
+ if resp['errors'] # info on why cannot create a plan from resp
22
+ logger.info "WARN: Not creating a cost estimate."
23
+ logger.info resp['errors'][0]['detail']
24
+ else
25
+ cost = cloud_cost.create(uid: plan['data']['id'], stream: @stream)
26
+ pr_comment(plan, cost)
27
+ end
28
+ logger.info "Terraspace Cloud #{plan['data']['attributes']['url']}"
29
+ end
30
+ success
31
+ rescue Exception => e
32
+ @exception = true
33
+ logger.info "Exception #{e.class}: #{e.message}".color(:red)
34
+ logger.info e.backtrace.join("\n")
35
+ false
36
+ end
37
+
38
+ def plan_only
7
39
  if Terraspace.cloud? && !@options[:out]
8
40
  @options[:out] = plan_path
9
41
  end
10
- Commander.new("plan", @options).run
42
+ cloud_plan.setup
43
+ success = commander.run
44
+ copy_out_file_to_root
45
+ success
46
+ end
47
+
48
+ def commander
49
+ Commander.new("plan", @options)
50
+ end
51
+ memoize :commander
52
+
53
+ def copy_out_file_to_root
54
+ file = @mod.out_option
55
+ return if !file || @options[:copy_to_root] == false
56
+ return if file =~ %r{^/} # not need to copy absolute path
57
+
58
+ name = file.sub("#{Terraspace.root}/",'')
59
+ src = "#{@mod.cache_dir}/#{name}"
60
+ dest = name
61
+ return unless File.exist?(src) # plan wont exists if the plan errors
62
+ FileUtils.mkdir_p(File.dirname(dest))
63
+ FileUtils.cp(src, dest)
64
+ !!dest
65
+ end
66
+
67
+ def cloud_plan
68
+ Terraspace::Cloud::Plan.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
69
+ end
70
+ memoize :cloud_plan
71
+
72
+ def cloud_cost
73
+ Terraspace::Cloud::Cost.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
11
74
  end
75
+ memoize :cloud_cost
12
76
  end
13
77
  end
@@ -1,26 +1,80 @@
1
- require 'securerandom'
2
-
3
1
  class Terraspace::CLI
4
2
  class Up < Base
5
- include TfcConcern
6
3
  include Concerns::PlanPath
4
+ include Terraspace::Cloud::Streamer
5
+ include Terraspace::Cloud::Vcs::Commenter
6
+ include TfcConcern
7
7
 
8
8
  def run
9
+ cloud_update.cani?
10
+ @stream = cloud_stream.open("up")
11
+ success = perform
12
+ create_cloud_records(success)
13
+ cloud_stream.close(success, @exception)
14
+ exit 1 unless success
15
+ end
16
+
17
+ def perform
18
+ success = nil
9
19
  build
10
20
  if @options[:yes] && !@options[:plan] && !tfc?
11
- if Terraspace.cloud? && !@options[:plan]
12
- @options[:plan] = plan_path # for terraform apply
13
- @options[:out] = plan_path # for terraform plan
14
- end
15
- Commander.new("plan", @options).run
16
- return unless File.exist?(plan_path) if Terraspace.cloud? # happens if plan fails
17
- Commander.new("apply", @options).run
21
+ success = plan
18
22
  else
19
- Commander.new("apply", @options).run
23
+ skip_plan = true
24
+ end
25
+ if success or skip_plan
26
+ success = apply
20
27
  end
28
+ success
29
+ rescue Exception => e
30
+ @exception = true
31
+ logger.info "Exception #{e.class}: #{e.message}".color(:red)
32
+ logger.info e.backtrace.join("\n")
33
+ false
21
34
  end
22
35
 
23
36
  private
37
+ def plan
38
+ if Terraspace.cloud? && !@options[:plan]
39
+ @options[:plan] = plan_path # for terraform apply
40
+ @options[:out] = plan_path # for terraform plan
41
+ end
42
+
43
+ plan = Plan.new(@options)
44
+ plan.plan_only # returns success: true or false
45
+ end
46
+
47
+ def apply
48
+ commander = Commander.new("apply", @options)
49
+ commander.run
50
+ end
51
+
52
+ def create_cloud_records(success)
53
+ update = cloud_update.create(success, @stream)
54
+ return unless success && update # possible from no changes / recording is disabled
55
+
56
+ resp = cloud_cost.cani?(exit_on_error: false)
57
+ if resp['errors'] # info on why cannot create a plan from resp
58
+ logger.info "WARN: Not creating a cost estimate."
59
+ logger.info resp['errors'][0]['detail']
60
+ else
61
+ cost = cloud_cost.create(uid: update['data']['id'], stream: @stream)
62
+ pr_comment(update, cost)
63
+ end
64
+
65
+ logger.info "Terraspace Cloud #{update['data']['attributes']['url']}" if update
66
+ end
67
+
68
+ def cloud_update
69
+ Terraspace::Cloud::Update.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
70
+ end
71
+ memoize :cloud_update
72
+
73
+ def cloud_cost
74
+ Terraspace::Cloud::Cost.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
75
+ end
76
+ memoize :cloud_cost
77
+
24
78
  # must build to compute tfc?
25
79
  def build
26
80
  Terraspace::Builder.new(@options).run
@@ -1,14 +1,16 @@
1
1
  class Terraspace::Cloud::Api
2
2
  class Cani
3
+ class Cannot < Terraspace::Error; end
3
4
  include Terraspace::Util::Logging
4
5
 
5
6
  def initialize(result)
6
7
  @result = result
7
8
  end
8
9
 
9
- # {"data":{"attributes":{"detail":"You are authorized to perform this action.","status":200,"title":"Authoriz
10
+ # Example http responses:
11
+ # {"data":{"attributes":{"detail":"You are authorized to perform this action.","status":200,"title":"Authoriz...
10
12
  # {"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
13
+ def handle(exit_on_error=true)
12
14
  yes = false # assume do not have permission
13
15
  detail = @result&.dig('data', 'attributes', 'detail')
14
16
  if detail&.include?('You are authorized to perform this action')
@@ -16,15 +18,19 @@ class Terraspace::Cloud::Api
16
18
  end
17
19
  return if yes
18
20
 
19
- if @result.nil? # 400 Bad Request
20
- logger.info "ERROR: It doesn't look like TS_TOKEN is valid".color(:red)
21
+ if exit_on_error
22
+ if @result.nil? # 400 Bad Request
23
+ logger.info "ERROR: It doesn't look like TS_TOKEN is valid".color(:red)
24
+ else
25
+ errors = @result.dig('errors')
26
+ detail = errors.first['detail']
27
+ # {"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"}]}
28
+ logger.info "ERROR: #{detail}".color(:red)
29
+ end
30
+ exit 1
21
31
  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)
32
+ @result # original http response with error info
26
33
  end
27
- exit 1
28
34
  end
29
35
  end
30
36
  end
@@ -2,7 +2,6 @@ class Terraspace::Cloud::Api
2
2
  module Concern
3
3
  extend Memoist
4
4
  include Errors
5
- include Record
6
5
  include Validate
7
6
 
8
7
  def api
@@ -5,6 +5,7 @@ class Terraspace::Cloud::Api
5
5
 
6
6
  # Always translate raw json response to ruby Hash
7
7
  def request(klass, path, data={})
8
+ exit_on_error = data.delete(:exit_on_error) # for cani logic
8
9
  url = url(path)
9
10
  req = build_request(klass, url, data)
10
11
  retries = 0
@@ -26,7 +27,7 @@ class Terraspace::Cloud::Api
26
27
  end
27
28
  end
28
29
  result = load_json(url, resp)
29
- Cani.new(result).handle if data[:cani]
30
+ Cani.new(result).handle(exit_on_error) if data[:cani]
30
31
  result
31
32
  end
32
33
 
@@ -93,7 +94,11 @@ class Terraspace::Cloud::Api
93
94
  "#{endpoint}/#{path}"
94
95
  end
95
96
 
96
- def get(path)
97
+ def get(path, data={})
98
+ unless data.empty?
99
+ separator = path.include?('?') ? '&' : '?'
100
+ path += separator + data.to_query
101
+ end
97
102
  request(Net::HTTP::Get, path)
98
103
  end
99
104
 
@@ -16,18 +16,42 @@ module Terraspace::Cloud
16
16
  "orgs/#{@org}/projects/#{@project}/stacks/#{@stack}"
17
17
  end
18
18
 
19
- def create_upload
20
- post("#{stack_path}/uploads", @options)
19
+ # data: {stream_id:}
20
+ def create_upload(data)
21
+ post("#{stack_path}/uploads", @options.merge(data))
21
22
  end
22
23
 
23
- # record_attrs: {upload_id: "upload-nRPSpyWd65Ps6978", kind: "apply", stack_id: '...'}
24
+ # data: {stream_id:}
25
+ def create_stream(data)
26
+ post("#{stack_path}/streams", @options.merge(data))
27
+ end
28
+
29
+ # data: {id:, success:}
30
+ def complete_stream(data={})
31
+ post("#{stack_path}/streams/#{data[:id]}/complete", @options.merge(data))
32
+ end
33
+
34
+ # data: {upload_id: "upload-nRPSpyWd65Ps6978", kind: "apply", stack_id: '...'}
24
35
  def create_plan(data)
25
- post("#{stack_path}/plans", data.merge(@options))
36
+ post("#{stack_path}/plans", @options.merge(data))
26
37
  end
27
38
 
28
39
  # data: {upload_id: "upload-nRPSpyWd65Ps6978", kind: "apply", stack_id: '...'}
29
40
  def create_update(data)
30
- post("#{stack_path}/updates", data.merge(@options))
41
+ post("#{stack_path}/updates", @options.merge(data))
42
+ end
43
+
44
+ # data: {upload_id: "upload-nRPSpyWd65Ps6978", stack_id: '...'}
45
+ def create_cost(data)
46
+ post("#{stack_path}/costs", @options.merge(data))
47
+ end
48
+
49
+ def get_previous_cost(data)
50
+ get("#{stack_path}/costs/previous", @options.merge(data))
51
+ end
52
+
53
+ def get_comment(data)
54
+ get("#{stack_path}/comment", @options.merge(data))
31
55
  end
32
56
  end
33
57
  end