wash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0c229c4c749d26525382c4c26255044308b763335b4d2d6d2ea8aa3495fb9b9
4
+ data.tar.gz: f60ba8213ec625de9ee9961db8bccc92c85af8a30fd916056d69809446fb1987
5
+ SHA512:
6
+ metadata.gz: 95ffb1adfde494f374e7dc76d02b68074ed120505e224da3667a80f9f15cc188b72fa158094a0f6dd755116a6964b396c8a65b0c67888ca4bc50603cd9f9f629
7
+ data.tar.gz: 6cd43ebb0dd830c14e8bf079e33dedd65fc2c25ccb6488b00073e5cf783f9b178f52992a04ae3773ed873fb5743a31774a9a2c1e642f6ae2a95190d53749ff96
data/lib/wash/entry.rb ADDED
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wash
4
+ # Entry represents a common base class for Wash entries. All plugin entries
5
+ # should extend this class.
6
+ class Entry
7
+ class << self
8
+ # attributes is a class-level tag specifying all the attributes that
9
+ # make sense for instances of this specific kind of entry. It will
10
+ # pass the specified attributes along to attr_accessor so that instances
11
+ # can set their values. For example, something like
12
+ #
13
+ # @example
14
+ # class Foo
15
+ # # Instances of Foo will be able to set the @mtime and @meta fields
16
+ # attributes :mtime, :meta
17
+ # end
18
+ #
19
+ # @param [Symbol] attr An attribute that will be set
20
+ # @param [Symbol] attrs More attributes that will be set
21
+ def attributes(attr, *attrs)
22
+ @attributes ||= []
23
+ @attributes += set_fields(attr, *attrs)
24
+ end
25
+
26
+ # slash_replacer is a class-level tag that specifies the slash replacer.
27
+ # It should only be used if there is a chance that instances of the given
28
+ # class can contain a "#" in their names. Otherwise, slash_replacer should
29
+ # be ignored.
30
+ #
31
+ # @example
32
+ # class Foo
33
+ # # Tell Wash to replace all "/"es with ":" in the given Foo instance's
34
+ # # name
35
+ # slash_replacer ":"
36
+ # end
37
+ #
38
+ # @param [String] char The slash replacer
39
+ def slash_replacer(char)
40
+ @slash_replacer = char
41
+ end
42
+
43
+ # state is a class-level tag that specifies the minimum state required
44
+ # to reconstruct all instances of this specific kind of entry. Each specified
45
+ # state field will be passed along to attr_accessor so that instances can
46
+ # get/set their values.
47
+ #
48
+ # @example
49
+ # class Foo
50
+ # # Indicates that api_key is the minimum state required to reconstruct
51
+ # # instances of Foo. The gem will serialize the api_key as part of each
52
+ # # instance's state key when passing them along to Wash. If Wash invokes
53
+ # # a method on a specific instance, then Wash.run will restore the api_key
54
+ # # prior to invoking the method (so all methods are free to directly reference
55
+ # # the @api_key field). Thus, plugin authors do not have to manage their entries'
56
+ # # states; the gem will do it for them via the state tag.
57
+ # state :api_key
58
+ # end
59
+ #
60
+ # Note that Wash.run uses {Class#allocate} when it reconstructs the entries, so
61
+ # it does not call the initialize method.
62
+ def state(field, *fields)
63
+ @state ||= []
64
+ @state += set_fields(field, *fields)
65
+ end
66
+
67
+ # label is a class-level tag specifying the entry's label. It is a helper for
68
+ # Entry schemas.
69
+ #
70
+ # @param lbl The label.
71
+ def label(lbl)
72
+ @label = lbl
73
+ end
74
+
75
+ # is_singleton is a class-level tag indicating that the given Entry's a singleton.
76
+ # It is a helper for Entry schemas.
77
+ #
78
+ # Note that if an Entry has the is_singleton tag and its name is not filled-in
79
+ # when that Entry is listed, then the Entry's name will be set to the specified
80
+ # label. This means that plugin authors do not have to set singleton entries'
81
+ # names, and it also enforces the convention that singleton entries' labels should
82
+ # match their names.
83
+ #
84
+ # @example
85
+ # class Foo
86
+ # label 'foo'
87
+ # # If Foo's instance does not set @name, then the gem will set @name to 'foo'
88
+ # is_singleton
89
+ # end
90
+ def is_singleton
91
+ @singleton = true
92
+ end
93
+
94
+ # meta_attribute_schema sets the meta attribute's schema to schema. It is a helper
95
+ # for Entry schemas.
96
+ #
97
+ # @param schema A hash containing the meta attribute's JSON schema
98
+ def meta_attribute_schema(schema)
99
+ @meta_attribute_schema = schema
100
+ end
101
+
102
+ # metadata_schema sets the metadata schema to schema. It is a helper for Entry schemas.
103
+ #
104
+ # @param schema A hash containing the metadata's JSON schema
105
+ def metadata_schema(schema)
106
+ @metadata_schema = schema
107
+ end
108
+
109
+ # parent_of indicates that this kind of Entry is the parent of the given child classes
110
+ # (i.e. child entries). It is a helper for Entry schemas.
111
+ #
112
+ # @example
113
+ # class Foo
114
+ # # This indicates that Foo#list will return instances of Bar and Baz. Note
115
+ # # that both direct class constants (Bar) and strings ('Baz') are valid
116
+ # # input. The latter's useful when the child class is loaded after the
117
+ # # parent.
118
+ # parent_of Bar, 'Baz'
119
+ # end
120
+ #
121
+ # @param [Wash::Entry] child_klass A child class object.
122
+ # @param [Wash::Entry] child_klasses More child class objects.
123
+ def parent_of(child_klass, *child_klasses)
124
+ @child_klasses ||= []
125
+ @child_klasses += [child_klass] + child_klasses
126
+ end
127
+
128
+ # children returns this Entry's child classes. It is a helper for Entry schemas, and
129
+ # is useful for DRY'ing up schema code when one kind of Entry's children matches another
130
+ # kind of Entry's children.
131
+ #
132
+ # @example
133
+ # class VolumeDir
134
+ # parent_of 'VolumeDir', 'VolumeFile'
135
+ # end
136
+ #
137
+ # class Volume
138
+ # parent_of *VolumeDir.children
139
+ # end
140
+ def children
141
+ @child_klasses
142
+ end
143
+
144
+ private
145
+
146
+ def schema(visited)
147
+ visited[type_id] = {
148
+ label: @label,
149
+ methods: methods,
150
+ singleton: @singleton,
151
+ meta_attribute_schema: @meta_attribute_schema,
152
+ metadata_schema: @metadata_schema,
153
+ }
154
+ unless @child_klasses
155
+ return
156
+ end
157
+ visited[type_id][:children] = @child_klasses
158
+ @child_klasses.each do |child_klass|
159
+ child_klass = const_get(child_klass)
160
+ if visited[child_klass.send(:type_id)]
161
+ next
162
+ end
163
+ child_klass.send(:schema, visited)
164
+ end
165
+ end
166
+
167
+ def methods
168
+ wash_methods = Method.instance_variable_get(:@methods)
169
+ methods = []
170
+ self.public_instance_methods.each do |method|
171
+ if wash_methods[method]
172
+ # Only include the Wash methods. This makes the script's output easier
173
+ # to read when debugging.
174
+ methods.push(method)
175
+ end
176
+ end
177
+ unless Wash.send(:entry_schemas_enabled?)
178
+ # Don't include :schema if entry-schema support is not enabled. Otherwise,
179
+ # Wash will return an error since entry schemas are an "on/off" feature.
180
+ methods.delete(:schema)
181
+ end
182
+ methods
183
+ end
184
+
185
+ def type_id
186
+ self.name
187
+ end
188
+
189
+ def set_fields(field, *fields)
190
+ fields.unshift(field)
191
+ fields.each do |field|
192
+ attr_accessor field
193
+ end
194
+ fields
195
+ end
196
+ end
197
+
198
+ # All entries have a name. Note that the name is always
199
+ # included in the entry's state hash.
200
+ attr_accessor :name
201
+
202
+ def to_json(*)
203
+ unless @name && @name.size > 0
204
+ unless singleton
205
+ raise "A nameless entry is being serialized. The entry is an instance of #{type_id}"
206
+ end
207
+ @name = label
208
+ end
209
+
210
+ hash = {
211
+ type_id: type_id,
212
+ name: @name,
213
+ }
214
+
215
+ # Include the methods
216
+ hash[:methods] = self.class.send(:methods).map do |method|
217
+ if prefetched_methods.include?(method)
218
+ [method, self.send(method)]
219
+ else
220
+ method
221
+ end
222
+ end
223
+
224
+ # Include the remaining keys. Note that these checks are here to
225
+ # ensure that we don't serialize empty keys. They're meant to save
226
+ # some space.
227
+ if attributes.size > 0 && (attributes_hash = to_hash(attributes))
228
+ hash[:attributes] = attributes_hash
229
+ end
230
+ if cache_ttls.size > 0
231
+ hash[:cache_ttls] = cache_ttls
232
+ end
233
+ if slash_replacer.size > 0
234
+ hash[:slash_replacer] = slash_replacer
235
+ end
236
+ hash[:state] = to_hash(state).merge(klass: type_id, name: @name).to_json
237
+ if Wash.send(:pretty_print?)
238
+ JSON.pretty_generate(hash)
239
+ else
240
+ JSON.generate(hash)
241
+ end
242
+ end
243
+
244
+ # type_id returns the entry's type ID, which is its fully-qualified class
245
+ # name.
246
+ def type_id
247
+ self.class.send(:type_id)
248
+ end
249
+
250
+ # prefetch indicates that the given methods should be prefetched. This means
251
+ # that the gem will invoke those methods on this particular entry instance and
252
+ # include their results when serializing that entry. Note that the methods are
253
+ # invoked during serialization.
254
+ #
255
+ # @example
256
+ # class Foo
257
+ # def initialize(content_size)
258
+ # if content_size < 10
259
+ # # content_size < 10, so tell the gem to invoke Foo#list and Foo#read
260
+ # # on this Foo instance during its serialization
261
+ # prefetch :list, :read
262
+ # end
263
+ # end
264
+ # end
265
+ #
266
+ # @param [Symbol] method A method that should be prefetched.
267
+ # @param [Symbol] methods More methods that should be prefetched.
268
+ def prefetch(method, *methods)
269
+ prefetched_methods.concat([method] + methods)
270
+ end
271
+
272
+ # cache_ttls sets the cache TTLs (time-to-live) of the given methods.
273
+ #
274
+ # @example
275
+ # class Foo
276
+ # def initialize(content_size)
277
+ # if content_size > 10000
278
+ # # content_size > 10000 so tell Wash to cache its read result for
279
+ # # 100 seconds
280
+ # cache_ttls read: 100
281
+ # end
282
+ # end
283
+ # end
284
+ #
285
+ # @param [Hash] ttls A hash of <method_name> => <method_ttl>
286
+ def cache_ttls(ttls = {})
287
+ @cache_ttls ||= {}
288
+ @cache_ttls = @cache_ttls.merge(ttls)
289
+ end
290
+
291
+ # schema returns the entry's schema. It should not be overridden.
292
+ def schema
293
+ schemaHash = {}
294
+ self.class.send(:schema, schemaHash)
295
+ schemaHash
296
+ end
297
+
298
+ private
299
+
300
+ def attributes
301
+ self.class.instance_variable_get(:@attributes) || []
302
+ end
303
+
304
+ def slash_replacer
305
+ self.class.instance_variable_get(:@slash_replacer) || ''
306
+ end
307
+
308
+ def state
309
+ self.class.instance_variable_get(:@state) || {}
310
+ end
311
+
312
+ def label
313
+ self.class.instance_variable_get(:@label)
314
+ end
315
+
316
+ def singleton
317
+ self.class.instance_variable_get(:@singleton)
318
+ end
319
+
320
+ def prefetched_methods
321
+ @prefetched_methods ||= []
322
+ end
323
+
324
+ def to_hash(fields)
325
+ field_hash = {}
326
+ fields.each do |field|
327
+ field = field.to_sym
328
+ if value = self.send(field)
329
+ field_hash[field] = self.send(field)
330
+ end
331
+ end
332
+ field_hash
333
+ end
334
+
335
+ def restore_state(state)
336
+ state.each do |field, value|
337
+ accessor = "#{field}=".to_sym
338
+ unless self.respond_to?(accessor)
339
+ raise "#{field} is an invalid state value"
340
+ end
341
+ self.send(accessor, value)
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wash
4
+ # @api private
5
+ module Method
6
+ def self.invoke(method, entry, *args)
7
+ method = method.to_sym
8
+ unless entry.respond_to?(method)
9
+ raise "Entry #{entry.name} (#{entry.type_id}) does not implement #{method}"
10
+ end
11
+ unless invocation = @methods[method]
12
+ raise "#{method} is not a supported Wash method"
13
+ end
14
+ invocation.call(entry, *args)
15
+ end
16
+ private_class_method :invoke
17
+
18
+ def self.method(name, &block)
19
+ name = name.to_sym
20
+ unless block
21
+ block = lambda do |entry, *args|
22
+ result = entry.send(name, *args)
23
+ Wash.send(:print_json, result)
24
+ end
25
+ end
26
+ @methods ||= {}
27
+ @methods[name] = block
28
+ end
29
+ private_class_method :method
30
+
31
+ method(:list)
32
+ method(:read)
33
+ method(:metadata)
34
+ method(:schema)
35
+
36
+ method(:exec) do |entry, *args|
37
+ opts, cmd, args = Wash.send(:parse_json, args[0]), args[1], args[2..-1]
38
+ if opts[:stdin]
39
+ opts[:stdin] = STDIN
40
+ else
41
+ opts[:stdin] = nil
42
+ end
43
+ ec = entry.exec(cmd, args, opts)
44
+ exit ec
45
+ end
46
+
47
+ method(:stream) do |entry, _|
48
+ entry.stream
49
+ raise "stream should never return"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wash
4
+ # Streamer is a class meant to be used by implementations of Wash::Entry#stream.
5
+ class Streamer
6
+ def initialize
7
+ @first_chunk = true
8
+ end
9
+
10
+ # write writes the given chunk to STDOUT then flushes it to ensure that Wash
11
+ # receives the data. If the chunk is the first chunk that's written, then
12
+ # write will print the "200" header prior to writing the chunk.
13
+ #
14
+ # @param chunk The chunk to be written
15
+ def write(chunk)
16
+ if @first_chunk
17
+ puts("200")
18
+ @first_chunk = false
19
+ end
20
+ print(chunk)
21
+ STDOUT.flush
22
+ end
23
+ end
24
+ end
data/lib/wash.rb ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Wash
6
+ # pretty_print enables pretty printing of methods that produce
7
+ # JSON output. It is a useful debugging tool.
8
+ def self.pretty_print
9
+ @pretty_print = true
10
+ end
11
+
12
+ # enable_entry_schemas enables {Entry schema}[https://puppetlabs.github.io/wash/docs/#entry-schemas]
13
+ # support. See {Wash::Entry}'s documentation for more details on
14
+ # the available Entry schema helpers.
15
+ def self.enable_entry_schemas
16
+ @entry_schemas_enabled = true
17
+ end
18
+
19
+ # prefetch_entry_schemas enables schema-prefetching. This option
20
+ # should be enabled once external plugin development's finished.
21
+ # If the external plugin is not using Entry schemas, then this
22
+ # option can be ignored.
23
+ def self.prefetch_entry_schemas
24
+ @prefetch_entry_schemas = true
25
+ end
26
+
27
+ # on_sigterm will execute the provided block when the plugin script
28
+ # receives a SIGTERM/SIGINT signal. It is useful for handling
29
+ # plugin-specific cleanup like dangling processes, files, etc.
30
+ #
31
+ # @example
32
+ # class Foo
33
+ # # ...
34
+ # def stream
35
+ # # ...
36
+ # Wash.on_sigterm do
37
+ # # Kill any orphaned processes/files
38
+ # end
39
+ # # ...
40
+ # end
41
+ # end
42
+ def self.on_sigterm(&block)
43
+ sigterm_handlers << block
44
+ end
45
+
46
+ # run is the plugin script's run function. All plugin scripts using
47
+ # this gem should invoke this function once they've specified the
48
+ # desired configuration options (e.g. like pretty_print).
49
+ #
50
+ # @param [Wash::Entry] root_klass The plugin root's class object
51
+ #
52
+ # @param [Array<String>] argv The plugin script's arguments (usually ARGV).
53
+ def self.run(root_klass, argv)
54
+ Signal.trap('INT') do
55
+ handle_sigterm
56
+ exit 130
57
+ end
58
+ Signal.trap('TERM') do
59
+ handle_sigterm
60
+ exit 143
61
+ end
62
+
63
+ method, argv = next_arg(argv)
64
+
65
+ if method == "init"
66
+ config, argv = next_arg(argv)
67
+ root = root_klass.new
68
+ unless root.respond_to?(:init)
69
+ raise "Plugin root #{root.type_id} does not implement init."
70
+ end
71
+ config = parse_json(config)
72
+ root.init(config)
73
+ if @prefetch_entry_schemas
74
+ root.prefetch :schema
75
+ end
76
+ print_json(root)
77
+ return
78
+ end
79
+
80
+ _, argv = next_arg(argv)
81
+
82
+ state, argv = next_arg(argv)
83
+ state = parse_json(state)
84
+ klass = const_get(state.delete(:klass))
85
+ # Use klass#allocate instead of klass#new to give plugin authors
86
+ # more freedom in how they decide to setup their constructors
87
+ entry = klass.allocate
88
+ entry.send(:restore_state, state)
89
+
90
+ Method.send(:invoke, method, entry, *argv)
91
+ end
92
+
93
+ def self.next_arg(argv)
94
+ if argv.size < 1
95
+ raise "Invalid plugin-script invocation. See https://puppetlabs.github.io/wash/docs/external_plugins/ for details on what this should look like"
96
+ end
97
+ return argv[0], argv[1..-1]
98
+ end
99
+ private_class_method :next_arg
100
+
101
+ def self.handle_sigterm
102
+ sigterm_handlers.each do |handler|
103
+ handler.call
104
+ end
105
+ end
106
+ private_class_method :handle_sigterm
107
+
108
+ def self.pretty_print?
109
+ @pretty_print
110
+ end
111
+ private_class_method :pretty_print?
112
+
113
+ def self.entry_schemas_enabled?
114
+ @entry_schemas_enabled
115
+ end
116
+ private_class_method :entry_schemas_enabled?
117
+
118
+ def self.print_json(result)
119
+ if pretty_print?
120
+ result_json = JSON.pretty_generate(result)
121
+ else
122
+ result_json = JSON.generate(result)
123
+ end
124
+ puts(result_json)
125
+ end
126
+ private_class_method :print_json
127
+
128
+ def self.parse_json(json)
129
+ JSON.parse(json,:symbolize_names => true)
130
+ end
131
+ private_class_method :parse_json
132
+
133
+ def self.sigterm_handlers
134
+ @sigterm_handlers ||= []
135
+ end
136
+ private_class_method :sigterm_handlers
137
+
138
+ require 'wash/entry'
139
+ require 'wash/method'
140
+ require 'wash/streamer'
141
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Puppet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A library for building Wash external plugins
14
+ email:
15
+ - puppet@puppet.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/wash.rb
21
+ - lib/wash/entry.rb
22
+ - lib/wash/method.rb
23
+ - lib/wash/streamer.rb
24
+ homepage: https://github.com/puppetlabs/wash-ruby
25
+ licenses:
26
+ - Apache-2.0
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: '2.3'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.0.3
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: A library for building Wash external plugins
47
+ test_files: []