wash 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.
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: []