terrafying 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ module Terrafying
2
+ VERSION = "0.0.0" # will be inserted by Drone
3
+ CLI_VERSION = "0.11.3"
4
+ end