terrafying 0.0.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.
- checksums.yaml +7 -0
- data/bin/terrafying +6 -0
- data/lib/hash/deep_merge.rb +6 -0
- data/lib/terrafying/aws.rb +542 -0
- data/lib/terrafying/cli.rb +73 -0
- data/lib/terrafying/dynamodb/config.rb +17 -0
- data/lib/terrafying/dynamodb/named_lock.rb +126 -0
- data/lib/terrafying/dynamodb/state.rb +92 -0
- data/lib/terrafying/dynamodb.rb +31 -0
- data/lib/terrafying/generator.rb +166 -0
- data/lib/terrafying/lock.rb +25 -0
- data/lib/terrafying/state.rb +51 -0
- data/lib/terrafying/util.rb +32 -0
- data/lib/terrafying/version.rb +4 -0
- data/lib/terrafying.rb +270 -0
- metadata +185 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'terrafying/dynamodb'
|
2
|
+
require 'terrafying/dynamodb/config'
|
3
|
+
|
4
|
+
module Terrafying
|
5
|
+
module DynamoDb
|
6
|
+
class NamedLock
|
7
|
+
def initialize(table_name, name)
|
8
|
+
@table_name = table_name
|
9
|
+
@name = name
|
10
|
+
@client = Terrafying::DynamoDb.client
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
@client.ensure_table(table) do
|
15
|
+
resp = @client.get_item({
|
16
|
+
table_name: @table_name,
|
17
|
+
key: {
|
18
|
+
"name" => @name,
|
19
|
+
},
|
20
|
+
consistent_read: true,
|
21
|
+
})
|
22
|
+
if resp.item
|
23
|
+
return {
|
24
|
+
status: :locked,
|
25
|
+
locked_at: resp.item["locked_at"],
|
26
|
+
metadata: resp.item["metadata"]
|
27
|
+
}
|
28
|
+
else
|
29
|
+
return {
|
30
|
+
status: :unlocked
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def acquire
|
37
|
+
@client.ensure_table(table) do
|
38
|
+
begin
|
39
|
+
lock_id = SecureRandom.uuid
|
40
|
+
@client.update_item(acquire_request(lock_id))
|
41
|
+
return lock_id
|
42
|
+
rescue ::Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
43
|
+
raise "Unable to acquire lock: #{status.inspect}" # TODO
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def steal
|
49
|
+
@client.ensure_table(table) do
|
50
|
+
begin
|
51
|
+
lock_id = SecureRandom.uuid
|
52
|
+
req = acquire_request(lock_id)
|
53
|
+
req.delete(:condition_expression)
|
54
|
+
@client.update_item(req)
|
55
|
+
return lock_id
|
56
|
+
rescue ::Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
57
|
+
raise "Unable to steal lock: #{status.inspect}" # TODO
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def release(lock_id)
|
63
|
+
@client.ensure_table(table) do
|
64
|
+
begin
|
65
|
+
@client.delete_item({
|
66
|
+
table_name: @table_name,
|
67
|
+
key: {
|
68
|
+
"name" => @name,
|
69
|
+
},
|
70
|
+
return_values: "NONE",
|
71
|
+
condition_expression: "lock_id = :lock_id",
|
72
|
+
expression_attribute_values: {
|
73
|
+
":lock_id" => lock_id,
|
74
|
+
},
|
75
|
+
})
|
76
|
+
nil
|
77
|
+
rescue ::Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
78
|
+
raise "Unable to release lock: #{status.inspect}" # TODO
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def acquire_request(lock_id)
|
85
|
+
{
|
86
|
+
table_name: @table_name,
|
87
|
+
key: {
|
88
|
+
"name" => @name,
|
89
|
+
},
|
90
|
+
return_values: "NONE",
|
91
|
+
update_expression: "SET lock_id = :lock_id, locked_at = :locked_at, metadata = :metadata",
|
92
|
+
condition_expression: "attribute_not_exists(lock_id)",
|
93
|
+
expression_attribute_values: {
|
94
|
+
":lock_id" => lock_id,
|
95
|
+
":locked_at" => Time.now.to_s,
|
96
|
+
":metadata" => {
|
97
|
+
"owner" => "#{`git config user.name`.chomp} (#{`git config user.email`.chomp})",
|
98
|
+
},
|
99
|
+
},
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def table
|
104
|
+
{
|
105
|
+
table_name: @table_name,
|
106
|
+
attribute_definitions: [
|
107
|
+
{
|
108
|
+
attribute_name: "name",
|
109
|
+
attribute_type: "S",
|
110
|
+
},
|
111
|
+
],
|
112
|
+
key_schema: [
|
113
|
+
{
|
114
|
+
attribute_name: "name",
|
115
|
+
key_type: "HASH",
|
116
|
+
},
|
117
|
+
],
|
118
|
+
provisioned_throughput: {
|
119
|
+
read_capacity_units: 1,
|
120
|
+
write_capacity_units: 1,
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'terrafying/dynamodb/config'
|
3
|
+
|
4
|
+
module Terrafying
|
5
|
+
module DynamoDb
|
6
|
+
class StateStore
|
7
|
+
def initialize(scope, opts = {})
|
8
|
+
@scope = scope
|
9
|
+
@client = Terrafying::DynamoDb.client
|
10
|
+
@table_name = Terrafying::DynamoDb.config.state_table
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
@client.ensure_table(table) do
|
15
|
+
resp = @client.query({
|
16
|
+
table_name: @table_name,
|
17
|
+
limit: 1,
|
18
|
+
key_conditions: {
|
19
|
+
"scope" => {
|
20
|
+
attribute_value_list: [@scope],
|
21
|
+
comparison_operator: "EQ",
|
22
|
+
}
|
23
|
+
},
|
24
|
+
scan_index_forward: false,
|
25
|
+
})
|
26
|
+
case resp.items.count
|
27
|
+
when 0 then return nil
|
28
|
+
when 1 then return resp.items.first["state"]
|
29
|
+
else raise 'More than one item found when retrieving state. This is a bug and should never happen.' if resp.items.count != 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def put(state)
|
35
|
+
@client.ensure_table(table) do
|
36
|
+
sha256 = Digest::SHA256.hexdigest(state)
|
37
|
+
json = JSON.parse(state)
|
38
|
+
@client.update_item({
|
39
|
+
table_name: @table_name,
|
40
|
+
key: {
|
41
|
+
"scope" => @scope,
|
42
|
+
"serial" => json["serial"].to_i,
|
43
|
+
},
|
44
|
+
return_values: "NONE",
|
45
|
+
update_expression: "SET sha256 = :sha256, #state = :state",
|
46
|
+
condition_expression: "attribute_not_exists(serial) OR sha256 = :sha256",
|
47
|
+
expression_attribute_names: {
|
48
|
+
"#state" => "state",
|
49
|
+
},
|
50
|
+
expression_attribute_values: {
|
51
|
+
":sha256" => sha256,
|
52
|
+
":state" => state,
|
53
|
+
}
|
54
|
+
})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def table
|
59
|
+
{
|
60
|
+
table_name: @table_name,
|
61
|
+
attribute_definitions: [
|
62
|
+
{
|
63
|
+
attribute_name: "scope",
|
64
|
+
attribute_type: "S",
|
65
|
+
},
|
66
|
+
{
|
67
|
+
attribute_name: "serial",
|
68
|
+
attribute_type: "N",
|
69
|
+
}
|
70
|
+
],
|
71
|
+
key_schema: [
|
72
|
+
{
|
73
|
+
attribute_name: "scope",
|
74
|
+
key_type: "HASH",
|
75
|
+
},
|
76
|
+
{
|
77
|
+
attribute_name: "serial",
|
78
|
+
key_type: "RANGE",
|
79
|
+
},
|
80
|
+
|
81
|
+
],
|
82
|
+
provisioned_throughput: {
|
83
|
+
read_capacity_units: 1,
|
84
|
+
write_capacity_units: 1,
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'json'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
# oh rubby
|
6
|
+
class ::Aws::DynamoDB::Client
|
7
|
+
def ensure_table(table_spec, &block)
|
8
|
+
retried = false
|
9
|
+
begin
|
10
|
+
yield block
|
11
|
+
rescue ::Aws::DynamoDB::Errors::ResourceNotFoundException => e
|
12
|
+
if not retried
|
13
|
+
create_table(table_spec)
|
14
|
+
retry
|
15
|
+
else
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Terrafying
|
23
|
+
module DynamoDb
|
24
|
+
def self.client
|
25
|
+
@@client ||= ::Aws::DynamoDB::Client.new({
|
26
|
+
region: Terrafying::Context::REGION,
|
27
|
+
#endpoint: 'http://localhost:8000',
|
28
|
+
})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'erb'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'deep_merge'
|
6
|
+
require 'terrafying/aws'
|
7
|
+
|
8
|
+
module Terrafying
|
9
|
+
|
10
|
+
class Ref
|
11
|
+
|
12
|
+
def initialize(var)
|
13
|
+
@var = var
|
14
|
+
end
|
15
|
+
|
16
|
+
def downcase
|
17
|
+
Ref.new("lower(#{@var})")
|
18
|
+
end
|
19
|
+
|
20
|
+
def strip
|
21
|
+
Ref.new("trimspace(#{@var})")
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"${#{@var}}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_str
|
29
|
+
self.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def <=>(other)
|
33
|
+
self.to_s <=> other.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def ==(other)
|
37
|
+
self.to_s == other.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
class Context
|
43
|
+
|
44
|
+
REGION = ENV.fetch("AWS_REGION", "eu-west-1")
|
45
|
+
|
46
|
+
PROVIDER_DEFAULTS = {
|
47
|
+
aws: { region: REGION }
|
48
|
+
}
|
49
|
+
|
50
|
+
attr_reader :output
|
51
|
+
|
52
|
+
def initialize
|
53
|
+
@output = {
|
54
|
+
"resource" => {}
|
55
|
+
}
|
56
|
+
@children = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def aws
|
60
|
+
@@aws ||= Terrafying::Aws::Ops.new REGION
|
61
|
+
end
|
62
|
+
|
63
|
+
def provider(name, spec)
|
64
|
+
@output["provider"] ||= {}
|
65
|
+
@output["provider"][name] = spec
|
66
|
+
end
|
67
|
+
|
68
|
+
def data(type, name, spec)
|
69
|
+
@output["data"] ||= {}
|
70
|
+
@output["data"][type.to_s] ||= {}
|
71
|
+
@output["data"][type.to_s][name.to_s] = spec
|
72
|
+
id_of(type, name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def resource(type, name, attributes)
|
76
|
+
@output["resource"][type.to_s] ||= {}
|
77
|
+
@output["resource"][type.to_s][name.to_s] = attributes
|
78
|
+
id_of(type, name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def template(relative_path, params = {})
|
82
|
+
dir = caller_locations[0].path
|
83
|
+
filename = File.join(File.dirname(dir), relative_path)
|
84
|
+
erb = ERB.new(IO.read(filename))
|
85
|
+
erb.filename = filename
|
86
|
+
erb.result(OpenStruct.new(params).instance_eval { binding })
|
87
|
+
end
|
88
|
+
|
89
|
+
def output_with_children
|
90
|
+
@children.inject(@output) { |out, c| out.deep_merge(c.output_with_children) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def id_of(type,name)
|
94
|
+
output_of(type, name, "id")
|
95
|
+
end
|
96
|
+
|
97
|
+
def output_of(type, name, value)
|
98
|
+
Ref.new("#{type}.#{name}.#{value}")
|
99
|
+
end
|
100
|
+
|
101
|
+
def pretty_generate
|
102
|
+
JSON.pretty_generate(output_with_children)
|
103
|
+
end
|
104
|
+
|
105
|
+
def resource_names
|
106
|
+
out = output_with_children
|
107
|
+
ret = []
|
108
|
+
for type in out["resource"].keys
|
109
|
+
for id in out["resource"][type].keys
|
110
|
+
ret << "#{type}.#{id}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
ret
|
114
|
+
end
|
115
|
+
|
116
|
+
def resources
|
117
|
+
out = output_with_children
|
118
|
+
ret = []
|
119
|
+
for type in out["resource"].keys
|
120
|
+
for id in out["resource"][type].keys
|
121
|
+
ret << "${#{type}.#{id}.id}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
ret
|
125
|
+
end
|
126
|
+
|
127
|
+
def add!(*c)
|
128
|
+
@children.push(*c)
|
129
|
+
c[0]
|
130
|
+
end
|
131
|
+
|
132
|
+
def tf_safe(str)
|
133
|
+
str.gsub(/[\.\s\/\?]/, "-")
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
class RootContext < Context
|
139
|
+
|
140
|
+
def initialize
|
141
|
+
super
|
142
|
+
|
143
|
+
output["provider"] = PROVIDER_DEFAULTS
|
144
|
+
end
|
145
|
+
|
146
|
+
def backend(name, spec)
|
147
|
+
@output["terraform"] = {
|
148
|
+
backend: {
|
149
|
+
name => spec,
|
150
|
+
},
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def generate(&block)
|
155
|
+
instance_eval(&block)
|
156
|
+
end
|
157
|
+
|
158
|
+
def method_missing(fn, *args)
|
159
|
+
resource(fn, args.shift.to_s, args.first)
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
Generator = RootContext.new
|
165
|
+
|
166
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'terrafying/dynamodb/named_lock'
|
2
|
+
|
3
|
+
module Terrafying
|
4
|
+
module Locks
|
5
|
+
class NoOpLock
|
6
|
+
def acquire
|
7
|
+
""
|
8
|
+
end
|
9
|
+
def steal
|
10
|
+
""
|
11
|
+
end
|
12
|
+
def release(lock_id)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.noop
|
17
|
+
NoOpLock.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.dynamodb(scope)
|
21
|
+
Terrafying::DynamoDb::NamedLock.new(Terrafying::DynamoDb.config.lock_table, scope)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'terrafying/dynamodb/state'
|
2
|
+
|
3
|
+
module Terrafying
|
4
|
+
module State
|
5
|
+
|
6
|
+
STATE_FILENAME = "terraform.tfstate"
|
7
|
+
|
8
|
+
def self.store(config)
|
9
|
+
if LocalStateStore.has_local_state?(config)
|
10
|
+
local(config)
|
11
|
+
else
|
12
|
+
remote(config)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.local(config)
|
17
|
+
LocalStateStore.new(config.path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.remote(config)
|
21
|
+
Terrafying::DynamoDb::StateStore.new(config.scope)
|
22
|
+
end
|
23
|
+
|
24
|
+
class LocalStateStore
|
25
|
+
def initialize(path)
|
26
|
+
@path = LocalStateStore.state_path(path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get
|
30
|
+
IO.read(@path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def put(state)
|
34
|
+
IO.write(@path, state)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete
|
38
|
+
File.delete(@path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.has_local_state?(config)
|
42
|
+
File.exists?(state_path(config.path))
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def self.state_path(path)
|
47
|
+
File.join(File.dirname(path), STATE_FILENAME)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
def data_url_from_string(str)
|
5
|
+
b64_contents = Base64.strict_encode64(str)
|
6
|
+
return "data:;base64,#{b64_contents}"
|
7
|
+
end
|
8
|
+
|
9
|
+
module Terrafying
|
10
|
+
|
11
|
+
module Util
|
12
|
+
|
13
|
+
def self.to_ignition(yaml)
|
14
|
+
config = YAML.load(yaml)
|
15
|
+
|
16
|
+
if config.has_key? "storage" and config["storage"].has_key? "files"
|
17
|
+
files = config["storage"]["files"]
|
18
|
+
config["storage"]["files"] = files.each { |file|
|
19
|
+
if file["contents"].is_a? String
|
20
|
+
file["contents"] = {
|
21
|
+
source: data_url_from_string(file["contents"]),
|
22
|
+
}
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
JSON.pretty_generate(config)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|