Skip to content

Commit

Permalink
add ability to generate chart schema.json (#413)
Browse files Browse the repository at this point in the history
* add ability to generate chart schema.json

* 7186 add schema version

* 7186 fixed schema version definition in tests

* Update codegen/render/funcs.go

Co-authored-by: Joshua Pritchard <[email protected]>

* 7186 removed unneeded header override

* 7186 removed unneeded header override part 2

* 7186 fixed version hard-coded

* 7186 fix goimports codegen issue

---------

Co-authored-by: Joshua Pritchard <[email protected]>
  • Loading branch information
Albert and josh-pritchard authored Feb 20, 2023
1 parent 40147fd commit defab31
Show file tree
Hide file tree
Showing 15 changed files with 18,576 additions and 17 deletions.
4 changes: 4 additions & 0 deletions changelog/v0.27.2/schema-json.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changelog:
- type: NEW_FEATURE
description: Adds ability to generate json schema for chart
issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/7186
133 changes: 133 additions & 0 deletions codegen/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"

goyaml "gopkg.in/yaml.v3"
v12 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand Down Expand Up @@ -243,6 +244,138 @@ var _ = Describe("Cmd", func() {
Expect(envMapField.HeadComment).To(Equal("# Specify environment variables for the container. See the [Kubernetes\n# documentation](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#envvarsource-v1-core)\n# for specification details."))
})

It("generates json schema for the values file", func() {
type CustomType1 struct {
Field1 string `json:"customField1"`
}
type CustomType2 struct {
Field2 string `json:"customField2"`
}
typeMapper := func(t reflect.Type, s map[string]interface{}) interface{} {
if t == reflect.TypeOf(CustomType2{}) {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"customField2_renamed": map[string]interface{}{
"type": "number",
},
},
}
}

return nil
}

cmd := &Command{
Groups: []Group{
{
GroupVersion: schema.GroupVersion{
Group: "things.test.io",
Version: "v1",
},
Module: "github.com/solo-io/skv2",
Resources: []Resource{
{
Kind: "Paint",
Spec: Field{Type: Type{Name: "PaintSpec"}},
Status: &Field{Type: Type{Name: "PaintStatus"}},
},
{
Kind: "ClusterResource",
Spec: Field{Type: Type{Name: "ClusterResourceSpec"}},
ClusterScoped: true,
},
},
RenderManifests: true,
RenderTypes: true,
RenderClients: true,
RenderController: true,
MockgenDirective: true,
ApiRoot: "codegen/test/api",
CustomTemplates: contrib.AllGroupCustomTemplates,
},
},
AnyVendorConfig: skv2Imports,
RenderProtos: true,

Chart: &Chart{
Operators: []Operator{
{
Name: "painter",
Deployment: Deployment{
Container: Container{
Image: Image{
Tag: "v0.0.0",
Repository: "painter",
Registry: "quay.io/solo-io",
PullPolicy: "IfNotPresent",
},
Args: []string{"foo"},
},
},
Values: &CustomType2{},
},
},
Values: &CustomType1{},
ValuesInlineDocs: &ValuesInlineDocs{
LineLengthLimit: 80,
},
// TODO here we should also test the custom type mappings ...
JsonSchema: &JsonSchema{
CustomTypeMapper: typeMapper,
},
Data: Data{
ApiVersion: "v1",
Description: "",
Name: "Painting Operator",
Version: "v0.0.1",
Home: "https://docs.solo.io/skv2/latest",
Sources: []string{
"https://github.com/solo-io/skv2",
},
},
},

ManifestRoot: "codegen/test/chart",
}

err := cmd.Execute()
Expect(err).NotTo(HaveOccurred())

fileContents, err := os.ReadFile("codegen/test/chart/values.schema.json")
Expect(err).NotTo(HaveOccurred())

type jsonSchema struct {
Properties *struct {
CustomField1 map[string]interface{} `json:"customField1"`
Painter *struct {
Properties map[string]interface{} `json:"properties"`
} `json:"painter"`
} `json:"properties"`
}

schema := jsonSchema{}
Expect(json.Unmarshal(fileContents, &schema)).NotTo(HaveOccurred())

// expect that the custom values are in the schema
Expect(schema.Properties.CustomField1).NotTo(BeNil())
Expect(schema.Properties.CustomField1["type"]).To(Equal("string"))

// expect that the values from the painter opeartor are in the schema
painter := schema.Properties.Painter
Expect(painter).NotTo(BeNil())

// expect painterSchema has some of the base properities
painterProperties := painter.Properties
Expect(painterProperties).To(HaveKey("image"))
Expect(painterProperties).To(HaveKey("env"))
Expect(painterProperties).To(HaveKey("sidecars"))
Expect(painterProperties).To(HaveKey("securityContext"))

// expect painter schema also contains properties from the custom values
Expect(painterProperties).To(HaveKey("customField2_renamed"))
})

DescribeTable("configuring the runAsUser value",
func(floatingUserId bool, runAsUser, expectedRunAsUser int) {
cmd := &Command{
Expand Down
18 changes: 18 additions & 0 deletions codegen/model/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package model

import (
"fmt"
"reflect"

"github.com/solo-io/skv2/codegen/model/values"

Expand Down Expand Up @@ -36,6 +37,10 @@ type Chart struct {

// if specificed, generate inline documentation for the values in chart's values.yaml files
ValuesInlineDocs *ValuesInlineDocs

// if specificed, values.schema.json will be generated with a JSON Schema that
// imposes structure on the values.yaml file
JsonSchema *JsonSchema
}

type ValuesReferenceDocs struct {
Expand All @@ -48,6 +53,15 @@ type ValuesInlineDocs struct {
LineLengthLimit int
}

type JsonSchema struct {
// (Optional) will be called to override the default json schema mapping
// for the type. This is useful for types that also override default json/yaml
// serialization behaviour. It accepts the json schema as a map and is
// expected to return a value that can serialize to the json schema or null if
// there is no custom mapping for this type
CustomTypeMapper func(reflect.Type, map[string]interface{}) interface{}
}

type Operator struct {
Name string

Expand Down Expand Up @@ -182,6 +196,10 @@ func (c Chart) BuildChartValues() values.UserHelmValues {
}
}

if c.JsonSchema != nil {
helmValues.JsonSchema.CustomTypeMapper = c.JsonSchema.CustomTypeMapper
}

return helmValues
}

Expand Down
7 changes: 7 additions & 0 deletions codegen/model/values/helm_chart_values.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package values

import (
"reflect"

appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
)
Expand All @@ -10,6 +12,7 @@ type UserHelmValues struct {
Operators []UserOperatorValues
CustomValues interface{}
ValuesInlineDocs *UserValuesInlineDocs
JsonSchema UserJsonSchema
}

type UserOperatorValues struct {
Expand All @@ -34,6 +37,10 @@ func (u *UserValuesInlineDocs) LineLength() int {
return u.LineLengthLimit
}

type UserJsonSchema struct {
CustomTypeMapper func(reflect.Type, map[string]interface{}) interface{}
}

// document values structure for an operator
type UserValues struct {
UserContainerValues `json:",inline"`
Expand Down
6 changes: 6 additions & 0 deletions codegen/render/chart_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func (r ChartRenderer) Render(chart model.Chart) ([]OutFile, error) {
templatesToRender = chartInputsNoOperators
}

if chart.JsonSchema != nil {
templatesToRender["chart/values.schema.jsontmpl"] = OutFile{
Path: "values.schema.json",
}
}

files, err := r.renderCoreTemplates(templatesToRender, chart)
if err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion codegen/render/export_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package render

import goyaml "gopkg.in/yaml.v3"
import (
"github.com/solo-io/skv2/codegen/model/values"
goyaml "gopkg.in/yaml.v3"
)

// this file makes some private package members visible for testing

Expand All @@ -12,6 +15,10 @@ func FromNode(n goyaml.Node) string {
return fromNode(n)
}

func ToJSONSchema(values values.UserHelmValues) string {
return toJSONSchema(values)
}

func MergeNodes(nodes ...goyaml.Node) goyaml.Node {
return mergeNodes(nodes...)
}
Loading

0 comments on commit defab31

Please sign in to comment.