swarmkit-client 0.1.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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc'
4
+ require 'google/protobuf/descriptor_pb'
5
+
6
+ module Swarmkit
7
+ module API
8
+ REVISION = '17b8d222e7dd'
9
+ PACKAGE = 'docker.swarmkit.v1'
10
+ POOL = Google::Protobuf::DescriptorPool.new
11
+ SCHEMA = Google::Protobuf::FileDescriptorSet.decode(File.binread(File.join(__dir__, 'schema.pb')))
12
+
13
+ SCHEMA.file.each { |file| POOL.add_serialized_file(file.to_proto) }
14
+
15
+ # reason: the pinned descriptors define every type and RPC without duplicated Ruby bindings.
16
+ def self.define_types(parent, prefix, messages, enums)
17
+ enums.each do |type|
18
+ parent.const_set(type.name, POOL.lookup("#{prefix}.#{type.name}").enummodule)
19
+ end
20
+ messages.each do |type|
21
+ next if type.options&.map_entry
22
+
23
+ name = "#{prefix}.#{type.name}"
24
+ klass = parent.const_set(type.name, POOL.lookup(name).msgclass)
25
+ define_types(klass, name, type.nested_type, type.enum_type)
26
+ end
27
+ end
28
+ private_class_method :define_types
29
+
30
+ SCHEMA.file.select { |file| file.package == PACKAGE }.each do |file|
31
+ define_types(self, PACKAGE, file.message_type, file.enum_type)
32
+ end
33
+
34
+ SERVICES = SCHEMA.file.flat_map(&:service).to_h do |definition|
35
+ service = const_set(definition.name, Class.new { include GRPC::GenericService })
36
+ service.service_name = "#{PACKAGE}.#{definition.name}"
37
+ service.marshal_class_method = :encode
38
+ service.unmarshal_class_method = :decode
39
+ POOL.lookup(service.service_name).each do |method|
40
+ input = method.input_type.msgclass
41
+ output = method.output_type.msgclass
42
+ input = service.stream(input) if method.client_streaming
43
+ output = service.stream(output) if method.server_streaming
44
+ service.rpc(method.name.to_sym, input, output)
45
+ end
46
+ service.const_set(:Stub, service.rpc_stub_class)
47
+ [GRPC::GenericService.underscore(definition.name).to_sym, service]
48
+ end.freeze
49
+ private_constant :SCHEMA
50
+ end
51
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require_relative '../swarmkit-client'
6
+
7
+ module Swarmkit
8
+ class CLI
9
+ class UsageError < StandardError; end
10
+
11
+ SENSITIVE = /auth|password|credential|token|(?:^|_)keys?$|
12
+ \A(?:data|payload|env|command|args|labels|options|headers|test|value|message|err)\z/ix
13
+
14
+ def initialize(input: $stdin, output: $stdout, error: $stderr)
15
+ @input = input
16
+ @output = output
17
+ @error = error
18
+ end
19
+
20
+ def run(argv)
21
+ options = {}
22
+ parser = OptionParser.new do |opts|
23
+ opts.banner = <<~HELP
24
+ Usage: swarmkit-client [options] SERVICE METHOD [JSON|@FILE|-]
25
+ swarmkit-client methods [SERVICE]
26
+ swarmkit-client describe SERVICE METHOD
27
+ JSON defaults to {}. Client streams take a JSON array; responses use JSON lines.
28
+ Sensitive fields are redacted. Explicit RPC calls can change cluster state.
29
+ HELP
30
+ %i[target ca cert key server_name].each do |name|
31
+ opts.on("--#{name.to_s.tr('_', '-')} VALUE") { |value| options[name] = value }
32
+ end
33
+ opts.on('--timeout SECONDS', Float, 'Deadline duration (default: 10)') { |value| options[:timeout] = value }
34
+ opts.on('-v', '--version') do
35
+ @output.puts VERSION
36
+ return 0
37
+ end
38
+ opts.on('-h', '--help') do
39
+ @output.puts opts
40
+ return 0
41
+ end
42
+ end
43
+ args = parser.parse(argv)
44
+ case args.first
45
+ when nil then @output.puts parser
46
+ when 'methods' then list_methods(args.drop(1))
47
+ when 'describe'
48
+ raise UsageError, 'describe requires SERVICE METHOD' unless args.size == 3
49
+
50
+ _, _, method = resolve(*args.drop(1))
51
+ describe(method)
52
+ else
53
+ raise UsageError, 'expected SERVICE METHOD [JSON|@FILE|-]' unless (2..3).cover?(args.size)
54
+
55
+ service, name, method = resolve(*args.first(2))
56
+ request = request(method, args[2] || '{}')
57
+ call(service, name, method, request, options)
58
+ end
59
+ 0
60
+ rescue UsageError, OptionParser::ParseError => e
61
+ @error.puts "error: #{e.message}"
62
+ 2
63
+ rescue JSON::ParserError, Google::Protobuf::ParseError, ArgumentError, TypeError
64
+ @error.puts 'error: invalid JSON request or connection options; use --help and describe SERVICE METHOD'
65
+ 2
66
+ rescue GRPC::BadStatus => e
67
+ @error.puts "error: RPC failed (gRPC status #{e.code})"
68
+ 1
69
+ rescue Errno::EPIPE
70
+ 0
71
+ rescue SystemCallError, IOError => e
72
+ @error.puts "error: I/O failed (#{e.class})"
73
+ 1
74
+ rescue Interrupt
75
+ 130
76
+ end
77
+
78
+ private
79
+
80
+ def list_methods(args)
81
+ raise UsageError, 'methods accepts at most one SERVICE' if args.size > 1
82
+
83
+ services = args.empty? ? API::SERVICES : {args.first.to_sym => service(args.first)}
84
+ services.each do |name, definition|
85
+ definition.rpc_descs.each_key { |rpc| @output.puts "#{name} #{GRPC::GenericService.underscore(rpc.to_s)}" }
86
+ end
87
+ end
88
+
89
+ def service(name)
90
+ API::SERVICES.fetch(name.to_sym) { raise UsageError, "unknown service; use 'methods'" }
91
+ end
92
+
93
+ def resolve(service_name, method_name)
94
+ method = API::POOL.lookup(service(service_name).service_name).find do |rpc|
95
+ GRPC::GenericService.underscore(rpc.name) == method_name
96
+ end
97
+ raise UsageError, "unknown RPC; use 'methods SERVICE'" unless method
98
+
99
+ [service_name, method_name, method]
100
+ end
101
+
102
+ def describe(method)
103
+ types = {}
104
+ queue = [method.input_type, method.output_type]
105
+ until queue.empty?
106
+ type = queue.shift
107
+ next if types.key?(type.name)
108
+
109
+ types[type.name] = type.to_proto.to_h
110
+ queue.concat(type.filter_map(&:subtype)) if type.is_a?(Google::Protobuf::Descriptor)
111
+ end
112
+ @output.puts JSON.pretty_generate(method: method.to_proto.to_h, types: types)
113
+ end
114
+
115
+ def request(method, source)
116
+ json = if source == '-'
117
+ @input.read
118
+ elsif source.start_with?('@')
119
+ File.read(source.delete_prefix('@'))
120
+ else
121
+ source
122
+ end
123
+ type = method.input_type.msgclass
124
+ return type.decode_json(json) unless method.client_streaming
125
+
126
+ items = JSON.parse(json)
127
+ raise UsageError, 'client-streaming RPCs require a JSON array' unless items.is_a?(Array)
128
+
129
+ items.map { |item| type.decode_json(JSON.generate(item)) }.each
130
+ end
131
+
132
+ def call(service, name, method, request, options)
133
+ Client.open(**options) do |client|
134
+ operation = client.public_send(service).public_send(name, request, return_op: true)
135
+ begin
136
+ response = operation.execute
137
+ method.server_streaming ? response.each { |message| emit(message) } : emit(response)
138
+ ensure
139
+ operation.cancel
140
+ end
141
+ end
142
+ end
143
+
144
+ def emit(message)
145
+ data = JSON.parse(message.class.encode_json(message, preserve_proto_fieldnames: true))
146
+ @output.puts JSON.generate(redact(data))
147
+ @output.flush
148
+ end
149
+
150
+ def redact(value)
151
+ case value
152
+ when Hash
153
+ value.to_h { |key, item| [key, key.match?(SENSITIVE) ? '[REDACTED]' : redact(item)] }
154
+ when Array then value.map { |item| redact(item) }
155
+ else value
156
+ end
157
+ end
158
+ end
159
+ end
Binary file
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'swarmkit-client/api'
5
+
6
+ module Swarmkit
7
+ class Client
8
+ SOCKET = '/var/run/docker/swarm/control.sock'
9
+ CA = '/var/lib/docker/swarm/certificates/swarm-root-ca.crt'
10
+
11
+ def self.open(**)
12
+ client = new(**)
13
+ yield client
14
+ ensure
15
+ client&.close
16
+ end
17
+
18
+ def initialize(target: "unix:#{SOCKET}", ca: CA, cert: nil, key: nil,
19
+ server_name: 'swarm-manager', timeout: 10, channel_args: {})
20
+ raise ArgumentError, 'cert and key must be supplied together' if cert.nil? != key.nil?
21
+
22
+ unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
23
+ raise ArgumentError,
24
+ 'timeout must be finite and positive'
25
+ end
26
+
27
+ credentials = GRPC::Core::ChannelCredentials.new(
28
+ File.read(ca), key && File.read(key), cert && File.read(cert)
29
+ )
30
+ args = {'grpc.max_receive_message_length' => 16 * 1024 * 1024}.merge(channel_args)
31
+ if server_name
32
+ args['grpc.ssl_target_name_override'] = server_name
33
+ args['grpc.default_authority'] = server_name
34
+ end
35
+ @channel = GRPC::Core::Channel.new(target, args, credentials)
36
+ @stubs = API::SERVICES.transform_values do |service|
37
+ service::Stub.new(target, credentials, channel_override: @channel, timeout: timeout)
38
+ end
39
+ end
40
+
41
+ # reason: all 11 services share one channel and the same accessor behavior.
42
+ API::SERVICES.each_key do |name|
43
+ define_method(name) { @stubs.fetch(name) }
44
+ end
45
+
46
+ def close
47
+ @channel.close
48
+ end
49
+ end
50
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Swarmkit
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,58 @@
1
+ {
2
+ "gogoproto/gogo.proto": {
3
+ "url": "https://raw.githubusercontent.com/gogo/protobuf/v1.3.2/gogoproto/gogo.proto",
4
+ "sha256": "f2f77edf7de807ded7884813d851656f4ccb18262db717a8f31061995f3e7324"
5
+ },
6
+ "github.com/docker/swarmkit/api/types.proto": {
7
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/types.proto",
8
+ "sha256": "f0a9f0dad6a87380feb7ba6715887250d4df64fc74a9ca5b9d7c14775dc81240"
9
+ },
10
+ "github.com/docker/swarmkit/api/specs.proto": {
11
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/specs.proto",
12
+ "sha256": "f25c844176ba6874af81b3b182721b96a107129319825ed796ed812208be082d"
13
+ },
14
+ "github.com/docker/swarmkit/protobuf/plugin/plugin.proto": {
15
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/protobuf/plugin/plugin.proto",
16
+ "sha256": "e1e65b489db311503b9fdc5e1f984b77660d1b7a3018ae92c0bbb124a86f4021"
17
+ },
18
+ "github.com/docker/swarmkit/api/objects.proto": {
19
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/objects.proto",
20
+ "sha256": "dd76a9c58c6af0a4d81dcababd569b344b3926b069fc67a7225e2f85474110e9"
21
+ },
22
+ "github.com/docker/swarmkit/api/control.proto": {
23
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/control.proto",
24
+ "sha256": "cd4ba9a8902eb1e468a0a6e718a952844e712d7db5a7f81dcedeaa889586778a"
25
+ },
26
+ "github.com/docker/swarmkit/api/watch.proto": {
27
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/watch.proto",
28
+ "sha256": "83eb87a44ca3a9d93977430c32c719422f92ce3a1bc7dd458175c567543752f7"
29
+ },
30
+ "github.com/docker/swarmkit/api/logbroker.proto": {
31
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/logbroker.proto",
32
+ "sha256": "8894f2e4f0c97bb082454dfccbf01e0cf26951a1f770aee163d28f64f2da5cd9"
33
+ },
34
+ "github.com/docker/swarmkit/api/dispatcher.proto": {
35
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/dispatcher.proto",
36
+ "sha256": "c93c7f3ace11a3c946bd2ddcbe34966932c05e5160b3b91051a8caeb268bc968"
37
+ },
38
+ "github.com/docker/swarmkit/api/ca.proto": {
39
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/ca.proto",
40
+ "sha256": "2ea6cf7bcb89d792fe5b4a98c8b1c7364de13cb149aefbb56c820043c2bc3c9d"
41
+ },
42
+ "go.etcd.io/raft/v3/raftpb/raft.proto": {
43
+ "url": "https://raw.githubusercontent.com/etcd-io/raft/v3.6.0/raftpb/raft.proto",
44
+ "sha256": "312ce6c6ff1dd797e17edfa8dbc7133f965d5fc5423a76e6aaff7b09ba8be19f"
45
+ },
46
+ "github.com/docker/swarmkit/api/raft.proto": {
47
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/raft.proto",
48
+ "sha256": "ad575667ea693066de0557cdff76e5bde4daaec35e8041ff4e3c5c5da066e6e4"
49
+ },
50
+ "github.com/docker/swarmkit/api/resource.proto": {
51
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/resource.proto",
52
+ "sha256": "c309a506da325a7c5801993d7e6a28a7597a2f4667eec4ee41aea87efd036229"
53
+ },
54
+ "github.com/docker/swarmkit/api/health.proto": {
55
+ "url": "https://raw.githubusercontent.com/moby/swarmkit/17b8d222e7dd/api/health.proto",
56
+ "sha256": "920a69b4b3296ceb46081982b98cb7b4bd0d79f3c6ccba4536f5320dbae59df8"
57
+ }
58
+ }
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: swarmkit-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Artyom B
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: google-protobuf
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.36'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.36'
26
+ - !ruby/object:Gem::Dependency
27
+ name: grpc
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.85'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.85'
40
+ description: Typed requests, responses and streams for all SwarmKit services over
41
+ verified TLS.
42
+ executables:
43
+ - swarmkit-client
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE
48
+ - NOTICE
49
+ - README.md
50
+ - bin/swarmkit-client
51
+ - docs/api.md
52
+ - lib/swarmkit-client.rb
53
+ - lib/swarmkit-client/api.rb
54
+ - lib/swarmkit-client/cli.rb
55
+ - lib/swarmkit-client/schema.pb
56
+ - lib/version.rb
57
+ - schema/sources.json
58
+ homepage: https://github.com/artyomb/swarmkit-client
59
+ licenses:
60
+ - Apache-2.0
61
+ metadata:
62
+ source_code_uri: https://github.com/artyomb/swarmkit-client
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '3.2'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.6.7
78
+ specification_version: 4
79
+ summary: Compact Ruby client for the complete SwarmKit gRPC API
80
+ test_files: []