Class: Terraspace::Terraform::RemoteState::Fetcher

Inherits:
Object
  • Object
show all
Extended by:
Memoist
Includes:
Compiler::CommandsConcern, Compiler::DirsConcern, Util::Logging
Defined in:
lib/terraspace/terraform/remote_state/fetcher.rb

Constant Summary collapse

@@pull_successes =
{}
@@download_shown =
false

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Util::Logging

#logger

Methods included from Compiler::DirsConcern

#cache_dirs, #dirs, #extract_stack_name, #local_paths, #mod_names, #select_stack?, #stack_names, #with_each_mod

Methods included from Compiler::CommandsConcern

#command_is?

Constructor Details

#initialize(parent, identifier, options = {}) ⇒ Fetcher

Returns a new instance of Fetcher.



8
9
10
11
12
13
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 8

def initialize(parent, identifier, options={})
  @parent, @identifier, @options = parent, identifier, options
  child_name, @output_key = identifier.split('.')
  @child = Terraspace::Mod.new(child_name)
  @child.resolved = @parent.resolved
end

Class Method Details

.flush!Object



153
154
155
156
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 153

def flush!
  @@pull_successes = {}
  @@cache = {}
end

Instance Method Details

#bucket_not_found_errorObject

mimic pull error



96
97
98
99
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 96

def bucket_not_found_error
  @@pull_successes[cache_key] = false
  @error_type = :bucket_not_found
end

#cache_keyObject



115
116
117
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 115

def cache_key
  @child.name
end

#initObject



87
88
89
90
91
92
93
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 87

def init
  Terraspace::CLI::Init.new(mod: @child.name, quiet: true, suppress_error_color: true).init
  true
rescue Terraspace::BucketNotFoundError # from Terraspace::Shell
  bucket_not_found_error
  false
end

#loadObject



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 101

def load
  return self unless pull_success?

  # use or set cache
  if @@cache[cache_key]
    @outputs = @@cache[cache_key]
  else
    @outputs = @@cache[cache_key] = read_statefile_outputs
  end

  self
end

#log_message(msg) ⇒ Object

Using debug level because all the tfvar files always get evaluated. So dont want these messages to show up and be noisy unless debugging.



147
148
149
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 147

def log_message(msg)
  logger.debug "DEBUG: #{msg}".color(:yellow)
end

#outputObject

Returns OutputProxy



22
23
24
25
26
27
28
29
30
31
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 22

def output
  run
  if pull_success?
    pull_success_output
  else
    @error_type ||= :state_not_found # could be set to :bucket_not_found by bucket_not_found_error
    error = output_error(@error_type)
    OutputProxy.new(@child, nil, @options.merge(error: error))
  end
end

#output_error(type) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 48

def output_error(type)
  msg = case type
  when :key_not_found
    "Output #{@output_key} was not found for the #{@parent.name} tfvars file. Either #{@child.name} stack has not been deployed yet or it does not have this output: #{@output_key}. Also, if local backend is being used and has been removed/cleaned, then it will also result zero-byte state.json with the 'terraform state pull' used to download the terraform state and output will not be found."
  when :state_not_found
    "Output #{@output_key} could not be looked up for the #{@parent.name} tfvars file. #{@child.name} stack needs to be deployed"
  when :bucket_not_found
    "The bucket for the backend could not be found"
  end
  msg = "(#{msg})"
  log_message(msg)
  msg
end

#output_valueObject



42
43
44
45
46
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 42

def output_value
  return unless @outputs.key?(@output_key)
  result = @outputs.dig(@output_key)
  result.dig("value") if result
end

#pullObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 64

def pull
  return if @@pull_successes[cache_key]
  logger.debug "Downloading tfstate files for dependencies defined in tfvars..." unless @@download_shown || @options[:quiet]
  @@download_shown = true
  logger.debug "Downloading tfstate for stack: #{@child.name}"

  success = init # init not yet run. only run .init directly, not .run. init can completely error and early exit.
  return unless success

  FileUtils.mkdir_p(File.dirname(state_path))
  command = "cd #{@child.cache_dir} && #{Terraspace.terraform_bin} state pull > #{state_path}"
  logger.debug "=> #{command}"
  success = system(command)
  # Can error if using a old terraform version and the statefile was created with a newer version of terraform
  # IE: Failed to refresh state: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.29;
  #     upgrade to Terraform v0.13.2 or greater to work with this state
  unless success
    logger.info "Error running: #{command}".color(:red)
    logger.info "Please fix the error before continuing"
  end
  @@pull_successes[cache_key] = success
end

#pull_success?Boolean

Returns:

  • (Boolean)


124
125
126
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 124

def pull_success?
  @@pull_successes[cache_key]
end

#pull_success_outputObject



33
34
35
36
37
38
39
40
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 33

def pull_success_output
  if @outputs.key?(@output_key)
    OutputProxy.new(@child, output_value, @options)
  else
    error = output_error(:key_not_found)
    OutputProxy.new(@child, nil, @options.merge(error: error))
  end
end

#read_statefile_outputsObject



119
120
121
122
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 119

def read_statefile_outputs
  data = JSON.load(IO.read(state_path))
  data ? data['outputs'] : {}
end

#runObject



15
16
17
18
19
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 15

def run
  validate! # check child stack exists
  pull
  load
end

#state_pathObject



128
129
130
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 128

def state_path
  "#{Terraspace.tmp_root}/remote_state/#{@child.build_dir}/state.json"
end

#validate!Object

Note we already validate mod exist at the terraform_output helper. This is just in case that logic changes.



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/terraspace/terraform/remote_state/fetcher.rb', line 133

def validate!
  unless @child.exist?
    logger.error "ERROR: stack #{@child.name} not found".color(:red)
    exit 1
  end
  select = Terraspace::Compiler::Select.new(@child.name)
  unless select.selected?
    logger.error "ERROR: stack #{@child.name} is configured to not be included. IE: config.all.include_stacks or config.all.exclude_stacks".color(:red)
    exit 1
  end
end