cloudinit_userdata 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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