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.
@@ -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