Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add schematobicep #8

Merged
merged 2 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions pkg/bicep/schematobicep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package bicep

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"

"github.com/massdriver-cloud/airlock/pkg/schema"
)

var indent string = " "

func SchemaToBicep(in io.Reader) ([]byte, error) {
inBytes, err := io.ReadAll(in)
if err != nil {
return nil, err
}

root := schema.Schema{}
err = json.Unmarshal(inBytes, &root)
if err != nil {
return nil, err
}

content := bytes.NewBuffer(nil)

flattenedProperties := schema.ExpandProperties(&root)
for prop := flattenedProperties.Oldest(); prop != nil; prop = prop.Next() {
err = createBicepParameter(prop.Key, prop.Value, content)
if err != nil {
return nil, err
}
}

return content.Bytes(), nil
}

func createBicepParameter(name string, sch *schema.Schema, buf *bytes.Buffer) error {
bicepType, err := getBicepTypeFromSchema(sch.Type)
if err != nil {
return err
}

writeDescription(sch, buf)
if allowParamErr := writeAllowedParams(sch, buf); allowParamErr != nil {
return allowParamErr
}
writeMinValue(sch, buf, bicepType)
writeMaxValue(sch, buf, bicepType)
writeMinLength(sch, buf, bicepType)
writeMaxLength(sch, buf, bicepType)
writeSecure(sch, buf, bicepType)
return writeBicepParam(name, sch, buf, bicepType)
}

func writeBicepParam(name string, sch *schema.Schema, buf *bytes.Buffer, bicepType string) error {
var defVal string

if sch.Default != nil {
renderedVal, err := renderBicep(sch.Default, "")
if err != nil {
return err
}

defVal = fmt.Sprintf(" = %s", renderedVal)
}

buf.WriteString(fmt.Sprintf("param %s %s%s\n", name, bicepType, defVal))
return nil
}

func renderBicep(val interface{}, prefix string) (string, error) {
switch reflect.TypeOf(val).Kind() {
case reflect.String:
return fmt.Sprintf("'%s'", val), nil
case reflect.Float64:
return fmt.Sprintf("%v", val), nil
case reflect.Bool:
return fmt.Sprintf("%v", val), nil
case reflect.Slice:
assertedVal, asserArrErr := val.([]interface{})
if asserArrErr != true {
return "", fmt.Errorf("unable to convert value into array: %v", val)
}

return parseArray(assertedVal, prefix)
case reflect.Map:
assertedVal, asserObjErr := val.(map[string]interface{})
if asserObjErr != true {
return "", fmt.Errorf("unable to convert value into object: %v", val)
}

return parseObject(assertedVal, prefix)
default:
return "", errors.New("unknown type: " + reflect.TypeOf(val).Kind().String())
}
}

func getBicepTypeFromSchema(schemaType string) (string, error) {
switch schemaType {
case "string":
return "string", nil
case "integer", "number":
return "int", nil
case "boolean":
return "bool", nil
case "object", "":
return "object", nil
case "array":
return "array", nil
default:
return "", errors.New("unknown type: " + schemaType)
}
}

func writeDescription(sch *schema.Schema, buf *bytes.Buffer) {
if sch.Description != "" {
// decorators are in sys namespace. to avoid potential collision with other parameters named "description", we use "sys.description" instead of just "description" https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/parameters#decorators
buf.WriteString(fmt.Sprintf("@sys.description('%s')\n", sch.Description))
}
}

func writeAllowedParams(sch *schema.Schema, buf *bytes.Buffer) error {
if sch.Enum != nil && len(sch.Enum) > 0 {
renderedVal, err := renderBicep(sch.Enum, "")
if err != nil {
return err
}

buf.WriteString(fmt.Sprintf("@allowed(%s)\n", renderedVal))
}
return nil
}

func writeMinValue(sch *schema.Schema, buf *bytes.Buffer, bicepType string) {
if bicepType == "int" && sch.Minimum != "" {
// set this to %v because sch.Minimum uses json.Number type
buf.WriteString(fmt.Sprintf("@minValue(%v)\n", sch.Minimum))
}
}

func writeMaxValue(sch *schema.Schema, buf *bytes.Buffer, bicepType string) {
if bicepType == "int" && sch.Maximum != "" {
buf.WriteString(fmt.Sprintf("@maxValue(%v)\n", sch.Maximum))
}
}

func writeMinLength(sch *schema.Schema, buf *bytes.Buffer, bicepType string) {
switch bicepType {
case "array":
if sch.MinItems != nil {
buf.WriteString(fmt.Sprintf("@minLength(%d)\n", *sch.MinItems))
}
case "string":
if sch.MinLength != nil {
buf.WriteString(fmt.Sprintf("@minLength(%d)\n", *sch.MinLength))
}
}
}

func writeMaxLength(sch *schema.Schema, buf *bytes.Buffer, bicepType string) {
switch bicepType {
case "array":
if sch.MaxItems != nil {
buf.WriteString(fmt.Sprintf("@maxLength(%d)\n", *sch.MaxItems))
}
case "string":
if sch.MaxLength != nil {
buf.WriteString(fmt.Sprintf("@maxLength(%d)\n", *sch.MaxLength))
}
}
}

func writeSecure(sch *schema.Schema, buf *bytes.Buffer, bicepType string) {
if bicepType == "string" && sch.Format == "password" {
buf.WriteString("@secure()\n")
}
}

func parseArray(arr []interface{}, prefix string) (string, error) {
parsedArr := "[\n"

for _, v := range arr {
renderedVal, err := renderBicep(v, prefix+indent)
if err != nil {
return "", err
}

parsedArr += fmt.Sprintf("%s%s", prefix+indent, renderedVal) + "\n"
}

parsedArr += fmt.Sprintf("%s]", prefix)
return parsedArr, nil
}

func parseObject(obj map[string]interface{}, prefix string) (string, error) {
parsedObj := "{\n"

for k, v := range obj {
renderedVal, err := renderBicep(v, prefix+indent)
if err != nil {
return "", err
}

parsedObj += fmt.Sprintf("%s%s: %s", prefix+indent, k, renderedVal) + "\n"
}

parsedObj += fmt.Sprintf("%s}", prefix)
return parsedObj, nil
}
42 changes: 42 additions & 0 deletions pkg/bicep/schematobicep_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package bicep_test

import (
"os"
"path/filepath"
"testing"

"github.com/massdriver-cloud/airlock/pkg/bicep"
)

func TestSchemaToBicep(t *testing.T) {
type testData struct {
name string
}
tests := []testData{
{
name: "simple",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
want, err := os.ReadFile(filepath.Join("testdata", tc.name+".bicep"))
if err != nil {
t.Fatalf("%d, unexpected error", err)
}

schemaFile, err := os.Open(filepath.Join("testdata", tc.name+".json"))
if err != nil {
t.Fatalf("%d, unexpected error", err)
}

got, err := bicep.SchemaToBicep(schemaFile)
if err != nil {
t.Fatalf("%d, unexpected error", err)
}

if string(got) != string(want) {
t.Fatalf("\ngot: %q\n want: %q", string(got), string(want))
}
})
}
}
127 changes: 127 additions & 0 deletions pkg/bicep/testdata/simple.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
param stringtest string
param integertest int
param numbertest int
param booltest bool
param arraytest array
param objecttest object
param nestedtest object
@allowed([
'foo'
'bar'
])
param enumtest string
@allowed([
1
2
])
param enumtestints int
@allowed([
true
false
])
param enumtestbools bool
@allowed([
[
'foo'
'bar'
]
[
'baz'
'qux'
]
])
param enumtestarrays array
@allowed([
{
foo: 'bar'
}
{
baz: 'qux'
}
])
param enumobjecttest object
@sys.description('This is a description')
param descriptiontest string
@sys.description('This is a new description')
@allowed([
'foo'
'bar'
'baz'
])
param descriptionenumtest string
@minValue(5)
param minvaluetest int
@maxValue(10)
param maxvaluetest int
@minValue(5)
@maxValue(10)
param minmaxvaluetest int
@minLength(5)
param minlengthstringtest string
@maxLength(10)
param maxlengthstringtest string
@minLength(5)
@maxLength(10)
param minmaxlengthstringtest string
@minLength(2)
param minlengtharraytest array
@maxLength(5)
param maxlengtharraytest array
@minLength(2)
@maxLength(5)
param minmaxlengtharraytest array
param defaultstringtest string = 'foo'
param defaultintegertest int = 5
param defaultbooltest bool = true
param defaultarraytest array = [
'foo'
'bar'
]
param defaultobjecttest object = {
bar: 'baz'
foo: 5
}
param defaultspaceobjecttest object = {
foo: 'bar baz'
lorem: 'ipsum'
}
param defaultarrayobjecttest array = [
{
bar: 'baz'
foo: 5
}
{
bar: 'qux'
foo: 10
}
]
param defaultnestedarraytest array = [
[
[
'foo'
]
[
'bar'
]
]
[
[
'baz'
]
[
'qux'
]
]
]
param defaultnestedobjecttest object = {
foo: {
bar: {
baz: 'qux'
}
}
quid: {
pro: 'quo'
}
}
@secure()
param securestringtest string
Loading