Skip to content

Commit

Permalink
Add the rest of the README
Browse files Browse the repository at this point in the history
  • Loading branch information
AislingHPE committed Aug 23, 2024
1 parent f9bd129 commit b809bfe
Showing 1 changed file with 219 additions and 2 deletions.
221 changes: 219 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,226 @@ 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
}

```

### 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 of the types used by Terraform currently are supported here. Here is how each of them is represented. See [Terraform Input Variables](https://developer.hashicorp.com/terraform/language/values/variables#type-constraints).

#### 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"
},
```

### 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.

0 comments on commit b809bfe

Please sign in to comment.