-
Notifications
You must be signed in to change notification settings - Fork 15
Copper DSL
Copper DSL is a simple language that's focused on fetching values from configuration files and checking their validity. It has built-in functionality to deal with IP Addresses, Semantic versioning of components and basic string manipulation.
Copper files contain the Copper DSL script. They have text files and have a .cop
extension. You can use any text editor to edit them.
There is an extension for VisualStudio Code that provides syntax highlighting for Copper files. This extension is under active development and doesn't support Copper DSL's full syntax.
A rule is like a test you would like to run against your configuration file. Just like code unit tests, it's better to keep the rules focused on one specific area of the configuration file and give then relevant names. A Copper file can contain as many rules as you like.
A rule must have a name. Names should begin with a letter and can contain any alphanumerical characters.
A rule must have an Action. Action is what should happen if the rule fails: An ensure
action means failure of the rule will fail the validation. A warn
action means a warning is shown next to the failed rule but the validations will pass.
A condition is a boolean logic that should be true for the rule to pass.
rule NAME (warn | ensure) { CONDITION }
Example
rule foo ensure { 1 = 1 }
Defines a rule called foo
which ensures the statement 1 = 1
returns true
You can define variables in Copper files as a way to avoid repeating the same things over and over again. For example, you can keep your the valid range of ports in a variable and use that variable in different rules. In Copper DSL, variables are more like constants in other languages and cannot be changed once set.
var VARIABLE = VALUE
Example
var foo = 1 var bar = "hello" var valid_ports = (8000..9000)
Using variables in a rule
rule bar warn { 8050 in valid_ports == true }
Copper files can be commented. Copper supports the Java comment syntax:
rule foo warn { // this is a single inline comment 1 = 1 /* this is a single line block comment */ } /* we are going to comment this part off rule bar ensure { false = true }*/
The condition inside of a rule is usually made up of a value compared against another value. The result of this comparison is either true or false.
The comparison operation can be one of the following:
Operation | Meaning |
---|---|
= or ==
|
Left side is equal the right side |
> |
Left side is greater than the right side |
< |
Left side is less than the right side |
>= |
Left side is greater than or equal the right side |
<= |
Left side is less than or equal the right side |
!= |
Left side is not equal the right side |
in |
Right side is included in the left side (only for sets and ranges) |
Comparisons can be combined with and
and or
to make up more complex conditions.
Example
rule ComplexRulesAreUs warn { 2 > 1 and 3 == 3 or 2 != 8 and 8 in [1,2,3,4] }
Boolean Operand | Meaning |
---|---|
and , & or &&
|
Boolean AND |
or , | or ||
|
Boolean OR |
Copper DSL supports the following data types:
A number is integer or decimal.
Example
var my_int = 12 rule foo ensure { my_int > 11.43 }
Strings are wrapped in double quotes "
.
Example
var a_string = "foo"
Strings have the following attributes:
Returns the length of the string: "foo".count
will return 3.
Replaces text in the string using the regular expression pattern given.
For example "abc".gsub("b", "!")
will return "a!c"
.
Returns the character at the given index: "abc".at(2)
returns "b"
.
Splits the string into an array: "foo/bar/baz".split("/")
returns ["foo", "bar", "baz"]
.
Arrays can contain any number of values. Arrays can hold values of different types. An array is wrapped in [
and ]
and each item is separated by a ,
.
Example
var my_array = [1,2,3,4]
Arrays have the following attributes:
Returns the number of items in the array: [1,2,3].count
will return 3.
Returns the first item of the array: ["foo","bar",45].first
will return "foo"
.
Returns the last item of the array: ["foo","bar",45].first
will return 45.
Returns the item at the given index: [1, "item 2", "third item", 4].at(2)
returns "item 2"
.
Returns true if the given item can be found in the array: [1,2,"foo"].contains(2)
will return true.
Removes duplicates from the array and returns a new array: [1,2,3,2,1].unique
will return [1,2,3]
Runs each item of a string only array through a regular expression and returns the item with the given index of the regexp:
["name1:tag1", "name2:tag2", "name3:tag3"].extract(".*:(.*)", 1)
will return ["tag1", "tag2", "tag3"]
. The number 1
in this case refers to the regexp group.
Another example
["path1/image1:tag1", "path2/image2:tag2"].extract(".*\/(.*):.*", 2)
will return ["image1", "image2", "image3"]
.
Converts each element of an array into a different data type. For example this can be used to convert an array of strings into Image data type.
["quay.io/mysql:1.2.3", "ubuntu:3.2.1"].as(:image)
returns an array of Image
data type (see below).
Returns an array by picking an attributes off of each item of the array. For example this can be used to pick the tag
attribute of an array of Image
.
The example below returns the length of each element of an array:
["a", "xo", "foo"].pick(:count)
returns [1, 2, 3]
pick
takes in the name of the attribute to pick in the form of a :
followed by the attribute name. For example to pick the tag
attribute you can use pick(:tag)
.
You can use =
or ==
to compare two arrays. This will return true if both arrays contain the same items but ignores the ordering of the items. For example:
[1,2,3] == [2,3,1]
while [1,2,3] != [1,2,3,4]
.
You can use the in
comparison for arrays: 1 in [1,2,3]
is true and "foo" in ["bar", "fuzz"]
is false.
Range contains all the numbers between two numbers. Ranges are wrapped in (
and )
with ..
between the low and the high numbers. Range is inclusive of both ends.
Example
var the_range = (1..10)
Returns true if the given item can be found in the range: (1..10).contains(1)
will return true.
You can use the in
comparison for ranges: 10 in (1..10)
is true and 13 in (23..45)
is false.
Copper DSL supports a growing set of configuration specific data types. Currently this includes the following:
An IPAddress can hold an IP address and/or subnet. You can use IPAddress to check various things about an IPAddress, like it's range, inclusion of other IP addresses, its class and more.
Example
var internal = ipaddress("62.0.0.0/24") var front_end = ipaddress("62.0.2.45")
IPAddress has the following attributes:
Returns the first IP address in a range: ipaddress("10.0.0.0/24").first
will return ipaddress("10.0.0.1")
Returns the last IP address in a range: ipaddress("10.0.0.0/24").last
will return ipaddress("10.0.0.254")
Returns the IP address and the prefix: ipaddress("10.0.0.1").full_address
will return "10.0.0.1/32"
Returns the IP address without the prefix: ipaddress("172.16.10.1/24").address
will return "172.16.10.1"
Returns the IP netmask: ipaddress("10.0.0.0/8").netmask
will return "255.0.0.0"
Returns an array of IP address octets: ipaddress("172.16.10.1").octets
will return [172, 16, 10, 1]
Returns the IP address prefix without the address: ipaddress("172.16.10.1/24").prefix
will return 8
Returns true if the given IP address is a network address. ipaddress("10.0.0.0/24").is_network
will return true while ipaddress("10.0.0.1/32").is_network
returns false.
Returns true if the given IP address is a local loopback address. ipaddress("127.0.0.1").is_loopback
will return true.
Returns true if the given IP address is a multicast address. ipaddress("224.0.0.1/32").is_multicast
will return true.
Returns true if the given IP address is a class A IP address. ipaddress("10.0.0.1/24").is_class_a
will return true.
Inclusion of an IP address in a network IP range can be checked using the in
comparison. For example ipaddress("10.1.1.32") in ipaddress("10.1.1.0/24")
returns true.
Returns true if the given IP address is a class A IP address. ipaddress("172.16.10.1/24").is_class_b
will return true.
Returns true if the given IP address is a class A IP address. ipaddress("192.168.1.1/30").is_class_c
will return true.
Semver holds and parses a string as a Semantic version. This allows support of semantic versioning and checks.
Example
var mysql_version = semver("6.5.0") var web_server = semver("1.2.4-pre")
Returns the major part of the version number: semver("6.5.7").major
returns "6"
.
Returns the minor part of the version number: semver("6.5.7").major
returns "5"
.
Returns the patch part of the version number: semver("6.5.7").major
returns "7"
.
Returns the build part of the version number if available: semver("3.7.9-pre.1+revision.15723").major
returns "revision.15623"
.
Returns the pre part of the version number if available: semver("3.7.9-pre.1+revision.15723").major
returns "pre.1"
.
Checks if a semver satisfies a Pessimistic version comparison: semver("1.6.5").satisfies("~> 1.5")
returns true.
You can use <
, >
, =
, ==
, <=
, >=
and !=
comparisons between two semvers.
Image holds a Docker image path and lets you access its different parts. It also understands some of the particular attributes of docker images (like no registry name means DockerHub or no tag means latest).
For example, you can parse a string containing an image name to an Image
like this:
var i = image("quay.io/cloud66/mysql:5.6.1")
this will let you access the image name constituents:
i.registry
or i.tag
. The Image
type, combined with as
and pick
will make a powerful tool for inspecting images used in a configuration file.
Returns the registry name of the image. It will return index.docker.io
if no registry is available in the image name.
Returns the name of the image. It will append library/
to the beginning of the image name if no namespace is available on the image name (DockerHub image names). For example, ubuntu:1.2.3
will return library/ubuntu
as name
.
Returns the tag of the image. It will return latest
if no tag is available on the image. For example mysql
will return latest
as the tag
.
Returns the URL for the registry, including the scheme. For example, quay.io/ubuntu:1.2.3
returns https://quay.io
as registry_url
.
Returns the Fully Qualified Image Name. This includes the scheme. For example ubuntu
will return https://index.docker.io/library/ubuntu:latest
.
In most cases, values read from a configuration file are strings. In order for them to be usable with Copper DSL's complex data types, you can read them as different types using the as
function.
As
function takes in a type name which is a :
followed by the type name. For example to convert a string into a Semver
use :semver
in the as
function: "1.2.3".as(:semver)
Example
Here, we are assuming the value of the `mysql_version` variable is a string `"5.6.7"`:
mysql_version.as(:semver).satisfies("~> 5.6")
The full name of the configuration file used in a check is available in the Copper DSL as filename
. filename
is a Filename
data type with the following attributes:
Returns the path to the configuration file (excluding the filename). For example samples/test.yml
returns samples
.
Returns the filename of the configuration file (excluding the path). For example samples/test.yml
returns test
.
Returns the file extension of the configuration file (including the leading .
). For example samples/test.yml
returns .yml
.
Returns the full filename of the configuration file. For example samples/test.yml
returns samples/test.yml
.
Returns the full expanded file path of the configuration file. For example samples/test.yml
will return (depending on the absolute location of the file) something like /Users/john/projects/tests/kubernetes/samples/test.yml
.
Copper DSL uses JSONPath format to read values from a configuration file. For any configuration file format, the content is first read and converted in to JSON which makes it possible to use JSONPath to find nodes and attributes in the configuration file.
The fetch
function accepts the JSONPath and returns an array of all matching nodes and attributes in the configuration.
This is a YAML configuration file used in the following examples
apiVersion: v1 kind: Service metadata: namespace: foobar name: foo-svc annotations: cloud66.com/snapshot-uid: 123-456-789 cloud66.com/snapshot-gitref: abcd labels: app: foo tier: bar spec: type: NodePort ports: - port: 8080 targetPort: 8090 - port: 8100 targetPort: 8100 - port: 5000 selector: app: foo tier: bar
To fetch the value of type
under spec
(which is NodePort
in the file above), we can use the following JSONPath format:
fetch("$.spec.type") // will return ["NodePort"]
To return all the targetPort
values under spec.port
you can use attribute selectors:
fetch("$.spec.ports..targetPort") // will return [8090, 8100]
To return the value of targetPort
for the 8080
port (8090
in the example above) you can use the filters:
fetch("$.spect.ports[?(@.port == 8080)]") // will return [8090]
You can use the JSONPath reference as a syntax guideline. Copper DSL implements a subset of JSONPath, listed below. You can also use the Online JSONPath evaluator for testing or refer to the debugging section below:
Operation | Meaning |
---|---|
$ |
The root element to query. This starts all path expressions. |
@ |
The current node being processed by a filter predicate. |
* |
Wildcard. Available anywhere a name or numeric are required. |
.. |
Deep scan. Available anywhere a name is required. |
.<name> |
Dot-notated child |
['<name>' (, '<name>')] |
Bracket-notated child or children |
[<number> (, <number>)] |
Array index or indexes |
[start:end] |
Array slice operator |
[?(<expression>)]<name> |
Filter expression. Expression must evaluate to a boolean value. |
Operator | Description |
---|---|
== |
left is equal to right (note that 1 is not equal to '1') |
!= |
left is not equal to right |
< |
left is less than right |
<= |
left is less or equal to right |
> |
left is greater than right |
>= |
left is greater than or equal to right |
Another example
apiVersion: extensions/v1beta1 kind: Deployment metadata: namespace: foobar name: foo spec: template: metadata: labels: app: foo tier: bar spec: containers: - name: foo image: index.docker.io/library/ubuntu:latest ports: - containerPort: 8080 - name: mysql image: quay.io/mysql:2.3.0 ports: - containerPort: 3306 - name: buzz image: quay.io/pg:latest ports: - containerPort: 8080 imagePullSecrets: - name: registry-pull-secret
Get the image tag of all containers
fetch("$.spec.spec.containers..image").extract(".*:(.*)", 1) // returns ["latest", "2.3.0", "latest"]
Get the image name of the container named mysql
fetch("$.spec.spec.containers[?(@.name == 'mysql')].image") // will return ["quay.io/mysql:2.3.0"]
You can dump the results of variables and comparisons to the console using the -> console
directive.
$ copper check --rules rule.cop --file config.yml --debug
Example
Write the value of variable `mysql_version` to the console
rule foo warn { mysql_variable -> console }
Write the result of a comparison to the console
rule foo warn { fetch("$.spec.template.images").contain("ubuntu") -> console }
Write the result of a fetch
to console
rule foo warn { fetch("$.spec.ports..targetPort") -> console }