c66-copper 0.0.1

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +5 -0
  3. data/bin/console +14 -0
  4. data/bin/copper +96 -0
  5. data/bin/setup +8 -0
  6. data/lib/copper/action.rb +9 -0
  7. data/lib/copper/attribute.rb +30 -0
  8. data/lib/copper/attribute_params.rb +9 -0
  9. data/lib/copper/attributes.rb +21 -0
  10. data/lib/copper/attributes_right_associated.rb +11 -0
  11. data/lib/copper/boolean.rb +9 -0
  12. data/lib/copper/comparison.rb +58 -0
  13. data/lib/copper/compop.rb +8 -0
  14. data/lib/copper/copper.rb +32 -0
  15. data/lib/copper/copper_node.rb +16 -0
  16. data/lib/copper/data_types/array.rb +61 -0
  17. data/lib/copper/data_types/data_type.rb +54 -0
  18. data/lib/copper/data_types/ip_addr.rb +82 -0
  19. data/lib/copper/data_types/range.rb +17 -0
  20. data/lib/copper/data_types/semver.rb +45 -0
  21. data/lib/copper/data_types/string.rb +23 -0
  22. data/lib/copper/error.rb +5 -0
  23. data/lib/copper/expression.rb +8 -0
  24. data/lib/copper/expression_utils.rb +13 -0
  25. data/lib/copper/functions/fetch.rb +27 -0
  26. data/lib/copper/functions/ip_address.rb +18 -0
  27. data/lib/copper/grammar/copper.treetop +161 -0
  28. data/lib/copper/identifier.rb +9 -0
  29. data/lib/copper/loader.rb +13 -0
  30. data/lib/copper/logic.rb +14 -0
  31. data/lib/copper/logic_op.rb +7 -0
  32. data/lib/copper/logic_right_associated.rb +16 -0
  33. data/lib/copper/number.rb +10 -0
  34. data/lib/copper/param.rb +13 -0
  35. data/lib/copper/param_right_associated.rb +10 -0
  36. data/lib/copper/parser.rb +42 -0
  37. data/lib/copper/range.rb +15 -0
  38. data/lib/copper/root.rb +13 -0
  39. data/lib/copper/rule_definition.rb +21 -0
  40. data/lib/copper/set.rb +11 -0
  41. data/lib/copper/single_var_definition.rb +20 -0
  42. data/lib/copper/string.rb +17 -0
  43. data/lib/copper/var_definition.rb +11 -0
  44. data/lib/copper/variable.rb +9 -0
  45. data/lib/copper/variable_identifier.rb +15 -0
  46. data/lib/copper/version.rb +5 -0
  47. data/lib/copper.rb +5 -0
  48. metadata +247 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb48cc101f266912993eabc1c9836ca74d693afb
4
+ data.tar.gz: b13bfa681af1d729ca5185ddff419940e5921713
5
+ SHA512:
6
+ metadata.gz: b1137fb18f6e4f9ce9772056fad87741df3ecf9a47f9b61110038021bfc2b1551840644ae957e28b44b2d7fe060f543f9c35f650954806d7decdfd100412fb9c
7
+ data.tar.gz: a69ea15d41f510779d435b9eb74bfad4752396d551dcbd5ee513c7b40ab0fd9263d02d35a0d745e02b195a655035c5c74a28c8b73a8d85e30e248a91ef67375f
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Copper
2
+
3
+ ### Copper is a configuration validator for Kubernetes by Cloud 66
4
+
5
+ For more information, please visit [http://copper.sh](http://copper.sh)
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "copper"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/copper ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ require 'thor'
3
+ require 'yaml'
4
+ require 'colorize'
5
+ require_relative '../lib/copper'
6
+
7
+ module Copper
8
+ class CopperCLI < Thor
9
+ package_name "copper"
10
+
11
+ desc "version", "Show Copper version"
12
+ def version
13
+ say "#{::Copper::APP_NAME} #{::Copper::VERSION}\n#{::Copper::COPYRIGHT_MESSAGE}"
14
+ end
15
+
16
+ desc "update", "Update Copper to the latest version"
17
+ option :version, type: :string, desc: "Force a specific version"
18
+ def update
19
+ say "Updating Copper..."
20
+ unless options[:version]
21
+ say `gem install copper --no-ri --no-rdoc`
22
+ else
23
+ say `gem install copper -v #{options[:version]} --no-ri --no-rdoc`
24
+ end
25
+ end
26
+
27
+ desc "check", "Runs the given rules against a file"
28
+ option :rules, type: :string, desc: "Rules in ccop file format"
29
+ option :file, type: :string, desc: "Configuration file"
30
+ option :format, type: :string, enum: ['yaml'], default: 'yaml'
31
+ option :debug, type: :boolean, default: false
32
+ option :'parserdebug', type: :boolean, default: false
33
+ def check
34
+ $debug = options[:debug] || false
35
+ $parser_debug = options[:'parserdebug'] || false
36
+ require 'byebug' if $debug
37
+
38
+ rule_file = options[:rules]
39
+ unless File.exists?(rule_file)
40
+ puts "Rule file #{rule_file} not found".red
41
+ exit(1)
42
+ end
43
+ content_file = options[:file]
44
+ unless File.exists?(content_file)
45
+ puts "Config file #{content_file} not found".red
46
+ exit(1)
47
+ end
48
+
49
+ rules = File.read(rule_file)
50
+ file = ""
51
+ raise ::NotImplementedError if options[:format] != 'yaml'
52
+ # load the yaml file and split them into separate yamls
53
+
54
+ content = File.read(content_file)
55
+ content.split('---').each_with_index do |part, idx|
56
+ puts "Validating part #{idx}"
57
+ file = YAML::load(part)
58
+ validate(rules, file)
59
+ end
60
+ end
61
+
62
+ no_commands {
63
+ def validate(rules, file)
64
+ ccop = ::Copper::Copper.new(rules, { context: file })
65
+ results = ccop.execute
66
+
67
+ if results.nil?
68
+ puts "Aborting"
69
+ exit
70
+ end
71
+
72
+ results.each do |rule|
73
+ action = rule[:action]
74
+ outcome = rule[:outcome]
75
+ if !outcome
76
+ if action == :warn
77
+ outcome_text = 'WARN'.yellow
78
+ else
79
+ outcome_text = 'FAIL'.red
80
+ end
81
+ else
82
+ outcome_text = 'PASS'.green
83
+ end
84
+ puts "\t#{rule[:name]} - #{outcome_text}"
85
+ end
86
+ rescue ::Copper::ParseError => exc
87
+ puts "Syntax error: #{exc.message} at line #{ccop.parser.failure_line}, column: #{ccop.parser.failure_column}"
88
+ rescue ::Copper::RuntimeError => exc
89
+ puts "Runtime error: #{exc.message}"
90
+ end
91
+ }
92
+
93
+ end
94
+
95
+ CopperCLI.start
96
+ end
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ module Copper
2
+ class Action < CopperNode
3
+
4
+ def value(vars = {})
5
+ return text_value.to_sym
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ module Copper
2
+
3
+ class Attribute < CopperNode
4
+
5
+ # attribute takes in a parent as it always depends on another node
6
+ # like "abc".count here count is the attribute and string (abc) is the parent
7
+ # parent is a PORO
8
+ def value(parent, vars = {})
9
+ raise "invalid use of attribute" if parent.nil? # this shouldn't happen as parser will not let .count to be called without a parent
10
+
11
+ # 0 is the name of the attribute
12
+ attribute = elements[0].value
13
+ # 1 is the params and is optional
14
+ params = elements[1].value(vars) unless elements[1].nil?
15
+
16
+ # now we should call the func based on the name of the attribute with params on parent
17
+ # here we first convert the PORO to a DataType to make the calls safe
18
+ dt_obj = factory(parent)
19
+ if dt_obj.respond_to?(attribute)
20
+ if params.nil?
21
+ return dt_obj.send(attribute)
22
+ else
23
+ return dt_obj.send(attribute, *params)
24
+ end
25
+ else
26
+ raise ParseError, "#{attribute} is not a valid attribute for #{dt_obj.class.name}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Copper
2
+ class AttributeParams < CopperNode
3
+
4
+ def value(vars = {})
5
+ return elements[0].value(vars)
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Copper
2
+
3
+ class Attributes < CopperNode
4
+
5
+ # parent is the object attributes work on
6
+ # like "abc".map(xxx).count : abc (string) is parent for gsub and array from map is the parent for count
7
+ def value(parent, vars = {})
8
+ # this has all the attributes as an array. they will be [attribute, attributes]
9
+ # attributes will then be [attribute and attributes] and so on
10
+
11
+ current_obj = elements[0].value(parent, vars)
12
+ # no right association, so we are the last attribute
13
+ return current_obj if elements[1].nil?
14
+
15
+ # has right association, which means we need to get the value of the current attribute and pass into the
16
+ # next one
17
+ return elements[1].value(current_obj, vars)
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ module Copper
2
+ class AttributesRightAssociated < CopperNode
3
+ def value(vars = {})
4
+ byebug
5
+ rhs = self.parent.elements[0].value(vars)
6
+ lhs = self.elements[1].value(vars)
7
+
8
+ return lhs
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Copper
2
+ class Boolean < CopperNode
3
+
4
+ def value(vars = {})
5
+ return text_value == 'true'
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,58 @@
1
+ module Copper
2
+ class Comparison < CopperNode
3
+
4
+ def value(vars = {})
5
+ lhs = elements[0].value(vars)
6
+ comp_op = elements[1].value(vars)
7
+ rhs = elements[2].value(vars)
8
+
9
+ raise ParseError, "cannot compare nil" if rhs.nil? || lhs.nil?
10
+
11
+ begin
12
+ case comp_op
13
+ when '='
14
+ return equality(lhs, rhs)
15
+ when '=='
16
+ return equality(lhs, rhs)
17
+ when '>'
18
+ return lhs > rhs
19
+ when '<'
20
+ return lhs < rhs
21
+ when '<='
22
+ return lhs <= rhs
23
+ when '>='
24
+ return lhs > rhs
25
+ when '!='
26
+ return lhs != rhs
27
+ when 'in'
28
+ # this depends on the types
29
+ rhs_class = ::Copper::DataTypes::DataType.get_class(rhs.class.name)
30
+ rhs_obj = rhs_class.new(rhs)
31
+ if rhs_obj.respond_to?(:in)
32
+ return rhs_obj.in(lhs)
33
+ else
34
+ raise ::Copper::RuntimeError, "in is not a valid comparision on #{rhs_class.name}"
35
+ end
36
+
37
+ when '->'
38
+ # this is a special case to pipe lhs to console
39
+ console(lhs)
40
+ end
41
+ rescue NoMethodError => exc
42
+ raise ParseError, "comparison error: #{exc.message}"
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def equality(lhs, rhs)
49
+ if lhs.is_a?(Array) && rhs.is_a?(Array)
50
+ return lhs.sort == rhs.sort
51
+ else
52
+ return lhs == rhs
53
+ end
54
+ rescue ArgumentError => exc
55
+ raise ParseError, "equality comparison failed: #{exc.message}"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ module Copper
2
+ class CompOp < CopperNode
3
+
4
+ def value(vars = {})
5
+ return text_value
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ module Copper
2
+ class Copper
3
+ def initialize(content, vars = {})
4
+ @content = content
5
+ @vars = vars
6
+
7
+ # add the resevered ones
8
+ @vars[:variables] = ::Copper::DataTypes::DataType::RESEVERED_TYPES
9
+ @parser = Parser.new
10
+ end
11
+
12
+ def execute(options = {})
13
+ root_node = @parser.parse(@content)
14
+ result = root_node.value(@vars) unless root_node.nil?
15
+ puts result if $debug
16
+
17
+ return result
18
+ end
19
+
20
+ def valid?
21
+ self.execute(@content, @vars)
22
+ return true
23
+ rescue CopperError
24
+ return false
25
+ end
26
+
27
+ def parser
28
+ @parser.cc_parser
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ module Copper
2
+ # Copper parser node with helper methods for cleaning.
3
+ class CopperNode < Treetop::Runtime::SyntaxNode
4
+
5
+ protected
6
+
7
+ def console(text)
8
+ puts "[DEBUG] #{self.class.name} ==> #{text} (#{text.class.name})" if $debug
9
+ end
10
+
11
+ def factory(poro)
12
+ return ::Copper::DataTypes::DataType.factory(poro)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ module Copper
2
+ module DataTypes
3
+ class Array < DataType
4
+
5
+ def count
6
+ return @value.size
7
+ end
8
+
9
+ def first
10
+ raise RuntimeError, "cannot call first on an empty array" if @value.empty?
11
+ return @value.first
12
+ end
13
+
14
+ def last
15
+ raise RuntimeError, "cannot call last on an empty array" if @value.empty?
16
+ return @value.last
17
+ end
18
+
19
+ def in(value)
20
+ @value.include?(value)
21
+ end
22
+
23
+ # get item at index
24
+ def at(index)
25
+ if index >= @value.size
26
+ raise RuntimeError, "index #{index} out of bound [0..#{@result.size - 1}]"
27
+ else
28
+ return @value[index]
29
+ end
30
+ end
31
+
32
+ # map the items into the given class
33
+ def as(clazz)
34
+ found_class = ::Copper::DataTypes::DataType.get_class(clazz)
35
+ return @value.map { |x| found_class.new(x).value }
36
+ end
37
+
38
+ def extract(regex, index)
39
+ result = []
40
+ @value.each do |v|
41
+ raise RuntimeError, "#{v} is not a String" unless v.is_a?(::String)
42
+ found = v.match(regex)
43
+ result << nil if found.nil?
44
+ result << nil if found[index].nil?
45
+ result << found[index]
46
+ end
47
+
48
+ return result
49
+ end
50
+
51
+ def unique
52
+ @value.uniq
53
+ end
54
+
55
+ def contains(item)
56
+ @value.include?(item)
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,54 @@
1
+ module Copper
2
+ module DataTypes
3
+ class DataType
4
+
5
+ CLASS_MAP = {
6
+ "String" => "::Copper::DataTypes::String",
7
+ "Array" => "::Copper::DataTypes::Array",
8
+ "Semantic::Version" => "::Copper::DataTypes::Semver",
9
+ "Semver" => "::Copper::DataTypes::Semver",
10
+ "Range" => "::Copper::DataTypes::Range",
11
+ "IPAddress" => "::Copper::DataTypes::IPAddress",
12
+ "IPAddress::IPv4" => "::Copper::DataTypes::IPAddress",
13
+ "IPAddress::IPv6" => "::Copper::DataTypes::IPAddress"
14
+ }
15
+
16
+ RESEVERED_TYPES = {
17
+ semver: "Semver",
18
+ array: "Array",
19
+ string: "String",
20
+ range: "Range",
21
+ ipaddr: "IPAddress"
22
+ }
23
+
24
+ def initialize(value)
25
+ @value = value
26
+ end
27
+
28
+ def value
29
+ @value
30
+ end
31
+
32
+ def as(clazz)
33
+ found_class = ::Copper::DataTypes::DataType.get_class(clazz)
34
+ return found_class.new(@value).value
35
+ end
36
+
37
+ # returns a DataType based on the given PORO
38
+ # this is to control the attribute exposure, better error handling, etc
39
+ def self.factory(poro)
40
+ poro_class = poro.class.name
41
+
42
+ clazz = self.get_class(poro_class)
43
+ return clazz.new(poro)
44
+ end
45
+
46
+ def self.get_class(class_name)
47
+ raise RuntimeError, "unknown return value #{class_name}" unless ::Copper::DataTypes::DataType::CLASS_MAP.has_key?(class_name)
48
+ return Module.const_get(::Copper::DataTypes::DataType::CLASS_MAP[class_name])
49
+ rescue NameError => exc
50
+ raise ::Copper::RuntimeError, "invalid return type #{class_name}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ require 'ipaddr'
2
+
3
+ module Copper
4
+ module DataTypes
5
+
6
+ class IPAddress
7
+ def initialize(value)
8
+ if value.is_a? ::String
9
+ @value = ::IPAddress.parse(value)
10
+ elsif value.is_a?(::IPAddress::IPv4)
11
+ @value = value
12
+ elsif value.is_a?(::IPAddress::IPv6)
13
+ @value = value
14
+ else
15
+ raise ::Copper::RuntimeError, "cannot convert #{value} to IPAddress"
16
+ end
17
+ rescue ArgumentError => exc
18
+ raise ::Copper::RuntimeError, exc.message
19
+ end
20
+
21
+ def first
22
+ @value.first
23
+ end
24
+
25
+ def last
26
+ @value.last
27
+ end
28
+
29
+ def in(value)
30
+ raise ::Copper::RuntimeError, "#{value} is not an IP address" unless value.is_a?(::IPAddress)
31
+ (@value.first.to_i <= value.to_i) && (@value.last.to_i >= value.to_i)
32
+ end
33
+
34
+ def full_address
35
+ @value.to_string
36
+ end
37
+
38
+ def address
39
+ @value.address
40
+ end
41
+
42
+ def netmask
43
+ @value.netmask
44
+ end
45
+
46
+ def octets
47
+ @value.octets
48
+ end
49
+
50
+ def prefix
51
+ @value.prefix.to_i
52
+ end
53
+
54
+ def is_network
55
+ @value.network?
56
+ end
57
+
58
+ def is_loopback
59
+ @value.loopback?
60
+ end
61
+
62
+ def is_multicast
63
+ @value.multicast?
64
+ end
65
+
66
+ def is_class_a
67
+ @value.a?
68
+ end
69
+
70
+ def is_class_b
71
+ @value.b?
72
+ end
73
+
74
+ def is_class_c
75
+ @value.c?
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,17 @@
1
+ module Copper
2
+ module DataTypes
3
+
4
+ class Range < DataType
5
+
6
+ def contains(value)
7
+ @value.include?(value)
8
+ end
9
+
10
+ def in(value)
11
+ @value.include?(value)
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ require 'semantic'
2
+
3
+ module Copper
4
+ module DataTypes
5
+ class Semver < DataType
6
+
7
+ def initialize(value)
8
+ if value.is_a? ::String
9
+ @value = ::Semantic::Version.new(value)
10
+ elsif value.is_a? ::Semantic::Version
11
+ @value = ::Semantic::Version.new(value.to_s)
12
+ else
13
+ raise ::Copper::RuntimeError, "cannot convert #{value} to Semver"
14
+ end
15
+ rescue ArgumentError => exc
16
+ raise ::Copper::RuntimeError, exc.message
17
+ end
18
+
19
+ def major
20
+ @value.major
21
+ end
22
+
23
+ def minor
24
+ @value.minor
25
+ end
26
+
27
+ def patch
28
+ @value.patch
29
+ end
30
+
31
+ def build
32
+ @value.build
33
+ end
34
+
35
+ def pre
36
+ @value.pre
37
+ end
38
+
39
+ def satisfies(condition)
40
+ @value.satisfies?(condition)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Copper
2
+ module DataTypes
3
+ class String < DataType
4
+
5
+ def count
6
+ return @value.size
7
+ end
8
+
9
+ def gsub(pattern, replacement)
10
+ return @value.gsub(pattern, replacement)
11
+ end
12
+
13
+ def at(index)
14
+ if index >= @value.size
15
+ raise ParseError, "index #{index} out of bound [0..#{@result.size - 1}]"
16
+ else
17
+ return @value[index]
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module Copper
2
+ class CopperError < StandardError; end
3
+ class ParseError < CopperError; end
4
+ class RuntimeError < CopperError; end
5
+ end
@@ -0,0 +1,8 @@
1
+ module Copper
2
+ class Expression < CopperNode
3
+
4
+ def value(vars = {})
5
+ return elements[0].value(vars)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module Copper
2
+ module ExpressionUtils
3
+
4
+ def handle_attributes(parent, vars)
5
+ tail = elements.last
6
+ return parent if tail.nil? || !tail.is_a?(::Copper::Attributes)
7
+
8
+ # run the tail
9
+ return tail.value(parent, vars)
10
+ end
11
+
12
+ end
13
+ end