gclouder 0.1.1

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