Class: Bolt::Plugin::Terraform

Inherits:
Object
  • Object
show all
Defined in:
lib/bolt/plugin/terraform.rb

Constant Summary collapse

KNOWN_KEYS =
Set['_plugin', 'dir', 'resource_type', 'uri', 'name', 'statefile',
'config', 'backend']
REQ_KEYS =
Set['dir', 'resource_type']

Instance Method Summary collapse

Constructor Details

#initialize(*_args) ⇒ Terraform

Returns a new instance of Terraform.



12
13
14
# File 'lib/bolt/plugin/terraform.rb', line 12

def initialize(*_args)
  @logger = Logging.logger[self]
end

Instance Method Details

#extract_resources(state) ⇒ Object

Format the list of resources into a list of [name, attribute map] pairs. This method handles both version 4 and earlier statefiles, doing the appropriate munging based on the shape of the data.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/bolt/plugin/terraform.rb', line 118

def extract_resources(state)
  if state['version'] >= 4
    state.fetch('resources', []).flat_map do |resource_set|
      prefix = "#{resource_set['type']}.#{resource_set['name']}"
      resource_set['instances'].map do |resource|
        instance_name = prefix
        instance_name += ".#{resource['index_key']}" if resource['index_key']
        # When using `terraform state pull` with terraform >= 0.12 version 3 statefiles
        # Will be converted to version 4. When converted attributes is converted to attributes_flat
        attributes = resource['attributes'] || resource['attributes_flat']
        [instance_name, attributes]
      end
    end
  else
    state.fetch('modules', {}).flat_map do |mod|
      mod.fetch('resources', {}).map do |name, resource|
        [name, resource.dig('primary', 'attributes')]
      end
    end
  end
end

#hooksObject



20
21
22
# File 'lib/bolt/plugin/terraform.rb', line 20

def hooks
  [:resolve_reference]
end

#load_local_statefile(opts) ⇒ Object



107
108
109
110
111
112
113
# File 'lib/bolt/plugin/terraform.rb', line 107

def load_local_statefile(opts)
  dir = opts['dir']
  filename = opts.fetch('statefile', 'terraform.tfstate')
  File.read(File.expand_path(File.join(dir, filename)))
rescue StandardError => e
  raise Bolt::FileError.new("Could not load Terraform state file #{filename}: #{e}", filename)
end

#load_remote_statefile(opts) ⇒ Object

Uses the Terraform CLI to pull remote state files



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/bolt/plugin/terraform.rb', line 84

def load_remote_statefile(opts)
  dir = File.expand_path(opts['dir'])

  begin
    stdout_str, stderr_str, status = Open3.capture3('terraform state pull', chdir: dir)
  rescue Errno::ENOENT
    reason = if File.directory?(dir)
               "Could not find executable 'terraform'"
             else
               "Could not find directory '#{dir}'"
             end
    raise Bolt::Error.new(reason, 'FILE_ERROR')
  end

  unless status.success?
    err = stdout_str + stderr_str
    msg = "Could not pull Terraform remote state file for #{opts['dir']}:\n#{err}"
    raise Bolt::Error.new(msg, 'bolt/terraform-state-error')
  end

  stdout_str
end

#load_statefile(opts) ⇒ Object



73
74
75
76
77
78
79
80
81
# File 'lib/bolt/plugin/terraform.rb', line 73

def load_statefile(opts)
  statefile = if opts['backend'] == 'remote'
                load_remote_statefile(opts)
              else
                load_local_statefile(opts)
              end

  JSON.parse(statefile)
end

#lookup(name, resource, path) ⇒ Object

Look up a nested value from the resource attributes. The key is of the form ‘foo.bar.0.baz`. For terraform statefile version 3, this will exactly correspond to a key in the resource. In version 4, it will correspond to a nested hash entry at {bar: [{baz: <value>]}} For simplicity’s sake, we just check both.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/bolt/plugin/terraform.rb', line 145

def lookup(name, resource, path)
  segments = path.split('.').map do |segment|
    begin
      Integer(segment)
    rescue ArgumentError
      segment
    end
  end

  value = resource[path] || resource.dig(*segments)
  unless value
    warn_missing_property(name, path)
  end
  value
end

#nameObject



16
17
18
# File 'lib/bolt/plugin/terraform.rb', line 16

def name
  'terraform'
end

#resolve_config(name, resource, config_template) ⇒ Object

Walk the “template” config mapping provided in the plugin config and replace all values with the corresponding value from the resource parameters.



164
165
166
167
168
169
170
171
172
# File 'lib/bolt/plugin/terraform.rb', line 164

def resolve_config(name, resource, config_template)
  Bolt::Util.walk_vals(config_template) do |value|
    if value.is_a?(String)
      lookup(name, resource, value)
    else
      value
    end
  end
end

#resolve_reference(opts) ⇒ Object



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
# File 'lib/bolt/plugin/terraform.rb', line 44

def resolve_reference(opts)
  validate_options(opts)

  state = load_statefile(opts)

  resources = extract_resources(state)

  regex = Regexp.new(opts['resource_type'])

  resources.select do |name, _resource|
    name.match?(regex)
  end.map do |name, resource|
    target = {}

    if opts.key?('uri')
      uri = lookup(name, resource, opts['uri'])
      target['uri'] = uri if uri
    end
    if opts.key?('name')
      real_name = lookup(name, resource, opts['name'])
      target['name'] = real_name if real_name
    end
    if opts.key?('config')
      target['config'] = resolve_config(name, resource, opts['config'])
    end
    target
  end.compact
end

#validate_options(opts) ⇒ Object

Make sure no unexpected keys are in the inventory config and that required keys are present



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/bolt/plugin/terraform.rb', line 30

def validate_options(opts)
  opt_keys = opts.keys.to_set

  unless KNOWN_KEYS.superset?(opt_keys)
    keys = opt_keys - KNOWN_KEYS
    raise Bolt::ValidationError, "Unexpected key(s) in inventory config: #{keys.to_a.inspect}"
  end

  unless opt_keys.superset?(REQ_KEYS)
    keys = REQ_KEYS - opt_keys
    raise Bolt::ValidationError, "Expected key(s) in inventory config: #{keys.to_a.inspect}"
  end
end

#warn_missing_property(name, property) ⇒ Object



24
25
26
# File 'lib/bolt/plugin/terraform.rb', line 24

def warn_missing_property(name, property)
  @logger.warn("Could not find property #{property} of terraform resource #{name}")
end