cloudinit_userdata 0.0.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2d7e996e9f77331d34d824d38ac387360d4bd5ec
4
- data.tar.gz: 2facecfe592044513a243ab9e0624fb0d685f433
3
+ metadata.gz: 6bdf94b32e15500f0207061b0e75d47a43218def
4
+ data.tar.gz: 167dd5eaeacba560bb01c830f9d9bfb1af9b9fba
5
5
  SHA512:
6
- metadata.gz: 3b7b9d40680f06be3b4ac052915d06828e678ce1d544f9e893c88c49e9d7e37a50872678945c5e723cab4cef692f0eae730df8483a9674fe279a13b59803a23f
7
- data.tar.gz: d707e9a42c52197fbda588714a597b3c83d5d1a0cba5be0c10c792a1c034b1f637fe4f859071c5d2ee6506f53c31fdfcd803c7b183f96a9b959120620f8d9ba1
6
+ metadata.gz: 5392c767a737ad08b23bc52907436b33de40436eb4875932b146c672821b9ea4f8fe6b83227b47047aeb3e0ec6dcd9d091aea9ccf72702166609a81c2ce8df87
7
+ data.tar.gz: 62d5381c1e5fbd4485fdca9dbc1f5f9bc67475e4cd7730fad727dcc49864a423b17acc5b9ba079a07fe1f01d76ae08a1aabf73be1e47a242f7baffd2fc3cc831
data/README.md CHANGED
@@ -11,25 +11,52 @@ Usage
11
11
  ```ruby
12
12
  require 'cloudinit_userdata'
13
13
 
14
- userdata = CloudInit::Userdata.new <<-USERDATA.strip
14
+ userdata = CloudInit::Userdata.parse <<-USERDATA.strip
15
15
  #!/bin/bash
16
16
 
17
17
  echo "Hello world!"
18
18
  USERDATA
19
19
 
20
- userdata.script? # true
21
- userdata.cloud_config? # false
22
- userdata.json? # false
20
+ userdata.class # CloudInit::Userdata::ShellScript
23
21
  userdata.valid? # true
24
22
  ```
25
23
 
26
- Understand userdata scripts (begin with `#!`), JSON (used by CoreOS/ignition),
27
- and all of the [core CloudInit formats](http://cloudinit.readthedocs.org/en/latest/topics/format.html).
24
+ Currently understands all of the
25
+ [core CloudInit formats](http://cloudinit.readthedocs.org/en/latest/topics/format.html).
26
+ Mime Multipart and gzipped userdata are also supported.
27
+
28
+ An adapter is also included for JSON userdata (used by CoreOS/ignition), but it
29
+ is not loaded by default, since it is not part of the original implementation.
30
+ To include it, do the following somewhere:
31
+
32
+ ```
33
+ require 'cloudinit_userdata/formats/json'
34
+ CloudInit::Userdata.register_format(CloudInit::Userdata::JSON)
35
+ ```
36
+
37
+ Custom Adapters
38
+ ---------------
39
+
40
+ You can implement formatters for your custom types of userdata. Formatters
41
+ should inherit from `CloudInit::Userdata`, and are expected to implement the
42
+ `.match?` method at minimum.
43
+
44
+ Formatters may also implement `#validate`, which will be called automatically
45
+ by `CloudInit::Userdata#valid?`.
46
+
47
+ Formatters may also implement `.mimetypes` and return an array of mimetype
48
+ strings that will be recognized for Mime Multipart userdata.
49
+
50
+ Finally, custom formatters must be registered with this library using the
51
+ `CloudInit::Userdata.register_format(<your format class>)`.
28
52
 
29
53
  Known Limitations
30
54
  -----------------
31
55
 
32
- * Currently, this library does not support [mime-multi part files](http://cloudinit.readthedocs.org/en/latest/topics/format.html#mime-multi-part-archive). We plan to eventually support this feature, but it is currently low priority.
56
+ * This library does not validate the semantics of userdata, since the only way
57
+ to do that accurately is to actually run the userdata on a machine, and we
58
+ obviously can't do that. Rather, this library attempts to provide some basic
59
+ intelligence and parse-checking around userdata formats.
33
60
 
34
61
  Contributing
35
62
  ------------
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.bindir = 'bin'
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
 
21
+ spec.add_dependency 'mail', '~> 2.6'
22
+
21
23
  spec.add_development_dependency 'rake', '~> 0'
22
24
  spec.add_development_dependency 'rspec', '~> 3'
23
25
  spec.add_development_dependency 'rdoc', '~> 4'
@@ -1,6 +1,8 @@
1
1
  module CloudInit
2
- class Error < RuntimeError; end
3
- class InvalidUserdata < Error; end
4
- class InvalidUserdataType < InvalidUserdata; end
5
- class ParseError < InvalidUserdata; end
2
+ class Userdata
3
+ class Error < RuntimeError; end
4
+ class InvalidUserdata < Error; end
5
+ class InvalidFormat < InvalidUserdata; end
6
+ class ParseError < InvalidUserdata; end
7
+ end
6
8
  end
@@ -0,0 +1,15 @@
1
+ module CloudInit
2
+ class Userdata
3
+ class Blank < Userdata
4
+ REGEXP = /\A[[:space:]]*\z/
5
+
6
+ def self.match?(value)
7
+ value.nil? || !REGEXP.match(value).nil?
8
+ end
9
+
10
+ def empty?
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module CloudInit
2
+ class Userdata
3
+ class CloudBoothook < Userdata
4
+ PREFIX = '#cloud-boothook'.freeze
5
+ MIMETYPES = %w(text/cloud-boothook).freeze
6
+
7
+ def self.match?(value)
8
+ value.start_with?(PREFIX)
9
+ end
10
+
11
+ def self.mimetypes
12
+ MIMETYPES
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ require 'yaml'
2
+ require 'cloudinit_userdata/errors'
3
+
4
+ module CloudInit
5
+ class Userdata
6
+ class CloudConfig < Userdata
7
+ PREFIX = "#cloud-config\n".freeze
8
+ MIMETYPES = %w(text/cloud-config).freeze
9
+
10
+ def validate
11
+ YAML.safe_load(raw)
12
+ rescue Psych::SyntaxError => e
13
+ raise ParseError, "Contains invalid YAML at line #{e.line}, column #{e.column}: #{e.problem} #{e.context}"
14
+ end
15
+
16
+ def self.match?(value)
17
+ value.start_with?(PREFIX)
18
+ end
19
+
20
+ def self.mimetypes
21
+ MIMETYPES
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ module CloudInit
5
+ class Userdata
6
+ # This class is really just a special formatter that wraps another
7
+ # formatter and delegates an un-gzipped version of the string to the
8
+ # underlying parser.
9
+ class Gzipped < Userdata
10
+ PREFIX = "\x1F\x8B".b.freeze
11
+
12
+ attr_accessor :formatter
13
+
14
+ def initialize(raw)
15
+ super
16
+ self.formatter = CloudInit::Userdata.parse(self.raw)
17
+ end
18
+
19
+ def raw(decompressed = true)
20
+ if decompressed
21
+ Zlib::GzipReader.new(StringIO.new(super())).read
22
+ else
23
+ super()
24
+ end
25
+ end
26
+
27
+ def empty?
28
+ formatter.empty?
29
+ end
30
+
31
+ def validate
32
+ formatter.validate
33
+ end
34
+
35
+ def to_s
36
+ raw(false)
37
+ end
38
+
39
+ def self.match?(value)
40
+ !value.nil? && value[0..1] == PREFIX
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ module CloudInit
2
+ class Userdata
3
+ class Include < Userdata
4
+ PREFIX = '#include'.freeze
5
+ MIMETYPES = %w(text/x-include-url text/x-include-once-url).freeze
6
+
7
+ def self.match?(value)
8
+ value.start_with?(PREFIX)
9
+ end
10
+
11
+ def self.mimetypes
12
+ MIMETYPES
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+ require 'cloudinit_userdata/errors'
3
+
4
+ module CloudInit
5
+ class Userdata
6
+ class JSON < Userdata
7
+ PREFIXES = %w([ {).freeze
8
+ MIMETYPES = %w(application/json).freeze
9
+
10
+ def validate
11
+ JSON.parse(raw)
12
+ rescue JSON::ParserError => e
13
+ raise ParseError, "Contains invalid JSON: #{e.message.sub(/^(\d+): /, '')}"
14
+ end
15
+
16
+ def self.match?(value)
17
+ PREFIXES.any? { |prefix| value.start_with?(prefix) }
18
+ end
19
+
20
+ def self.mimetypes
21
+ MIMETYPES
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ require 'mail'
2
+ require 'cloudinit_userdata/errors'
3
+
4
+ module CloudInit
5
+ class Userdata
6
+ class MimeMultipart < Userdata
7
+ MATCH_STRING = 'Content-Type: multipart/mixed'.freeze
8
+ MIMETYPES = %w(multipart/mixed).freeze
9
+
10
+ attr_accessor :formatters
11
+
12
+ def initialize(raw)
13
+ super
14
+ self.formatters = self.class.parse_formatters(raw)
15
+ end
16
+
17
+ def validate
18
+ formatters.each(&:validate)
19
+ end
20
+
21
+ def empty?
22
+ formatters.all?(&:empty?)
23
+ end
24
+
25
+ def self.match?(value)
26
+ value.include?(MATCH_STRING)
27
+ end
28
+
29
+ def self.mimetypes
30
+ MIMETYPES
31
+ end
32
+
33
+ def self.parse_formatters(raw)
34
+ Mail.new(raw).parts.map do |part|
35
+ mime = part.mime_type
36
+ formatter = Userdata.formats.find { |f| f.mimetypes.include?(mime) }
37
+ raise InvalidFormat, "Userdata format for mime type #{mime} not found" unless formatter
38
+ formatter.new(part.body.raw_source)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ module CloudInit
2
+ class Userdata
3
+ class PartHandler < Userdata
4
+ PREFIX = '#part-handler'.freeze
5
+ MIMETYPES = %w(text/part-handler).freeze
6
+
7
+ def self.match?(value)
8
+ value.start_with?(PREFIX)
9
+ end
10
+
11
+ def self.mimetypes
12
+ MIMETYPES
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ require 'cloudinit_userdata/errors'
2
+
3
+ module CloudInit
4
+ class Userdata
5
+ class ShellScript < Userdata
6
+ SHEBANG = '#!'.freeze
7
+ SHEBANG_REGEXP = /^#!\S.+\n/
8
+ MIMETYPES = %w(text/x-shellscript).freeze
9
+
10
+ def validate
11
+ return if raw =~ SHEBANG_REGEXP
12
+ raise InvalidUserdata, 'Script is not a properly formatted to call an executable on line 1'
13
+ end
14
+
15
+ def self.match?(value)
16
+ value.start_with?(SHEBANG)
17
+ end
18
+
19
+ def self.mimetypes
20
+ MIMETYPES
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module CloudInit
2
+ class Userdata
3
+ class UpstartJob < Userdata
4
+ PREFIX = '#upstart-job'.freeze
5
+ MIMETYPES = %w(text/upstart-job).freeze
6
+
7
+ def self.match?(value)
8
+ value.start_with?(PREFIX)
9
+ end
10
+
11
+ def self.mimetypes
12
+ MIMETYPES
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,48 +1,22 @@
1
- require 'zlib'
2
- require 'stringio'
3
- require 'cloudinit_userdata/validator'
1
+ require 'cloudinit_userdata/errors'
4
2
 
5
3
  module CloudInit
6
4
  class Userdata
7
- BLANK_REGEXP = /\A[[:space:]]*\z/
8
- GZIP_PREFIX = "\x1F\x8B".b.freeze
9
-
10
- SHEBANG = '#!'.freeze
11
- CLOUD_CONFIG_PREFIX = "#cloud-config\n".freeze
12
- JSON_PREFIXES = %w([ {).freeze
13
- PREFIXES = JSON_PREFIXES + [
14
- SHEBANG, CLOUD_CONFIG_PREFIX,
15
- '#include', '#upstart-job', '#cloud-boothook', '#part-handler'
16
- ].freeze
5
+ @formats = []
17
6
 
18
7
  attr_accessor :raw
8
+ alias to_s raw
19
9
 
20
10
  def initialize(raw)
21
11
  self.raw = raw
22
12
  end
23
13
 
24
- def script?
25
- to_s(:human).start_with?(SHEBANG)
26
- end
27
-
28
- def cloud_config?
29
- to_s(:human).start_with?(CLOUD_CONFIG_PREFIX)
30
- end
31
-
32
- def json?
33
- JSON_PREFIXES.include?(to_s(:human)[0])
34
- end
35
-
36
14
  def empty?
37
- raw.nil? || !BLANK_REGEXP.match(to_s(:human)).nil?
38
- end
39
-
40
- def gzipped?
41
- !raw.nil? && raw[0..1] == GZIP_PREFIX
15
+ false
42
16
  end
43
17
 
44
- def decompressed
45
- Zlib::GzipReader.new(StringIO.new(raw)).read
18
+ def validate
19
+ # noop
46
20
  end
47
21
 
48
22
  def valid?
@@ -52,16 +26,26 @@ module CloudInit
52
26
  false
53
27
  end
54
28
 
55
- def validate
56
- Validator.new(self).call
29
+ def self.match?(_value)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def self.mimetypes
34
+ []
35
+ end
36
+
37
+ def self.parse(value)
38
+ formatter = @formats.find { |f| f.match?(value) }
39
+ raise InvalidFormat, 'Unrecognized userdata format' unless formatter
40
+ formatter.new(value)
41
+ end
42
+
43
+ def self.register_format(klass)
44
+ @formats << klass unless @formats.include?(klass)
57
45
  end
58
46
 
59
- def to_s(format = nil)
60
- case format
61
- when nil, :raw then raw
62
- when :human, :decompressed
63
- gzipped? ? decompressed : raw
64
- end
47
+ class << self
48
+ attr_reader :formats
65
49
  end
66
50
  end
67
51
  end
@@ -1,5 +1,5 @@
1
1
  module CloudInit
2
2
  class Userdata
3
- VERSION = '0.0.1'.freeze
3
+ VERSION = '1.0.0'.freeze
4
4
  end
5
5
  end
@@ -3,7 +3,39 @@ $LOAD_PATH.unshift File.dirname(__FILE__)
3
3
  require 'cloudinit_userdata/version'
4
4
  require 'cloudinit_userdata/errors'
5
5
  require 'cloudinit_userdata/userdata'
6
- require 'cloudinit_userdata/validator'
7
6
 
8
7
  module CloudInit
9
8
  end
9
+
10
+ #
11
+ # Register our userdata formats.
12
+ #
13
+
14
+ # Note that the Gzipped formatter should go first so that it can deal with
15
+ # binary data.
16
+ require 'cloudinit_userdata/formats/gzipped'
17
+ CloudInit::Userdata.register_format(CloudInit::Userdata::Gzipped)
18
+
19
+ require 'cloudinit_userdata/formats/blank'
20
+ CloudInit::Userdata.register_format(CloudInit::Userdata::Blank)
21
+
22
+ require 'cloudinit_userdata/formats/cloud_boothook'
23
+ CloudInit::Userdata.register_format(CloudInit::Userdata::CloudBoothook)
24
+
25
+ require 'cloudinit_userdata/formats/cloud_config'
26
+ CloudInit::Userdata.register_format(CloudInit::Userdata::CloudConfig)
27
+
28
+ require 'cloudinit_userdata/formats/include'
29
+ CloudInit::Userdata.register_format(CloudInit::Userdata::Include)
30
+
31
+ require 'cloudinit_userdata/formats/mime_multipart'
32
+ CloudInit::Userdata.register_format(CloudInit::Userdata::MimeMultipart)
33
+
34
+ require 'cloudinit_userdata/formats/part_handler'
35
+ CloudInit::Userdata.register_format(CloudInit::Userdata::PartHandler)
36
+
37
+ require 'cloudinit_userdata/formats/shell_script'
38
+ CloudInit::Userdata.register_format(CloudInit::Userdata::ShellScript)
39
+
40
+ require 'cloudinit_userdata/formats/upstart_job'
41
+ CloudInit::Userdata.register_format(CloudInit::Userdata::UpstartJob)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudinit_userdata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bell
@@ -10,6 +10,20 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 2016-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mail
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -127,8 +141,17 @@ files:
127
141
  - cloudinit_userdata.gemspec
128
142
  - lib/cloudinit_userdata.rb
129
143
  - lib/cloudinit_userdata/errors.rb
144
+ - lib/cloudinit_userdata/formats/blank.rb
145
+ - lib/cloudinit_userdata/formats/cloud_boothook.rb
146
+ - lib/cloudinit_userdata/formats/cloud_config.rb
147
+ - lib/cloudinit_userdata/formats/gzipped.rb
148
+ - lib/cloudinit_userdata/formats/include.rb
149
+ - lib/cloudinit_userdata/formats/json.rb
150
+ - lib/cloudinit_userdata/formats/mime_multipart.rb
151
+ - lib/cloudinit_userdata/formats/part_handler.rb
152
+ - lib/cloudinit_userdata/formats/shell_script.rb
153
+ - lib/cloudinit_userdata/formats/upstart_job.rb
130
154
  - lib/cloudinit_userdata/userdata.rb
131
- - lib/cloudinit_userdata/validator.rb
132
155
  - lib/cloudinit_userdata/version.rb
133
156
  homepage: https://www.packet.net
134
157
  licenses: []
@@ -1,49 +0,0 @@
1
- require 'cloudinit_userdata/errors'
2
-
3
- module CloudInit
4
- class Userdata
5
- class Validator
6
- attr_accessor :userdata
7
-
8
- def initialize(userdata)
9
- self.userdata = userdata
10
- end
11
-
12
- def call
13
- case
14
- when userdata.empty? then return
15
- when ::CloudInit::Userdata::PREFIXES.none? { |prefix| value.start_with?(prefix) }
16
- raise InvalidUserdataType, 'Unrecognized userdata format'
17
- when userdata.script? then validate_script
18
- when userdata.cloud_config? then validate_cloud_config
19
- when userdata.json? then validate_json
20
- end
21
- end
22
-
23
- private
24
-
25
- def validate_script
26
- return if value =~ /^#!\S.+\n/
27
- raise InvalidUserdata, 'Script is not a properly formatted to call an executable on line 1'
28
- end
29
-
30
- def validate_cloud_config
31
- require 'yaml'
32
- YAML.safe_load(value)
33
- rescue Psych::SyntaxError => e
34
- raise ParseError, "Contains invalid YAML at line #{e.line}, column #{e.column}: #{e.problem} #{e.context}"
35
- end
36
-
37
- def validate_json
38
- require 'json'
39
- JSON.parse(value)
40
- rescue JSON::ParserError => e
41
- raise ParseError, "Contains invalid JSON: #{e.message.sub(/^(\d+): /, '')}"
42
- end
43
-
44
- def value
45
- @value ||= userdata.to_s(:human)
46
- end
47
- end
48
- end
49
- end