Skip to content

Commit

Permalink
Add the rest of the README (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
AislingHPE authored Aug 23, 2024
1 parent f9bd129 commit 522e88f
Showing 1 changed file with 222 additions and 3 deletions.
225 changes: 222 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ do not directly support (see Custom Validation Rules).
The default behaviour of TerraSchema is to scan the current directory for Terraform
configuration files, and create a file called `schema.json` at the same location. It
returns an error if no Terraform configuration files are present, or if no variables
are defined within those files. Variable are marked as `required` in the Schema only
are defined within those files. Variable are marked as `required` in the schema only
if they don't have a default value set, and additional variables are permitted.

Note: an `input.tfvars.json` file with additional variables (ones which don't correspond
Expand Down Expand Up @@ -75,9 +75,228 @@ running Terraform.
# Design

### Parsing Terraform Configuration Files
Parsing Terraform files is done using the [HCL package](https://github.com/hashicorp/hcl). Initially, the plan was to use an existing application such as [terraform-docs](https://github.com/terraform-docs/terraform-docs/) to preform the parsing step, but some of the fields of the `variable` block weren't implemented, such as validation rules.

### Custom Validation Rules
TerraSchema parses each terraform configuration file as a HCL (HashiCorp Configuration Language) file and picks out any blocks which match the definition of an input variable in Terraform. A typical `variable` block looks like this:

```hcl
variable "age" {
type = number
default = 10
description = "Your age"
nullable = false
sensitive = false
validation {
condition = var.age >= 0
error_message = "Age must not be negative"
}
}
```

Note: All of these fields are optional.

This `variable` is translated into the following format in the `reader` package, so that it can be used by the rest of the application:

```Go
type VariableBlock struct {
Type hcl.Expression // or nil
Default hcl.Expression // or nil
Description *string
Nullable *bool
Sensitive *bool
Validation *struct{
Condition hcl.Expression
ErrorMessage string
}
}
```

Empty expressions (such as `Type` and `Default`) are filtered out by the `reader` package after unmarshalling the `variable` block by setting them to nil.

This struct is then passed to the JSON Schema package so that it can create a schema based on these variable definitions.

Here is an example schema generate from a module with only the variable listed above. More examples of generated schema files can be found in the `test` folder.

```JSON
{
"$schema": "http://json-schema.org/draft-07/schema#",
// can be overridden with `--disallow-additional-properties`
"additionalProperties": true,
"properties": {
"age": {
"description": "Your age",
"default": 10,
"minimum": 0,
"type": "number",
},
},
"required": [] // only variables without a default are required, unless `--require-all` is set
}

```

### Translating Types to JSON Schema
Translation of types to Terraform is done in 2 steps. The first step is to take the `hcl.Expression` for the type from the `VariableBlock` struct, and use [go-cty](https://github.com/zclconf/go-cty/) to convert it to a 'type constraint', which is a JSON blob representing all the information about the type in a more machine-readable format.

The second phase is taking that type information and converting it to a JSON Schema definition. All types used by Terraform currently are supported here. Here is how each of them is represented. Also see [Terraform Input Variables](https://developer.hashicorp.com/terraform/language/values/variables#type-constraints) for more information on Terraform input variable types.

#### string
```json
{
"type": "string"
}
```

#### number
```json
{
"type": "number"
}
```

#### bool
```json
{
"type": "boolean"
}
```

#### list(\<TYPE>)
```json
{
"type": "array",
"items": {
"type": "<TYPE>"
}
}
```

#### set(\<TYPE>)
```json
{
"type": "array",
"items": {
"type": "<TYPE>"
},
"uniqueItems": true
}
```

#### map(\<TYPE>)
```json
{
"type": "object",
"additionalProperties": {
"type": "<TYPE>"
}
}
```

#### object({\<NAME> = \<TYPE>,... })
```json
{
// can be overridden with `--disallow-additional-properties`
"additionalProperties": true,
"type": "object",
"properties": {
"<NAME>": {
"type": "<TYPE>"
},
...
},
"required": [
"<NAME>",
...
]
}
```

#### tuple(\<TYPE 1>, ... \<TYPE N>)
```json
{
"type": "array",
"items": [
{
"type": "<TYPE 1>"
},
...
{
"type": "<TYPE N>"
}
],
"minItems": N,
"maxItems": N
}
```

Additionally, any nesting of these types is also valid, and will create a schema according to these rules.

---

Issue: [Optional Type Attributes](https://developer.hashicorp.com/terraform/language/expressions/type-constraints#optional-object-type-attributes) are not fully supported by go-cty (as of v1.15.0), and the program will error if it encounters a type of the form

```hcl
type = optional(<TYPE>,<DEFAULT-VALUE>)
```
with the following error:

```
Invalid type specification; Optional attribute modifier expects only one argument: the attribute type.
```

Optional declarations of the form `optional(<TYPE>)` are supported.

### Custom Validation Rules

A subset of common validation patterns have been implemented. If a validation rule is present and can't be converted to an existing rule, then the application will print a warning. The current list of valid validation rules for a variable with the name `name` is as follows:

| Condition | Variable Type | JSON Output |
| -------------------------------------------------------- | ---------------------- | -------------------------------------------------- |
| **Enum conditions** | | |
| `var.name == 1 \|\| 2 == var.name \|\| ...` | any | `{"enum": [1, 2, ...]}` |
| `contains([1,2,...], var.name)` | any | `{"enum": [1, 2, ...]}` |
| **Regex conditions** | | |
| `can(regex("<pattern>", var.name))` | `string` | `{"pattern": "<pattern>"}` |
| **Number value comparison conditions** | | |
| `var.name < 10 && var.name > 0 && ...` | `number` | `{"exclusiveMinimum": 0", "exclusiveMaximum": 10}` |
| `var.name <= 10 && var.name >= 0 && ...` | `number` | `{"minimum": 0, "maximum": 10"}` |
| **String length comparison conditions** | | |
| `length(var.name) < 10 && length(var.name) > 0 && ...` | `string` | `{"minLength": 1,"maxLength": 9}` |
| `length(var.name) <= 10 && length(var.name) >= 0 && ...` | `string` | `{"minLength": 0, "maxLength": 10, }` |
| `length(var.name) == 5 && ...` | `string` | `{"minLength": 5, "maxLength": 5"}` |
| **Object length comparison conditions** | | |
| `length(var.name) < 10 && length(var.name) > 0 && ...` | `map`, `object` | `{"minProperties": 1,"maxProperties": 9}` |
| `length(var.name) <= 10 && length(var.name) >= 0 && ...` | `map`, `object` | `{"minProperties": 0, "maxProperties": 10, }` |
| `length(var.name) == 5 && ...` | `map`, `object` | `{"minProperties": 5, "maxProperties": 5"}` |
| **Array length comparison conditions** | | |
| `length(var.name) < 10 && length(var.name) > 0 && ...` | `list`, `tuple`, `set` | `{"minItems": 1,"maxItems": 9}` |
| `length(var.name) <= 10 && length(var.name) >= 0 && ...` | `list`, `tuple`, `set` | `{"minItems": 0, "maxItems": 10, }` |
| `length(var.name) == 5 && ...` | `list`, `tuple`, `set` | `{"minItems": 5, "maxItems": 5"}` |

### Nullable Variables

If `nullable` is true in the `variable` block, then the JSON Schema will be modified to look like this. This method is primarily chosen for compatibility with react-jsonschema-form.

```JSON
"<NAME>": {
"anyOf": [
{
"title": "null",
"type": "null"
},
{
"title": "<TYPE>",
"type": "<TYPE>"
}
],
"description": "<DESCRIPTION>",
"default": "<DEFAULT>",
"title": "<NAME>: Select a type"
},
```

This is actually a slight behaviour change from the validator used by terraform. If `nullable` is unset, then terraform treats them as `nullable` by default. I chose not to implement that behaviour here and instead am making the Terraform module author specify `nullable = true`. This is because otherwise schema definitions for simple programs would have to become a lot more verbose just to handle this case.

### Default Handling

### Nullable Variables
Default handling is relatively straightforward. The default specified in Terraform is rendered to a JSON object, and added to the default field in the JSON Schema. Type checking is not performed on the default value. This is in line with how the JSON Schema creators generally expect this field to be used. See their notes on [annotations](https://json-schema.org/understanding-json-schema/reference/annotations#:~:text=The%20default%20keyword%20specifies%20a%20default%20value.).

0 comments on commit 522e88f

Please sign in to comment.