c66-copper 0.0.1

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