jcf 0.0.9

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +43 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Guardfile +21 -0
  7. data/LICENSE.txt +20 -0
  8. data/README.md +120 -0
  9. data/Rakefile +167 -0
  10. data/cucumber.yml +1 -0
  11. data/exe/jcf +21 -0
  12. data/lib/.DS_Store +0 -0
  13. data/lib/jcf/.DS_Store +0 -0
  14. data/lib/jcf/aws/cloudwatch.rb +90 -0
  15. data/lib/jcf/cf/.DS_Store +0 -0
  16. data/lib/jcf/cf/base.rb +113 -0
  17. data/lib/jcf/cf/metric.rb +39 -0
  18. data/lib/jcf/cf/organization.rb +10 -0
  19. data/lib/jcf/cf/quota.rb +11 -0
  20. data/lib/jcf/cf/relationships.rb +88 -0
  21. data/lib/jcf/cf/service_broker.rb +10 -0
  22. data/lib/jcf/cf/service_instance.rb +10 -0
  23. data/lib/jcf/cf/service_offering.rb +10 -0
  24. data/lib/jcf/cf/service_plan.rb +10 -0
  25. data/lib/jcf/cf/space.rb +10 -0
  26. data/lib/jcf/cf/user.rb +37 -0
  27. data/lib/jcf/cf.rb +29 -0
  28. data/lib/jcf/cli/command.rb +35 -0
  29. data/lib/jcf/cli/commands/.DS_Store +0 -0
  30. data/lib/jcf/cli/commands/cf/metrics.rb +156 -0
  31. data/lib/jcf/cli/commands/cf/organizations.rb +24 -0
  32. data/lib/jcf/cli/commands/cf/quota.rb +26 -0
  33. data/lib/jcf/cli/commands/cf/service_brokers.rb +30 -0
  34. data/lib/jcf/cli/commands/cf/service_instances.rb +37 -0
  35. data/lib/jcf/cli/commands/cf/service_offerings.rb +26 -0
  36. data/lib/jcf/cli/commands/cf/service_plans.rb +30 -0
  37. data/lib/jcf/cli/commands/cf/spaces.rb +26 -0
  38. data/lib/jcf/cli/commands/cf/users.rb +24 -0
  39. data/lib/jcf/cli/commands/version.rb +16 -0
  40. data/lib/jcf/cli/commands.rb +21 -0
  41. data/lib/jcf/cli/errors.rb +17 -0
  42. data/lib/jcf/cli/output_formatters/csv.rb +35 -0
  43. data/lib/jcf/cli/output_formatters/json.rb +19 -0
  44. data/lib/jcf/cli/output_formatters/text.rb +39 -0
  45. data/lib/jcf/cli/output_formatters.rb +26 -0
  46. data/lib/jcf/cli.rb +53 -0
  47. data/lib/jcf/version.rb +5 -0
  48. data/lib/jcf.rb +9 -0
  49. data/sig/jcf.rbs +4 -0
  50. metadata +327 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "English"
5
+ require "active_model"
6
+ require "active_model/serializers/json"
7
+ require "active_support/core_ext/hash"
8
+ require "active_support/core_ext/string"
9
+ require "active_support/core_ext/object"
10
+
11
+ module JCF
12
+ module CF
13
+ class Base
14
+ include ActiveModel::Model
15
+ include ActiveModel::Serialization
16
+ include ActiveModel::Validations
17
+
18
+ attr_accessor :name, :guid, :relationships, :raw
19
+
20
+ validates_presence_of :name, :guid
21
+
22
+ def attributes
23
+ { name: name, guid: guid, relationships: relationships }
24
+ end
25
+
26
+ def initialize(name: nil, guid: nil, relationships: nil)
27
+ @name = name
28
+ @guid = guid
29
+ @relationships = Relationships.new(self, relationships)
30
+ end
31
+
32
+ class << self
33
+ attr_accessor :endpoint
34
+
35
+ def find_by(attrs)
36
+ objects = all
37
+ objects.find_all do |obj|
38
+ # only find by attributes we have and our object has
39
+ keys = obj.attributes.keys & attrs.keys
40
+ keys.all? do |key|
41
+ obj.attributes[key].include? attrs[key]
42
+ end
43
+ end
44
+ end
45
+
46
+ def first(attrs)
47
+ find_by(attrs).first
48
+ end
49
+
50
+ def find(guid)
51
+ new(guid: guid).populate!
52
+ end
53
+
54
+ def all(params = {})
55
+ params.compact!
56
+
57
+ resources(params: params)
58
+ end
59
+
60
+ def resource_url
61
+ endpoint || name.demodulize.tableize
62
+ end
63
+
64
+ private
65
+
66
+ def resources(params: {})
67
+ params.compact!
68
+
69
+ hash = JCF::CF.curl(resource_url, params: params)
70
+ populate_objects(hash)
71
+ end
72
+
73
+ def populate_objects(hash)
74
+ hash[:resources].map! do |object|
75
+ o = new(
76
+ guid: object[:guid],
77
+ name: object[:name],
78
+ relationships: object[:relationships]
79
+ )
80
+ o
81
+ end
82
+ end
83
+ end
84
+
85
+ def to_s
86
+ "#{name} #{guid}"
87
+ end
88
+
89
+ def populate!
90
+ resource(guid)
91
+ end
92
+
93
+ private
94
+
95
+ def resource(guid)
96
+ object = self.class.all.find { |obj| obj.guid == guid }
97
+ return object if object
98
+
99
+ hash = JCF::CF.curl("#{self.class.resource_url}/#{guid}", params: nil)
100
+ parse_object(hash)
101
+ end
102
+
103
+ def parse_object(hash)
104
+ hash[:resources].first if hash[:resources].is_a?(Array)
105
+
106
+ hash[:guid]
107
+ hash[:name]
108
+ (hash[:relationships] || {})
109
+ self
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Metric < Base
8
+ attr_accessor :name, :region, :deploy_env, :organization, :organization_guid, :space, :space_guid,
9
+ :service_broker_name, :service_broker_guid, :service_offering, :service_plan,
10
+ :storage_allocated, :storage_used, :storage_free, :iops, :cpu
11
+
12
+ # rubocop:disable Metrics/MethodLength
13
+ def attributes
14
+ {
15
+ name: name,
16
+ region: region,
17
+ organization: organization,
18
+ organization_guid: organization_guid,
19
+ space: space,
20
+ space_guid: space_guid,
21
+ service_broker_name: service_broker_name,
22
+ service_broker_guid: service_broker_guid,
23
+ service_offering: service_offering,
24
+ service_plan: service_plan,
25
+ storage_used: storage_used,
26
+ storage_allocated: storage_allocated,
27
+ storage_free: storage_free,
28
+ iops: iops,
29
+ cpu: cpu
30
+ }
31
+ end
32
+ # rubocop:enable Metrics/MethodLength
33
+
34
+ def to_s
35
+ "#{name}: used(#{storage_used}) allocated(#{storage_allocated}) free(#{storage_free}) iops(#{iops}) cpu(#{cpu})"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Organization < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Quota < Base
8
+ self.endpoint = "organization_quotas"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Relationships
8
+ include Enumerable
9
+
10
+ attr_reader :belongs_to
11
+
12
+ def initialize(belongs_to, relationships = {})
13
+ relationships = {} if relationships.nil?
14
+ raise ArgumentError, "expects a hash" unless relationships.is_a?(Hash)
15
+
16
+ @belongs_to = belongs_to
17
+ @relationships = convert_to_classes(relationships)
18
+ end
19
+
20
+ def each(&block)
21
+ @relationships&.each(&block)
22
+ end
23
+
24
+ def inspect
25
+ @relationships.inspect
26
+ end
27
+
28
+ def relationship?(method_name)
29
+ klass = get_klass(method_name)
30
+
31
+ matches = @relationships.filter_map do |relationship|
32
+ relationship.instance_of?(klass) ? relationship : nil
33
+ end.compact
34
+
35
+ matches.any?
36
+ rescue NameError
37
+ false
38
+ end
39
+
40
+ def respond_to_missing?(method_name, include_private = false)
41
+ relationship?(method_name) || super
42
+ end
43
+
44
+ def method_missing(method_name, *args, &block)
45
+ if relationship?(method_name)
46
+ klass = get_klass(method_name)
47
+
48
+ matches = @relationships.filter_map do |relationship|
49
+ relationship.instance_of?(klass) ? relationship : nil
50
+ end
51
+
52
+ matches.size == 1 ? matches.first : matches
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def to_s
59
+ @relationships.collect do |relationship|
60
+ "#{relationship.class.name.demodulize}: #{relationship.name || ""} (#{relationship.guid})"
61
+ end.join(", ")
62
+ end
63
+
64
+ private
65
+
66
+ def convert_to_classes(relationships = {})
67
+ return {} if relationships.nil?
68
+
69
+ relationships.collect do |relationship_type, data_hash|
70
+ klass = get_klass(relationship_type)
71
+ data = make_array(data_hash[:data])
72
+
73
+ data.collect do |d|
74
+ klass.new(guid: (d || {})[:guid])
75
+ end.flatten
76
+ end.flatten
77
+ end
78
+
79
+ def get_klass(method)
80
+ "JCF::CF::#{method.to_s.singularize.camelize}".constantize
81
+ end
82
+
83
+ def make_array(data)
84
+ data.is_a?(Array) ? data : [data]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class ServiceBroker < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class ServiceInstance < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class ServiceOffering < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class ServicePlan < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class Space < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module JCF
6
+ module CF
7
+ class User < Base
8
+ attr_accessor :username, :guid, :presentation_name, :raw
9
+
10
+ validates_presence_of :username, :guid, :presentation_name
11
+
12
+ def attributes
13
+ { username: username, guid: guid, presentation_name: presentation_name }
14
+ end
15
+
16
+ def initialize(username: nil, guid: nil, presentation_name: nil)
17
+ super()
18
+ @username = username
19
+ @guid = guid
20
+ @presentation_name = presentation_name
21
+ end
22
+
23
+ def self.populate_objects(hash)
24
+ hash[:resources].map! do |object|
25
+ o = new(
26
+ guid: object[:guid],
27
+ username: object[:username],
28
+ presentation_name: object[:presentation_name]
29
+ )
30
+ o
31
+ end
32
+ rescue NoMethodError
33
+ puts "object is #{hash[:resources]}"
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/jcf/cf.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JCF
4
+ module CF
5
+ def self.curl(endpoint, params: {})
6
+ params[:per_page] ||= 5000 if params
7
+ url = sanitize_url(endpoint, params)
8
+
9
+ response = do_curl(url)
10
+
11
+ raise(JCF::CLI::NotLoggedInError, response) if response == "\n" || response.include?("FAILED")
12
+
13
+ JSON.parse(response).deep_symbolize_keys
14
+ end
15
+
16
+ def self.sanitize_url(endpoint, params)
17
+ url = endpoint.dup
18
+ url << "?#{params.to_query}" if params && params != {}
19
+ url
20
+ end
21
+
22
+ def self.do_curl(url)
23
+ JCF.cache.get_or_set("cf curl \"/v3/#{url}\"".parameterize) do
24
+ puts "cf curl \"/v3/#{url}\"" if ENV["DEBUG"]
25
+ `cf curl \"/v3/#{url}\"`
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "active_support/core_ext/string/inflections"
5
+
6
+ module JCF
7
+ module CLI
8
+ class Command < Dry::CLI::Command
9
+ module Output
10
+ attr_reader :out, :err, :formatter
11
+
12
+ def call(*args, **opts)
13
+ @out = opts[:output] ? File.new(opts[:output], "w") : $stdout
14
+ @err = $stderr
15
+ output = opts[:format]
16
+ @output_file = opts[:output]
17
+ @formatter = OutputFormatters.formatter(output)
18
+
19
+ super(*args, **opts)
20
+ ensure
21
+ @out.close if @out && @out != $stdout
22
+ end
23
+ end
24
+
25
+ # mix in the global options
26
+ def self.inherited(klass)
27
+ super
28
+ klass.option :format, aliases: ["--format"], default: "text", values: %w[text json csv], desc: "Output format"
29
+ klass.option :output, aliases: ["--output"], desc: "Output file"
30
+
31
+ klass.prepend(Output)
32
+ end
33
+ end
34
+ end
35
+ end
Binary file
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: THIS IS A PROTOTYPE, REDO THIS INTO SOMETHING LESS SCRIPTY
4
+
5
+ require "shellwords"
6
+ require_relative "../../errors"
7
+ require "active_support"
8
+ require "active_support/core_ext/numeric/conversions"
9
+ require "filesize"
10
+ require "concurrent"
11
+
12
+ module JCF
13
+ module CLI
14
+ module Commands
15
+ module CF
16
+ class Metrics < Command
17
+ argument :env, required: true, values: %w[dev01 dev02 dev03 dev04 dev05 staging prod prod-lon],
18
+ desc: "Choose an environment"
19
+ argument :type, required: true, values: %w[postgres aws-s3-bucket],
20
+ desc: "Choose a service instance type to query", default: "postgres"
21
+
22
+ option :org, aliases: ["-o"], required: true, type: :string,
23
+ desc: "Choose an organization (can be multiple comma-separated)"
24
+ option :name, aliases: ["-n"], type: :string, desc: "Choose a service instance name"
25
+
26
+ def call(*_args, **options)
27
+ validate_options(options)
28
+ orgs = options[:org].include?(",") ? options[:org].split(",") : [options[:org]]
29
+
30
+ orgs.each do |org|
31
+ org_guid = organizations.find { |o| o.name == org }.guid
32
+ err.puts "Found org guid: #{org_guid}"
33
+
34
+ offering_guid = service_offerings.find { |s| s.name == options[:type] }&.guid
35
+ err.puts "Found offering guid: #{offering_guid}"
36
+
37
+ unless offering_guid
38
+ err.puts "No offerings found for type #{options[:type]}"
39
+ exit(1)
40
+ end
41
+
42
+ # find the plans for those gatherings
43
+ plan_guids = service_plans.find_all do |plan|
44
+ plan.relationships.service_offering.guid == offering_guid
45
+ end.collect(&:guid)
46
+ err.puts "Found plan guids: #{plan_guids.count}"
47
+
48
+ # look up the instances that match the plans and org
49
+ # "/v3/service_instances?organization_guids=${org_guids}&service_plan_guids=${plan_guids}"
50
+ instances = JCF::CF::ServiceInstance.all(
51
+ organization_guids: org_guid,
52
+ service_plan_guids: plan_guids.join(",")
53
+ )
54
+ instances.select! { |i| i.name.include? options[:name] } if options[:name]
55
+ err.puts "Found instances: #{instances.count}"
56
+
57
+ cw = JCF::AWS::CloudWatch.new
58
+ values = []
59
+
60
+ Thread.abort_on_exception = true
61
+ # use a the number of processors as the number of threads
62
+ instances.each_slice(Concurrent.processor_count) do |slice|
63
+ slice.collect do |instance|
64
+ service_plan = instance.relationships.service_plan.populate!
65
+ service_offering = service_plan.relationships.service_offering.populate!
66
+ service_broker = service_offering.relationships.service_broker.populate!
67
+ # puts "Getting metrics for #{instance.name}"
68
+ # puts "service_plan: #{service_plan.name}"
69
+ # puts "service_offering: #{service_offering.name}"
70
+ # puts "service_broker: #{service_broker.name}"
71
+ # return nil
72
+
73
+ Thread.new do
74
+ m = JCF::CF::Metric.new
75
+ m.name = (instance.name || "")
76
+ err.puts "Getting metrics for #{m.name}"
77
+ m.region = ENV["AWS_REGION"]
78
+ m.deploy_env = options[:env]
79
+ m.organization = org
80
+ m.organization_guid = org_guid
81
+ space = spaces.find { |s| s.guid == instance.relationships.space.guid }
82
+ m.space = space.name
83
+ m.space_guid = space.guid
84
+ m.service_broker_name = service_broker.name
85
+ m.service_broker_guid = service_broker.guid
86
+ m.service_offering = service_offering.name
87
+ m.service_plan = service_plan.name
88
+ if service_offering.name == "postgres"
89
+ m.storage_used = to_gb(cw.rds_storage_used(name: rds_guid(instance.guid)) || "")
90
+ m.storage_allocated = to_gb(cw.storage_allocated(name: rds_guid(instance.guid)) || "")
91
+ m.storage_free = to_gb(cw.storage_free(name: rds_guid(instance.guid)) || "")
92
+ m.iops = (cw.iops(name: rds_guid(instance.guid)).to_fs(:rounded, precision: 0) || "")
93
+ m.cpu = (cw.cpu(name: rds_guid(instance.guid)).to_fs(:rounded, precision: 0) || "")
94
+ end
95
+ if service_offering.name == "aws-s3-bucket"
96
+ m.storage_used = to_gb(cw.s3_storage_used(name: s3_guid(options[:env], instance.guid)) || "")
97
+ end
98
+ values << m
99
+ end
100
+ end.map(&:value)
101
+ end
102
+
103
+ values << JCF::CF::Metric.new if values.empty?
104
+ out.puts formatter.format(values)
105
+ err.puts "Done."
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def rds_guid(guid)
112
+ "rdsbroker-#{guid}"
113
+ end
114
+
115
+ def s3_guid(deploy_env, guid)
116
+ "paas-s3-broker-#{deploy_env}-#{guid}"
117
+ end
118
+
119
+ def validate_options(options)
120
+ raise JCF::CLI::InvalidOptionError, "No organization given" unless options[:org]
121
+ end
122
+
123
+ def organizations
124
+ @organizations ||= JCF::CF::Organization.all
125
+ end
126
+
127
+ def spaces
128
+ @spaces ||= JCF::CF::Space.all
129
+ end
130
+
131
+ def service_instances
132
+ @service_instances ||= JCF::CF::ServiceInstance.all
133
+ end
134
+
135
+ def service_plans
136
+ @service_plans ||= JCF::CF::ServicePlan.all
137
+ end
138
+
139
+ def service_offerings
140
+ @service_offerings ||= JCF::CF::ServiceOffering.all
141
+ end
142
+
143
+ def service_brokers
144
+ JCF::CF::ServiceBroker.all
145
+ end
146
+
147
+ def to_gb(bytes)
148
+ Filesize.from("#{bytes} b").to("GB").to_fs(:rounded, precision: 2)
149
+ rescue ArgumentError
150
+ 0
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../../errors"
5
+
6
+ module JCF
7
+ module CLI
8
+ module Commands
9
+ module CF
10
+ class Organizations < Command
11
+ argument :name, required: false, desc: "Organization name"
12
+
13
+ def call(name: nil, **)
14
+ if name
15
+ out.puts formatter.format(JCF::CF::Organization.find_by(name: name))
16
+ else
17
+ out.puts formatter.format(JCF::CF::Organization.all)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../../errors"
5
+
6
+ module JCF
7
+ module CLI
8
+ module Commands
9
+ module CF
10
+ class Quota < Command
11
+ argument :name, required: false, desc: "Quota name"
12
+
13
+ option :org, aliases: ["-o", "--org", "--organization"], type: :string, desc: "Filter to an organization"
14
+
15
+ def call(name: nil, **options)
16
+ if name
17
+ out.puts formatter.format(JCF::CF::Quota.find_by(name: name))
18
+ else
19
+ out.puts formatter.format(JCF::CF::Quota.all(org: options[:org]))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../../errors"
5
+
6
+ module JCF
7
+ module CLI
8
+ module Commands
9
+ module CF
10
+ class ServiceBrokers < Command
11
+ argument :name, required: false, desc: "Service Broker name"
12
+
13
+ option :space, aliases: ["-s", "--space"], type: :string, desc: "Filter to a space"
14
+
15
+ def call(name: nil, **options)
16
+ if name
17
+ out.puts formatter.format(JCF::CF::ServiceBroker.find_by(name: name))
18
+ else
19
+ out.puts formatter.format(
20
+ JCF::CF::ServiceBroker.all(
21
+ space_guids: options[:space]
22
+ )
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end