terraspace 2.0.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) 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/.github/FUNDING.yml +1 -0
  12. data/.pipedream/README.md +1 -1
  13. data/.pipedream/pipeline.rb +15 -8
  14. data/CHANGELOG.md +16 -0
  15. data/lib/templates/base/project/Gemfile.tt +5 -0
  16. data/lib/templates/base/project/config/app.rb +6 -2
  17. data/lib/templates/plugin/ci/CHANGELOG.md.tt +1 -1
  18. data/lib/templates/plugin/ci/lib/%gem_name%/vars.rb.tt +1 -1
  19. data/lib/terraspace/app.rb +16 -0
  20. data/lib/terraspace/cli/base.rb +1 -0
  21. data/lib/terraspace/cli/commander.rb +2 -1
  22. data/lib/terraspace/cli/concerns/plan_path.rb +1 -1
  23. data/lib/terraspace/cli/down.rb +43 -6
  24. data/lib/terraspace/cli/logs/concern.rb +1 -1
  25. data/lib/terraspace/cli/new/plugin/ci.rb +1 -4
  26. data/lib/terraspace/cli/plan.rb +66 -2
  27. data/lib/terraspace/cli/up.rb +65 -11
  28. data/lib/terraspace/cloud/api/cani.rb +15 -9
  29. data/lib/terraspace/cloud/api/concern.rb +0 -1
  30. data/lib/terraspace/cloud/api/http_methods.rb +7 -2
  31. data/lib/terraspace/cloud/api.rb +29 -5
  32. data/lib/terraspace/cloud/base.rb +16 -29
  33. data/lib/terraspace/cloud/ci.rb +1 -8
  34. data/lib/terraspace/cloud/comment.rb +28 -0
  35. data/lib/terraspace/cloud/context.rb +1 -0
  36. data/lib/terraspace/cloud/cost/infracost.rb +80 -0
  37. data/lib/terraspace/cloud/cost.rb +68 -0
  38. data/lib/terraspace/cloud/git.rb +0 -0
  39. data/lib/terraspace/cloud/plan.rb +33 -15
  40. data/lib/terraspace/cloud/stream.rb +113 -0
  41. data/lib/terraspace/cloud/streamer.rb +9 -0
  42. data/lib/terraspace/cloud/update.rb +19 -19
  43. data/lib/terraspace/cloud/{folder → upload}/base.rb +2 -2
  44. data/lib/terraspace/cloud/{folder → upload}/package.rb +2 -2
  45. data/lib/terraspace/cloud/{folder → upload}/tidy.rb +1 -1
  46. data/lib/terraspace/cloud/upload.rb +53 -0
  47. data/lib/terraspace/cloud/vcs/base.rb +6 -0
  48. data/lib/terraspace/cloud/vcs/ci_env.rb +15 -0
  49. data/lib/terraspace/cloud/vcs/commenter.rb +75 -0
  50. data/lib/terraspace/cloud/vcs/interface.rb +14 -0
  51. data/lib/terraspace/cloud/vcs/local_env.rb +25 -0
  52. data/lib/terraspace/cloud/{ci/vcs → vcs/local_git}/base.rb +6 -3
  53. data/lib/terraspace/cloud/vcs/local_git/bitbucket.rb +17 -0
  54. data/lib/terraspace/cloud/vcs/local_git/github.rb +17 -0
  55. data/lib/terraspace/cloud/vcs/local_git/gitlab.rb +17 -0
  56. data/lib/terraspace/cloud/{ci/manual.rb → vcs/local_git.rb} +18 -10
  57. data/lib/terraspace/cloud/vcs.rb +21 -0
  58. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +1 -1
  59. data/lib/terraspace/logger.rb +3 -2
  60. data/lib/terraspace/shell/error.rb +1 -1
  61. data/lib/terraspace/terraform/runner.rb +4 -22
  62. data/lib/terraspace/util/popen.rb +67 -0
  63. data/lib/terraspace/version.rb +1 -1
  64. metadata +26 -23
  65. data/lib/templates/plugin/ci/lib/%gem_name%/pr.rb.tt +0 -15
  66. data/lib/terraspace/cloud/api/concern/record.rb +0 -18
  67. data/lib/terraspace/cloud/ci/generic.rb +0 -25
  68. data/lib/terraspace/cloud/ci/vcs/bitbucket.rb +0 -11
  69. data/lib/terraspace/cloud/ci/vcs/github.rb +0 -11
  70. data/lib/terraspace/cloud/ci/vcs/gitlab.rb +0 -11
  71. data/lib/terraspace/cloud/ci/vcs.rb +0 -18
  72. data/lib/terraspace/cloud/folder/uploader.rb +0 -37
  73. data/lib/terraspace/cloud/folder.rb +0 -11
  74. data/lib/terraspace/terraform/ihooks/after/apply.rb +0 -8
  75. data/lib/terraspace/terraform/ihooks/after/destroy.rb +0 -8
  76. data/lib/terraspace/terraform/ihooks/after/plan.rb +0 -46
  77. data/lib/terraspace/terraform/ihooks/base.rb +0 -17
  78. data/lib/terraspace/terraform/ihooks/before/apply.rb +0 -8
  79. data/lib/terraspace/terraform/ihooks/before/destroy.rb +0 -8
  80. 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: 1a0597eb436b3d646b4163c32659fefccc2c8a023a1d155e6427ea2c809a9325
4
- data.tar.gz: 40219e371fc4c9e8bb9f9b6e8c8e4806b3a1c5490a27d3e566467a966f8cf98e
3
+ metadata.gz: 3651e7cf7b8fe679a5266a2bc6bbad9f30d2c98d8636bbf6105ee7d61585d2ed
4
+ data.tar.gz: 8dd9e2fb0801ea3f4e82711460edbf18cc578932443d7ef9c2844fe644e49a09
5
5
  SHA512:
6
- metadata.gz: 466a494240d4b8c15f0ed6c8fb3a33973b5fbdf590c8ca1b18ce34718b84f0c14ce7487f2813d9a6202b5da9d500c93f408e669642b19c4c86f1e20e90d1d32a
7
- data.tar.gz: 798bb6923e162bb18586de66f3ce9f5ac1e0182297a790569d6e40cd8104160b5ad9164bf81cc0e44d13bc140826f3ee30cb7701bb05c916e4ca365ab2d2d0c4
6
+ metadata.gz: da083a64468e8b447e10881762d52e52859ce74c72be6d8f7ca0e8a457da1818f99cd3e615a87b7f43758abcc46297b1eec1c2e1550b1b89c96e47c0ad851b96
7
+ data.tar.gz: 6b51fd61cbdabe1a5e51fac73d3a93fba10143ac040526d8283d48afa90a9c3d26e738e1d50449bfd1a029cce99001a2af0f91edb3f0ca005779f256572eac9c
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
@@ -0,0 +1 @@
1
+ github: boltops-tools
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,22 @@
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.1] - 2022-07-12
7
+ - [#248](https://github.com/boltops-tools/terraspace/pull/248) fix destroy success return
8
+
9
+ ## [2.1.0] - 2022-07-11
10
+ - [#247](https://github.com/boltops-tools/terraspace/pull/247) cost estimates and real-time stream logging
11
+ - Cloud Cost Estimation support
12
+ - Live Stream Logging support
13
+ - CI/CD plugin updates. Decoupled CI from PR plugins
14
+ - Improve git info. Get git info with or without CI
15
+ - Fix utf8 encoding edge cases
16
+
17
+ ## [2.0.3] - 2022-07-04
18
+ - [#237](https://github.com/boltops-tools/terraspace/pull/237) remove duplicate paths in layering
19
+ - [#244](https://github.com/boltops-tools/terraspace/pull/244) rename _cache2 to .cache2
20
+ - [#245](https://github.com/boltops-tools/terraspace/pull/245) set utf8 in case system is not configured with LANG=en_US.UTF-8
21
+
6
22
  ## [2.0.2] - 2022-06-21
7
23
  - [#242](https://github.com/boltops-tools/terraspace/pull/242) :PROJECT variable: TS_PROJECT as well as config.cloud.project
8
24
 
@@ -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
@@ -2,7 +2,7 @@ module Terraspace::CLI::Concerns
2
2
  module PlanPath
3
3
  # Use when --out option not used
4
4
  def plan_path
5
- ".terraspace-cache/_cache2/plan/plan.binary"
5
+ ".terraspace-cache/.cache2/plan/plan.binary"
6
6
  end
7
7
  end
8
8
  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,26 @@ 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
+ success
54
+ end
55
+
56
+ def cloud_update
57
+ Terraspace::Cloud::Update.new(@options.merge(stack: @mod.name, kind: kind, vcs_vars: vcs_vars))
22
58
  end
59
+ memoize :cloud_update
23
60
  end
24
61
  end
@@ -2,7 +2,7 @@ class Terraspace::CLI::Logs
2
2
  module Concern
3
3
  # Filters for lines that belong to the last ran process pid
4
4
  def readlines(path)
5
- lines = IO.readlines(path)
5
+ lines = IO.readlines(path).map { |l| l.force_encoding('UTF-8') }
6
6
  found = lines.reverse.find do |line|
7
7
  pid(line) # search in reverse order for lines with interesting info
8
8
  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