gclouder 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +104 -0
  3. data/bin/gclouder +7 -0
  4. data/lib/gclouder/config/arguments.rb +35 -0
  5. data/lib/gclouder/config/cli_args.rb +77 -0
  6. data/lib/gclouder/config/cluster.rb +66 -0
  7. data/lib/gclouder/config/defaults.rb +35 -0
  8. data/lib/gclouder/config/files/project.rb +24 -0
  9. data/lib/gclouder/config/project.rb +34 -0
  10. data/lib/gclouder/config/resource_representations.rb +31 -0
  11. data/lib/gclouder/config_loader.rb +25 -0
  12. data/lib/gclouder/config_section.rb +26 -0
  13. data/lib/gclouder/dependencies.rb +11 -0
  14. data/lib/gclouder/gcloud.rb +39 -0
  15. data/lib/gclouder/gsutil.rb +25 -0
  16. data/lib/gclouder/header.rb +9 -0
  17. data/lib/gclouder/helpers.rb +77 -0
  18. data/lib/gclouder/logging.rb +181 -0
  19. data/lib/gclouder/mappings/argument.rb +31 -0
  20. data/lib/gclouder/mappings/file.rb +31 -0
  21. data/lib/gclouder/mappings/property.rb +31 -0
  22. data/lib/gclouder/mappings/resource_representation.rb +31 -0
  23. data/lib/gclouder/monkey_patches/array.rb +19 -0
  24. data/lib/gclouder/monkey_patches/boolean.rb +12 -0
  25. data/lib/gclouder/monkey_patches/hash.rb +44 -0
  26. data/lib/gclouder/monkey_patches/ipaddr.rb +10 -0
  27. data/lib/gclouder/monkey_patches/string.rb +30 -0
  28. data/lib/gclouder/resource.rb +63 -0
  29. data/lib/gclouder/resource_cleaner.rb +58 -0
  30. data/lib/gclouder/resources/compute/addresses.rb +108 -0
  31. data/lib/gclouder/resources/compute/bgp-vpns.rb +220 -0
  32. data/lib/gclouder/resources/compute/disks.rb +99 -0
  33. data/lib/gclouder/resources/compute/firewall_rules.rb +82 -0
  34. data/lib/gclouder/resources/compute/instances.rb +147 -0
  35. data/lib/gclouder/resources/compute/networks/subnets.rb +104 -0
  36. data/lib/gclouder/resources/compute/networks.rb +110 -0
  37. data/lib/gclouder/resources/compute/project_info/ssh_keys.rb +171 -0
  38. data/lib/gclouder/resources/compute/routers.rb +83 -0
  39. data/lib/gclouder/resources/compute/vpns.rb +199 -0
  40. data/lib/gclouder/resources/container/clusters.rb +257 -0
  41. data/lib/gclouder/resources/container/node_pools.rb +193 -0
  42. data/lib/gclouder/resources/dns.rb +390 -0
  43. data/lib/gclouder/resources/logging/sinks.rb +98 -0
  44. data/lib/gclouder/resources/project/iam_policy_binding.rb +293 -0
  45. data/lib/gclouder/resources/project.rb +85 -0
  46. data/lib/gclouder/resources/project_id.rb +71 -0
  47. data/lib/gclouder/resources/pubsub/subscriptions.rb +100 -0
  48. data/lib/gclouder/resources/pubsub/topics.rb +95 -0
  49. data/lib/gclouder/resources/storagebuckets.rb +103 -0
  50. data/lib/gclouder/resources/validate/global.rb +27 -0
  51. data/lib/gclouder/resources/validate/local.rb +68 -0
  52. data/lib/gclouder/resources/validate/region.rb +28 -0
  53. data/lib/gclouder/resources/validate/remote.rb +78 -0
  54. data/lib/gclouder/resources.rb +148 -0
  55. data/lib/gclouder/shell.rb +71 -0
  56. data/lib/gclouder/version.rb +5 -0
  57. data/lib/gclouder.rb +278 -0
  58. metadata +102 -0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module StorageBuckets
6
+ include GClouder::Logging
7
+ include GClouder::Shell
8
+ include GClouder::Resource::Cleaner
9
+
10
+ def self.header(stage = :ensure)
11
+ info "[#{stage}] storage / buckets", title: true, indent: 1
12
+ end
13
+
14
+ def self.validate
15
+ return if Local.list.empty?
16
+ header :validate
17
+ Local.validate
18
+ end
19
+
20
+ def self.ensure
21
+ return if Local.list.empty?
22
+ header
23
+
24
+ Local.list.each do |region, region_config|
25
+ info region, heading: true, indent: 2
26
+ region_config.each do |bucket|
27
+ info
28
+ StorageBucket.ensure(region, bucket)
29
+ end
30
+ end
31
+ end
32
+
33
+ module Local
34
+ def self.list
35
+ instances
36
+ end
37
+
38
+ def self.validate
39
+ # Validation knowledge included here because we don't have arguments parser for gsutil.
40
+ # We also don't support every key that gsutil does. See StorageBucket.ensure() below.
41
+ permitted_and_required_keys = {
42
+ "default_access"=>{"type"=>"String", "required"=>true}
43
+ }
44
+
45
+ Resources::Validate::Region.instances(
46
+ instances,
47
+ permitted_keys: permitted_and_required_keys
48
+ )
49
+ end
50
+
51
+ def self.instances
52
+ Resources::Region.instances(path: ["storage", "buckets"])
53
+ end
54
+ end
55
+
56
+ module Remote
57
+ include GClouder::GSUtil
58
+
59
+ # FIXME: make more robust(!)
60
+ def self.list
61
+ gsutil("ls", "-L").to_s.split("gs://").delete_if(&:empty?).each_with_object({}) do |data, collection|
62
+ normalized = data.split("\n").map! { |b| b.delete("\t") }
63
+ bucket_name = normalized[0].delete("/ :")
64
+ region = normalized.select { |e| e.match("^Location constraint:") }.last.split(":").last.downcase
65
+ collection[region] ||= []
66
+ collection[region] << { "name" => bucket_name }
67
+ end
68
+ end
69
+ end
70
+
71
+ module StorageBucket
72
+ include GClouder::GSUtil
73
+ include GClouder::Config::CLIArgs
74
+
75
+ def self.setDefaultAccess(bucket_name, default_access)
76
+ info "# gsutil defacl ch -u #{default_access} gs://#{bucket_name}" if cli_args[:debug]
77
+
78
+ return if cli_args[:dry_run]
79
+
80
+ # Just use shell, as -p flag is not valid for 'defacl ch'.
81
+ shell("gsutil defacl ch -u #{default_access} gs://#{bucket_name}")
82
+ end
83
+
84
+ def self.check_exists?(region, bucket_name)
85
+ gsutil_exec("ls", "gs://#{bucket_name} > /dev/null 2>&1 && echo 0 || echo 1").to_i == 0
86
+ end
87
+
88
+ def self.ensure(region, bucket)
89
+ if check_exists?(region, bucket["name"])
90
+ good bucket["name"]
91
+ return
92
+ end
93
+
94
+ add "#{bucket["name"]} [#{bucket["default_access"]}]"
95
+ gsutil "mb", "-l #{region} gs://#{bucket["name"]}"
96
+
97
+
98
+ setDefaultAccess bucket["name"], bucket["default_access"] if bucket.key?("default_access")
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Validate
6
+ module Global
7
+ include GClouder::Logging
8
+ include GClouder::Config::Project
9
+ include GClouder::Config::CLIArgs
10
+ include Local
11
+
12
+ def self.instances(data, required_keys: {}, permitted_keys: {}, ignore_keys: [])
13
+ return unless data.key?("global")
14
+
15
+ data["global"].each do |instance|
16
+ info instance["name"], heading: true, indent: 3
17
+
18
+ next if !has_unknown_keys?(instance, permitted_keys, ignore_keys) &&
19
+ has_required_keys?(instance, required_keys, ignore_keys, indent: 3)
20
+
21
+ fatal "\nerror: validation failure"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Validate
6
+ module Local
7
+ include GClouder::Logging
8
+
9
+ def self.included(klass)
10
+ klass.extend Local
11
+ end
12
+
13
+ # FIXME: this should probably recurse
14
+ def has_required_keys?(instance, required_keys, ignore_keys, indent: 3)
15
+ success = true
16
+
17
+ required_keys["name"] = {
18
+ "type" => "String",
19
+ "required" => true
20
+ }
21
+
22
+ required_keys.each do |key, data|
23
+ next if ignore_keys.include?(key)
24
+
25
+ if !instance.key?(key)
26
+ bad "#{key} is missing", indent: indent
27
+ success = false
28
+ end
29
+ end
30
+
31
+ success
32
+ end
33
+
34
+ def has_unknown_keys?(instance, permitted_keys, ignore_keys, indent: 0)
35
+ success = false
36
+
37
+ # a name is required for every resources
38
+ permitted_keys["name"] = {
39
+ "type" => "String",
40
+ "required" => true
41
+ }
42
+
43
+ instance.each do |key, value|
44
+ next if ignore_keys.include?(key)
45
+
46
+ if !permitted_keys.key?(key)
47
+ bad "#{key} is an invalid key", indent: 4 + indent
48
+ success = true
49
+ next
50
+ end
51
+
52
+ required_type = Object.const_get(permitted_keys[key]["type"])
53
+
54
+ if !value.is_a?(required_type)
55
+ bad "#{key} invalid type: #{value.class} (should be: #{required_type})", indent: 4 + indent
56
+ success = true
57
+ next
58
+ end
59
+
60
+ good "#{key} is a #{required_type} (#{value})", indent: 4 + indent
61
+ end
62
+
63
+ success
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Validate
6
+ module Region
7
+ include GClouder::Logging
8
+ include GClouder::Config::Project
9
+ include GClouder::Config::CLIArgs
10
+ extend Local
11
+
12
+ def self.instances(data, required_keys: {}, permitted_keys: {}, ignore_keys: [], skip_region: false, indent: 0)
13
+ data.each do |region, instances|
14
+ info region, indent: 2 + indent, heading: true unless skip_region
15
+ instances.each do |instance|
16
+ info instance["name"], indent: 3 + indent, heading: true
17
+
18
+ next if !has_unknown_keys?(instance, permitted_keys, ignore_keys, indent: indent) &&
19
+ has_required_keys?(instance, required_keys, ignore_keys, indent: 4 + indent)
20
+
21
+ fatal "\nerror: validation failure"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Validate
6
+ module Remote
7
+ include GClouder::Logging
8
+ include GClouder::Config::Project
9
+ include GClouder::Config::CLIArgs
10
+
11
+ def self.instances(local, remote, skip_keys: [])
12
+ remote.each do |region, resources|
13
+ info region, heading: true, indent: 2
14
+ resources.each do |resource|
15
+ #FIXME: This won't work with duplicate names
16
+ local_config = local.fetch(region, []).select {|s| s["name"] == resource["name"] }.first
17
+
18
+ failure = false
19
+
20
+ next unless local_config
21
+
22
+ info resource["name"], indent: 3, heading: true
23
+
24
+ local_config.each do |key, value|
25
+ skipped = false
26
+ skip_message = nil
27
+
28
+ # FIXME: we should recurse down into the data structure and check the values..
29
+ if value.is_a?(Hash) || value.is_a?(Array)
30
+ skip_message ||= "(can't validate complex object)"
31
+ skipped = true
32
+ end
33
+
34
+ if skip_keys.include?(key)
35
+ skip_message ||= "(skip_keys in resource definition)"
36
+ skipped = true
37
+ else
38
+ if !resource.key?(key)
39
+ bad "#{key} (missing key)", indent: 4
40
+
41
+ failure = true
42
+ next
43
+ end
44
+
45
+ if value != resource[key]
46
+ bad "#{key} (\"#{value.to_s.truncate(30)}\" != \"#{resource[key].to_s.truncate(30)}\")", indent: 4
47
+
48
+ failure = true
49
+ next
50
+ end
51
+ end
52
+
53
+ message = "#{key}" " (#{value.to_s.truncate(60)})"
54
+ message += " [skipped]" if skipped
55
+ message += " #{skip_message}" if skip_message
56
+
57
+ good message, indent: 4
58
+ end
59
+
60
+ next unless failure
61
+
62
+ info
63
+ info "local config:"
64
+ pp local_config.sort.to_h
65
+ info
66
+
67
+ info "remote config:"
68
+ pp resource.sort.to_h
69
+ info
70
+
71
+ fatal "error: immutable remote resource differs from local definition for resource: #{region}/#{resource["name"]}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ module Resources
5
+ module Local
6
+ def self.included(klass)
7
+ klass.extend Local
8
+ end
9
+
10
+ # FIXME: error if path doesnt exist..
11
+ def get_section(data, path, silent: false)
12
+ path.each do |key|
13
+ return [] unless data.key?(key)
14
+ data = data[key]
15
+ end
16
+
17
+ data
18
+ end
19
+ end
20
+
21
+ module Global
22
+ include GClouder::Logging
23
+ include GClouder::Config::Project
24
+ include GClouder::Config::CLIArgs
25
+ include Local
26
+
27
+ def self.instances(path: [])
28
+ data = get_section(project, path)
29
+
30
+ return {} if data.empty?
31
+
32
+ { "global" => data }
33
+ end
34
+ end
35
+
36
+ module Region
37
+ include GClouder::Logging
38
+ include GClouder::Helpers
39
+ include GClouder::Config::Project
40
+ include GClouder::Config::CLIArgs
41
+ include Local
42
+
43
+ def self.instances(path: [])
44
+ return {} unless project.key?("regions")
45
+
46
+ data = project["regions"].each_with_object({}) do |(region, region_config), instances|
47
+ instances[region] ||= []
48
+
49
+ data = get_section(region_config, path, silent: true)
50
+
51
+ data.each do |instance|
52
+ if GClouder::Config::Defaults.section?(path)
53
+ defaults = to_deep_merge_hash(GClouder::Config::Defaults.section(path))
54
+ instance = defaults.deep_merge(instance)
55
+ end
56
+
57
+ instances[region] << instance
58
+ end
59
+ end
60
+
61
+ data.delete_if { |_k, v| v.empty? }
62
+ end
63
+ end
64
+
65
+ # FIXME: should this be split out into a separate module that deals with remote state?
66
+ module Remote
67
+ def self.instances(path: [], ignore_keys: [], skip_instances: {}, args: nil)
68
+ resource_name = path.join(" ").to_sym
69
+
70
+ Resource.list(resource_name, args).each_with_object({}) do |resource, collection|
71
+ skip = false
72
+
73
+ skip_instances.each do |key, value|
74
+ next unless resource.key?(key)
75
+ if resource[key] =~ value
76
+ skip = true
77
+ break
78
+ end
79
+ end
80
+
81
+ next if skip
82
+
83
+ YAML.load_file("assets/mappings/file.yml")
84
+
85
+ # FIXME: this is so keys with partial matches work..
86
+ file_mapping_key = path.join("::")
87
+ file_mapping = YAML.load_file("assets/mappings/file.yml").fetch(file_mapping_key, nil)
88
+
89
+ resource_representation_path = file_mapping.nil? ? path : file_mapping
90
+
91
+ #ap GClouder::Config::ResourceRepresentations.properties
92
+
93
+ # contains list of non-output-only remote properties
94
+ resource_representation = GClouder::Config::ResourceRepresentations.section(resource_representation_path)
95
+
96
+ #ap resource_representation
97
+
98
+ # FIXME: partial key matches here are bad.. i.e: [compute, networks] matches [compute, networks, subnetworks]
99
+ # maps remote property names back to arguments
100
+ property_mappings_key = path.join("::")
101
+ property_mappings = YAML.load_file("assets/mappings/property.yml").fetch(property_mappings_key, [])
102
+
103
+ # Assume global, unless we can find or infer a region...
104
+ region = "global"
105
+ region = resource["region"] if resource.key?("region")
106
+ zone = resource["zone"] if !resource.key?("region") && resource.key?("zone")
107
+
108
+ if resource.key?("selfLink") && resource["selfLink"].match(/zones\//) && !zone
109
+ zone = resource["selfLink"].match(/zones\/([^\/]+)/)[1]
110
+ end
111
+
112
+ if zone
113
+ resource["zone"] = zone
114
+ region = zone.sub(/-[a-z]$/, "")
115
+ end
116
+
117
+ # 1: convert key names to snake_case
118
+ resource = resource.to_snake_keys
119
+
120
+ # 2: delete any keys not in resource_representations (because they're output only)
121
+ # FIXME: warn about deleting keys?
122
+ resource.delete_if do |key, _value|
123
+ resource_representation[key] == "OutputOnly" && key != "name"
124
+ end
125
+
126
+ # 3: convert the names of any keys using the mappings file
127
+ property_mappings.each do |argument, resource_representation|
128
+ # FIXME: don't overwrite arguments..
129
+ resource[argument] = resource.dig(*resource_representation)
130
+ resource.delete(resource_representation)
131
+ end
132
+
133
+ ignore_keys.each do |key|
134
+ next unless resource.key?(key)
135
+ resource.delete(key)
136
+ end
137
+
138
+ # ?: if there are any keys *not* in the resource_representations file then we have a problem
139
+
140
+ # ?: if there are any keys which dont match an argument, then we have a problem
141
+
142
+ collection[region] ||= []
143
+ collection[region] << resource
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "open3"
4
+
5
+ module GClouder
6
+ module Shell
7
+ include GClouder::Logging
8
+
9
+ def self.included(klass)
10
+ klass.extend Shell
11
+ end
12
+
13
+ def shell(command, failure: true, silent: false)
14
+ stdout, stderr, status, failed = run(command)
15
+
16
+ if (GClouder.cli_args[:debug] || failed) && !silent
17
+ header(command)
18
+ dump_fds(stdout, stderr)
19
+ end
20
+
21
+ if !failed && silent
22
+ return true
23
+ end
24
+
25
+ if failed && !failure
26
+ return false
27
+ end
28
+
29
+ if failed && silent
30
+ return false
31
+ end
32
+
33
+ if failed && !silent
34
+ footer(status)
35
+ end
36
+
37
+ if silent
38
+ return
39
+ end
40
+
41
+ stdout
42
+ end
43
+
44
+ private
45
+
46
+ def header(command)
47
+ info
48
+ info "# #{command}"
49
+ end
50
+
51
+ def dump_fds(stdout, stderr)
52
+ dump(stdout)
53
+ dump(stderr)
54
+ end
55
+
56
+ def dump(fd)
57
+ return if fd.empty?
58
+ info fd
59
+ end
60
+
61
+ def footer(status)
62
+ fatal "there was an error running the previous shell command which exited with non-0: #{status}"
63
+ end
64
+
65
+ def run(command)
66
+ stdout, stderr, status = Open3.capture3(command)
67
+ failed = status.to_i > 0
68
+ [stdout, stderr, status, failed]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module GClouder
4
+ VERSION = "0.1.1"
5
+ end