From 6d9d7a2f3717fa68dbdc1644ba0fc03988b60b8a Mon Sep 17 00:00:00 2001 From: Jacob Colvin Date: Wed, 8 Jan 2025 21:51:28 -0500 Subject: [PATCH] Generate helm KCL schemas with go generate --- Taskfile.yaml | 7 + cmd/gen/main.go | 53 +++++ gen.go | 3 + modules/helm/chart.k | 23 +++ modules/helm/chart_base.k | 38 ++++ modules/helm/chart_config.k | 20 ++ modules/helm/main.k | 73 ------- modules/helm/main_test.k | 3 - pkg/helmmodels/chart.go | 124 ------------ pkg/helmmodels/chartmodule/chart.go | 226 ++++++++++++++++++++++ pkg/helmmodels/chartmodule/chart_test.go | 33 ++++ pkg/helmmodels/pluginmodule/chart.go | 159 +++++++++++++++ pkg/helmmodels/pluginmodule/chart_test.go | 40 ++++ pkg/helmutil/add.go | 21 +- pkg/helmutil/add_test.go | 12 +- pkg/helmutil/set.go | 2 +- pkg/helmutil/update.go | 2 +- pkg/jsonschema/jsonschema_kcl.go | 2 +- pkg/jsonschema/reflector.go | 39 +++- 19 files changed, 650 insertions(+), 230 deletions(-) create mode 100644 cmd/gen/main.go create mode 100644 gen.go create mode 100644 modules/helm/chart.k create mode 100644 modules/helm/chart_base.k create mode 100644 modules/helm/chart_config.k delete mode 100644 pkg/helmmodels/chart.go create mode 100644 pkg/helmmodels/chartmodule/chart.go create mode 100644 pkg/helmmodels/chartmodule/chart_test.go create mode 100644 pkg/helmmodels/pluginmodule/chart.go create mode 100644 pkg/helmmodels/pluginmodule/chart_test.go diff --git a/Taskfile.yaml b/Taskfile.yaml index 32a1639..2c254a1 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -93,6 +93,13 @@ tasks: export CC=$CC_{{.C_ENV}} CXX=$CXX_{{.C_ENV}} go test -ldflags="-s -w" -bench=. -benchmem -tags={{.BUILD_TAGS}} {{.FLAGS}} {{.PKG}} + go-gen: + desc: Generates Go code + cmds: + - | + export CC=$CC_{{.C_ENV}} CXX=$CXX_{{.C_ENV}} + go generate ./... + go-build: desc: Builds Go binaries vars: diff --git a/cmd/gen/main.go b/cmd/gen/main.go new file mode 100644 index 0000000..c0d44e3 --- /dev/null +++ b/cmd/gen/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/MacroPower/kclipper/pkg/helmmodels/pluginmodule" +) + +func main() { + basePath := "modules" + if err := generate(basePath); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func generate(path string) error { + modPath := filepath.Join(path, "helm") + + fcb, err := os.Create(filepath.Join(modPath, "chart_base.k")) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer fcb.Close() + pcb := &pluginmodule.ChartBase{} + if err = pcb.GenerateKCL(fcb); err != nil { + return fmt.Errorf("failed to generate KCL: %w", err) + } + + fcc, err := os.Create(filepath.Join(modPath, "chart_config.k")) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer fcc.Close() + pcc := &pluginmodule.ChartConfig{} + if err = pcc.GenerateKCL(fcc); err != nil { + return fmt.Errorf("failed to generate KCL: %w", err) + } + + fc, err := os.Create(filepath.Join(modPath, "chart.k")) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer fc.Close() + pc := &pluginmodule.Chart{} + if err = pc.GenerateKCL(fc); err != nil { + return fmt.Errorf("failed to generate KCL: %w", err) + } + + return nil +} diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..fe64d56 --- /dev/null +++ b/gen.go @@ -0,0 +1,3 @@ +package main + +//go:generate go run -tags=netgo cmd/gen/main.go diff --git a/modules/helm/chart.k b/modules/helm/chart.k new file mode 100644 index 0000000..ed81bde --- /dev/null +++ b/modules/helm/chart.k @@ -0,0 +1,23 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +schema Chart(ChartBase): + r""" + Defines a Helm chart. + + Attributes + ---------- + values : any, optional + Helm values to be passed to Helm template. These take precedence over valueFiles. + valueFiles : [str], optional + Helm value files to be passed to Helm template. + postRenderer : ({str:}) -> {str:}, optional + Lambda function to modify the Helm template output. Evaluated for each resource in the Helm template output. + """ + + values?: any + valueFiles?: [str] + postRenderer?: ({str:}) -> {str:} + diff --git a/modules/helm/chart_base.k b/modules/helm/chart_base.k new file mode 100644 index 0000000..cd55783 --- /dev/null +++ b/modules/helm/chart_base.k @@ -0,0 +1,38 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +schema ChartBase: + r""" + Represents attributes common in `helm.Chart` and `helm.ChartConfig`. + + Attributes + ---------- + chart : str, required + Helm chart name. + repoURL : str, required + URL of the Helm chart repository. + targetRevision : str, required + Semver tag for the chart's version. + releaseName : str, optional + Helm release name to use. If omitted the chart name will be used. + namespace : str, optional + Optional namespace to template with. + skipCRDs : bool, optional + Set to `True` to skip the custom resource definition installation step (Helm's `--skip-crds`). + passCredentials : bool, optional + Set to `True` to pass credentials to all domains (Helm's `--pass-credentials`). + schemaValidator : "KCL" | "HELM", optional + Validator to use for the Values schema. + """ + + chart: str + repoURL: str + targetRevision: str + releaseName?: str + namespace?: str + skipCRDs?: bool + passCredentials?: bool + schemaValidator?: "KCL" | "HELM" + diff --git a/modules/helm/chart_config.k b/modules/helm/chart_config.k new file mode 100644 index 0000000..b62eb24 --- /dev/null +++ b/modules/helm/chart_config.k @@ -0,0 +1,20 @@ +""" +This file was generated by the KCL auto-gen tool. DO NOT EDIT. +Editing this file might prove futile when you re-run the KCL auto-gen generate command. +""" + +schema ChartConfig(ChartBase): + r""" + Configuration that can be defined in `charts.k`, in addition to those specified in `helm.ChartBase`. + + Attributes + ---------- + schemaGenerator : "AUTO" | "VALUE-INFERENCE" | "URL" | "CHART-PATH" | "LOCAL-PATH" | "NONE", optional + Schema generator to use for the Values schema. + schemaPath : str, optional + Path to the schema to use, when relevant for the selected schemaGenerator. + """ + + schemaGenerator?: "AUTO" | "VALUE-INFERENCE" | "URL" | "CHART-PATH" | "LOCAL-PATH" | "NONE" + schemaPath?: str + diff --git a/modules/helm/main.k b/modules/helm/main.k index fc6cf32..b782d42 100644 --- a/modules/helm/main.k +++ b/modules/helm/main.k @@ -1,80 +1,10 @@ """ This module provides an interface for the kclipper Helm plugin. """ -import regex import file import yaml import kcl_plugin.helm as helm_plugin -schema ChartBase: - r"""Helm chart resource. - - Attributes - ---------- - chart: str - The Helm chart name. - repoURL: str - The URL of the Helm chart repository. - targetRevision: str - TargetRevision defines the semver tag for the chart's version. - releaseName: str, optional. - The Helm release name to use. If omitted it will use the chart name. - namespace: str, optional. - Namespace is an optional namespace to template with. - skipCRDs: bool, default is False, optional. - Set to `True` to skip the custom resource definition installation step - (Helm's `--skip-crds`). - passCredentials: bool, default is False, optional. - Set to `True` to pass credentials to all domains (Helm's `--pass-credentials`). - schemaValidator : "KCL" | "HELM", default is "KCL", optional. - The schema validator to use. - """ - chart: str - repoURL: str - targetRevision: str - releaseName?: str - namespace?: str - skipCRDs?: bool = False - passCredentials?: bool = False - schemaValidator?: "KCL" | "HELM" - - check: - not regex.match(repoURL, r"^oci://"), \ - "Invalid repoURL: ${repoURL}. OCI registries must not include a scheme (e.g. `oci://`)" - -schema Chart(ChartBase): - """Helm chart resource. - - Attributes - ---------- - values: any, default is {}, optional. - Specifies Helm values to be passed to Helm template. These take precedence over valueFiles. - valueFiles: [str], default is [], optional. - Specifies Helm value files to be passed to Helm template. - preRenderer: (Chart) -> Chart, optional. - Lambda function to modify Chart before rendering the Helm template. - postRenderer: ({str:}) -> {str:}, optional. - Lambda function to modify the Helm template output. Evaluated for each resource in the Helm template output. - """ - values?: any = {} - valueFiles?: [str] = [] - preRenderer?: (Chart) -> Chart - postRenderer?: ({str:}) -> {str:} - -schema ChartConfig(ChartBase): - r""" - Helm Chart Configuration - - Attributes - ---------- - schemaGenerator : "AUTO" | "VALUE-INFERENCE" | "URL" | "CHART-PATH" | "LOCAL-PATH" | "NONE", optional, default is "AUTO" - The generator to use for the Values schema. - schemaPath : str, optional. - The path to the JSON Schema to use when schemaGenerator is "URL", "CHART-PATH", or "LOCAL-PATH". - """ - schemaGenerator?: "AUTO" | "VALUE-INFERENCE" | "URL" | "CHART-PATH" | "LOCAL-PATH" | "NONE" - schemaPath?: str - type Charts = {str:ChartConfig} template = lambda chart: Chart -> [{str:}] { @@ -97,9 +27,6 @@ template = lambda chart: Chart -> [{str:}] { _chart = chart _values: {str:} = {} - if chart.preRenderer: - _chart = chart.preRenderer(_chart) - if _chart.valueFiles and len(_chart.valueFiles) > 0: _values = { k: v diff --git a/modules/helm/main_test.k b/modules/helm/main_test.k index 3e5c23c..c84660a 100644 --- a/modules/helm/main_test.k +++ b/modules/helm/main_test.k @@ -15,9 +15,6 @@ test_Chart = lambda { chart = "test-oci" repoURL = "example.com" targetRevision = "0.1.0" - preRenderer = lambda c: Chart { - c - } postRenderer = lambda r: {str:} { r } diff --git a/pkg/helmmodels/chart.go b/pkg/helmmodels/chart.go deleted file mode 100644 index d2b537c..0000000 --- a/pkg/helmmodels/chart.go +++ /dev/null @@ -1,124 +0,0 @@ -package helmmodels - -import ( - "bytes" - "fmt" - - "github.com/iancoleman/strcase" - "kcl-lang.io/kcl-go/pkg/tools/gen" - - "github.com/MacroPower/kclipper/pkg/jsonschema" - "github.com/MacroPower/kclipper/pkg/kclutil" -) - -type ChartData struct { - Charts map[string]ChartConfig `json:"charts"` -} - -// ChartBase represents the KCL schema `helm.ChartBase`. -type ChartBase struct { - // Chart is the Helm chart name. - Chart string `json:"chart" jsonschema:"description=The Helm chart name."` - // RepoURL is the URL of the Helm chart repository. - RepoURL string `json:"repoURL" jsonschema:"description=The URL of the Helm chart repository."` - // TargetRevision is the semver tag for the chart's version. - TargetRevision string `json:"targetRevision" jsonschema:"description=The semver tag for the chart's version."` - // ReleaseName is the Helm release name to use. If omitted it will use the chart name. - ReleaseName string `json:"releaseName,omitempty" jsonschema:"-,description=The Helm release name to use. If omitted it will use the chart name."` - // SkipCRDs will skip the custom resource definition installation step (--skip-crds). - SkipCRDs bool `json:"skipCRDs,omitempty" jsonschema:"-,description=Skip the custom resource definition installation step."` - // PassCredentials will pass credentials to all domains (--pass-credentials). - PassCredentials bool `json:"passCredentials,omitempty" jsonschema:"-,description=Pass credentials to all domains."` - // SchemaValidator is the validator to use for the Values schema. - SchemaValidator jsonschema.ValidatorType `json:"schemaValidator,omitempty" jsonschema:"-,description=The validator to use for the Values schema."` -} - -type ChartConfig struct { - ChartBase - // SchemaGenerator is the generator to use for the Values schema. - SchemaGenerator jsonschema.GeneratorType `json:"schemaGenerator,omitempty" jsonschema:"-,description=The generator to use for the Values schema."` - // SchemaPath is the path to the schema to use. - SchemaPath string `json:"schemaPath,omitempty" jsonschema:"description=The path to the JSONSchema to use when schemaGenerator = URL or PATH or LOCAL-PATH."` -} - -func (c *ChartConfig) GetSnakeCaseName() string { - return strcase.ToSnake(c.Chart) -} - -func (c *ChartConfig) GenerateKCL(b *bytes.Buffer) error { - r := jsonschema.NewReflector() - js := r.Reflect(&ChartConfig{}) - if cv, ok := js.Properties.Get("schemaPath"); ok { - cv.Default = c.SchemaPath - } - if cv, ok := js.Properties.Get("schemaGenerator"); ok { - if c.SchemaGenerator != "" { - cv.Default = c.SchemaGenerator - } - cv.Enum = jsonschema.GeneratorTypeEnum - } - if cv, ok := js.Properties.Get("schemaValidator"); ok { - if c.SchemaValidator != "" { - cv.Default = c.SchemaValidator - } - cv.Enum = jsonschema.ValidatorTypeEnum - } - - jsBytes, err := js.MarshalJSON() - if err != nil { - return fmt.Errorf("failed to marshal json schema: %w", err) - } - - if err := kclutil.Gen.GenKcl(b, "settings", jsBytes, &gen.GenKclOptions{ - Mode: gen.ModeJsonSchema, - CastingOption: gen.OriginalName, - }); err != nil { - return fmt.Errorf("failed to generate kcl schema: %w", err) - } - - return nil -} - -type Chart struct { - ChartBase - // Values is the values to use for the chart. - Values any `json:"values,omitempty" jsonschema:"description=The values to use for the chart."` -} - -func (c *Chart) GetSnakeCaseName() string { - return strcase.ToSnake(c.Chart) -} - -func (c *Chart) GenerateKCL(b *bytes.Buffer) error { - r := jsonschema.NewReflector() - js := r.Reflect(&Chart{}) - if cv, ok := js.Properties.Get("chart"); ok { - cv.Default = c.Chart - } - if cv, ok := js.Properties.Get("repoURL"); ok { - cv.Default = c.RepoURL - } - if cv, ok := js.Properties.Get("targetRevision"); ok { - cv.Default = c.TargetRevision - } - if cv, ok := js.Properties.Get("schemaValidator"); ok { - if c.SchemaValidator != "" { - cv.Default = c.SchemaValidator - } - cv.Enum = jsonschema.ValidatorTypeEnum - } - - jsBytes, err := js.MarshalJSON() - if err != nil { - return fmt.Errorf("failed to marshal json schema: %w", err) - } - - if err := kclutil.Gen.GenKcl(b, "chart", jsBytes, &gen.GenKclOptions{ - Mode: gen.ModeJsonSchema, - CastingOption: gen.OriginalName, - }); err != nil { - return fmt.Errorf("failed to generate kcl schema: %w", err) - } - - return nil -} diff --git a/pkg/helmmodels/chartmodule/chart.go b/pkg/helmmodels/chartmodule/chart.go new file mode 100644 index 0000000..81fdd34 --- /dev/null +++ b/pkg/helmmodels/chartmodule/chart.go @@ -0,0 +1,226 @@ +package chartmodule + +import ( + "bufio" + "bytes" + "fmt" + "io" + "reflect" + "regexp" + + "github.com/iancoleman/strcase" + + "github.com/MacroPower/kclipper/pkg/helmmodels/pluginmodule" + "github.com/MacroPower/kclipper/pkg/jsonschema" +) + +var ( + SchemaDefinitionRegexp = regexp.MustCompile(`schema\s+(\S+):\s*`) + SchemaValuesRegexp = regexp.MustCompile(`(\s+values\??\s*:\s+)(.*)`) +) + +type ChartData struct { + Charts map[string]ChartConfig `json:"charts"` +} + +type ( + ChartBase pluginmodule.ChartBase + HelmChartConfig pluginmodule.ChartConfig + HelmChart pluginmodule.Chart +) + +// All possible chart configuration that can be defined in `charts.k`, +// inheriting from `helm.ChartConfig(helm.ChartBase)`. +type ChartConfig struct { + ChartBase `json:",inline"` + HelmChartConfig `json:",inline"` +} + +func (c *ChartConfig) GetSnakeCaseName() string { + return strcase.ToSnake(c.Chart) +} + +func (c *ChartConfig) GenerateKCL(w io.Writer) error { + r, err := newSchemaReflector() + if err != nil { + return fmt.Errorf("failed to create schema reflector: %w", err) + } + js := r.Reflect(reflect.TypeOf(ChartConfig{})) + if cv, ok := js.Properties.Get("chart"); ok { + cv.Default = c.ChartBase.Chart + } + if cv, ok := js.Properties.Get("repoURL"); ok { + cv.Default = c.ChartBase.RepoURL + } + if cv, ok := js.Properties.Get("targetRevision"); ok { + cv.Default = c.ChartBase.TargetRevision + } + if cv, ok := js.Properties.Get("namespace"); ok { + if c.Namespace != "" { + cv.Default = c.ChartBase.Namespace + } else { + js.Properties.Delete("namespace") + } + } + if cv, ok := js.Properties.Get("releaseName"); ok { + if c.ReleaseName != "" { + cv.Default = c.ChartBase.ReleaseName + } else { + js.Properties.Delete("releaseName") + } + } + if cv, ok := js.Properties.Get("skipCRDs"); ok { + if c.SkipCRDs { + cv.Default = c.ChartBase.SkipCRDs + } else { + js.Properties.Delete("skipCRDs") + } + } + if cv, ok := js.Properties.Get("passCredentials"); ok { + if c.PassCredentials { + cv.Default = c.ChartBase.PassCredentials + } else { + js.Properties.Delete("passCredentials") + } + } + if cv, ok := js.Properties.Get("schemaValidator"); ok { + if c.ChartBase.SchemaValidator != jsonschema.DefaultValidatorType { + cv.Default = c.ChartBase.SchemaValidator + cv.Enum = jsonschema.ValidatorTypeEnum + } else { + js.Properties.Delete("schemaValidator") + } + } + if cv, ok := js.Properties.Get("schemaPath"); ok { + if c.HelmChartConfig.SchemaPath != "" { + cv.Default = c.HelmChartConfig.SchemaPath + } else { + js.Properties.Delete("schemaPath") + } + } + if cv, ok := js.Properties.Get("schemaGenerator"); ok { + if c.HelmChartConfig.SchemaGenerator != jsonschema.DefaultGeneratorType { + cv.Default = c.HelmChartConfig.SchemaGenerator + cv.Enum = jsonschema.GeneratorTypeEnum + } else { + js.Properties.Delete("schemaGenerator") + } + } + + err = jsonschema.ReflectedSchemaToKCL(js, w) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) + } + + return nil +} + +// All possible chart configuration, inheriting from `helm.Chart(helm.ChartBase)`. +type Chart struct { + ChartBase `json:",inline"` + HelmChart `json:",inline"` +} + +func (c *Chart) GetSnakeCaseName() string { + return strcase.ToSnake(c.ChartBase.Chart) +} + +func (c *Chart) GenerateKCL(w io.Writer) error { + r, err := newSchemaReflector() + if err != nil { + return fmt.Errorf("failed to create schema reflector: %w", err) + } + js := r.Reflect(reflect.TypeOf(Chart{})) + js.Description = "All possible chart configuration, inheriting from `helm.Chart(helm.ChartBase)`." + if cv, ok := js.Properties.Get("chart"); ok { + cv.Default = c.ChartBase.Chart + } + if cv, ok := js.Properties.Get("repoURL"); ok { + cv.Default = c.ChartBase.RepoURL + } + if cv, ok := js.Properties.Get("targetRevision"); ok { + cv.Default = c.ChartBase.TargetRevision + } + if cv, ok := js.Properties.Get("namespace"); ok { + if c.Namespace != "" { + cv.Default = c.ChartBase.Namespace + } else { + js.Properties.Delete("namespace") + } + } + if cv, ok := js.Properties.Get("releaseName"); ok { + if c.ReleaseName != "" { + cv.Default = c.ChartBase.ReleaseName + } else { + js.Properties.Delete("releaseName") + } + } + if cv, ok := js.Properties.Get("skipCRDs"); ok { + if c.SkipCRDs { + cv.Default = c.ChartBase.SkipCRDs + } else { + js.Properties.Delete("skipCRDs") + } + } + if cv, ok := js.Properties.Get("passCredentials"); ok { + if c.PassCredentials { + cv.Default = c.ChartBase.PassCredentials + } else { + js.Properties.Delete("passCredentials") + } + } + if cv, ok := js.Properties.Get("schemaValidator"); ok { + if c.ChartBase.SchemaValidator != jsonschema.DefaultValidatorType { + cv.Default = c.ChartBase.SchemaValidator + cv.Enum = jsonschema.ValidatorTypeEnum + } else { + js.Properties.Delete("schemaValidator") + } + } + if cv, ok := js.Properties.Get("values"); ok { + cv.Type = "null" + } + if _, ok := js.Properties.Get("valueFiles"); ok { + js.Properties.Delete("valueFiles") + } + if _, ok := js.Properties.Get("postRenderer"); ok { + js.Properties.Delete("postRenderer") + } + + b := &bytes.Buffer{} + err = jsonschema.ReflectedSchemaToKCL(js, b) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) + } + nb := &bytes.Buffer{} + scanner := bufio.NewScanner(b) + for scanner.Scan() { + line := scanner.Text() + line = inheritHelmChart(line) + if SchemaValuesRegexp.MatchString(line) { + line = SchemaValuesRegexp.ReplaceAllString(line, "${1}Values | ${2}") + } + nb.WriteString(line + "\n") + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to scan kcl schema: %w", err) + } + if _, err := nb.WriteTo(w); err != nil { + return fmt.Errorf("failed to write to KCL schema: %w", err) + } + + return nil +} + +func newSchemaReflector() (*jsonschema.Reflector, error) { + r := jsonschema.NewReflector() + + return r, nil +} + +func inheritHelmChart(line string) string { + if SchemaDefinitionRegexp.MatchString(line) { + return SchemaDefinitionRegexp.ReplaceAllString(line, "import helm\n\nschema ${1}(helm.Chart):") + } + return line +} diff --git a/pkg/helmmodels/chartmodule/chart_test.go b/pkg/helmmodels/chartmodule/chart_test.go new file mode 100644 index 0000000..7697382 --- /dev/null +++ b/pkg/helmmodels/chartmodule/chart_test.go @@ -0,0 +1,33 @@ +package chartmodule_test + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/MacroPower/kclipper/pkg/helmmodels/chartmodule" +) + +func TestGenerateChart(t *testing.T) { + t.Parallel() + + os.Chdir("../../../") + + b := &bytes.Buffer{} + + cc := chartmodule.ChartConfig{} + err := cc.GenerateKCL(b) + require.NoError(t, err) + require.NotEmpty(t, b.String()) + // assert.Equal(t, "", b.String()) + + b.Truncate(0) + c := chartmodule.Chart{} + err = c.GenerateKCL(b) + require.NoError(t, err) + require.NotEmpty(t, b.String()) + assert.Equal(t, "", b.String()) +} diff --git a/pkg/helmmodels/pluginmodule/chart.go b/pkg/helmmodels/pluginmodule/chart.go new file mode 100644 index 0000000..2ebc85a --- /dev/null +++ b/pkg/helmmodels/pluginmodule/chart.go @@ -0,0 +1,159 @@ +package pluginmodule + +import ( + "bufio" + "bytes" + "fmt" + "io" + "reflect" + "regexp" + + "github.com/MacroPower/kclipper/pkg/jsonschema" +) + +var SchemaDefinitionRegexp = regexp.MustCompile(`schema\s+(\S+):\s*`) + +// Represents attributes common in `helm.Chart` and `helm.ChartConfig`. +type ChartBase struct { + // Helm chart name. + Chart string `json:"chart"` + // URL of the Helm chart repository. + RepoURL string `json:"repoURL"` + // Semver tag for the chart's version. + TargetRevision string `json:"targetRevision"` + // Helm release name to use. If omitted the chart name will be used. + ReleaseName string `json:"releaseName,omitempty"` + // Optional namespace to template with. + Namespace string `json:"namespace,omitempty"` + // Set to `True` to skip the custom resource definition installation step (Helm's `--skip-crds`). + SkipCRDs bool `json:"skipCRDs,omitempty"` + // Set to `True` to pass credentials to all domains (Helm's `--pass-credentials`). + PassCredentials bool `json:"passCredentials,omitempty"` + // Validator to use for the Values schema. + SchemaValidator jsonschema.ValidatorType `json:"schemaValidator,omitempty"` +} + +func (c *ChartBase) GenerateKCL(w io.Writer) error { + r, err := newSchemaReflector() + if err != nil { + return fmt.Errorf("failed to create schema reflector: %w", err) + } + js := r.Reflect(reflect.TypeOf(ChartBase{})) + if cv, ok := js.Properties.Get("schemaValidator"); ok { + cv.Enum = jsonschema.ValidatorTypeEnum + } + + err = jsonschema.ReflectedSchemaToKCL(js, w) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) + } + + return nil +} + +// Defines a Helm chart. +type Chart struct { + // Helm values to be passed to Helm template. These take precedence over valueFiles. + Values any `json:"values,omitempty"` + // Helm value files to be passed to Helm template. + ValueFiles []string `json:"valueFiles,omitempty"` + // Lambda function to modify the Helm template output. Evaluated for each resource in the Helm template output. + PostRenderer any `json:"postRenderer,omitempty"` +} + +var SchemaPostRendererRegexp = regexp.MustCompile(`(\s+postRenderer\??\s*:\s+)any(.*)`) + +const PostRendererKCLType string = "({str:}) -> {str:}" + +func (c *Chart) GenerateKCL(w io.Writer) error { + r, err := newSchemaReflector() + if err != nil { + return fmt.Errorf("failed to create schema reflector: %w", err) + } + js := r.Reflect(reflect.TypeOf(Chart{})) + // if cv, ok := js.Properties.Get("values"); ok { + // cv.Default = struct{}{} + // } + + b := &bytes.Buffer{} + err = jsonschema.ReflectedSchemaToKCL(js, b) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) + } + nb := &bytes.Buffer{} + scanner := bufio.NewScanner(b) + for scanner.Scan() { + line := scanner.Text() + line = inheritChartBase(line) + if SchemaPostRendererRegexp.MatchString(line) { + line = SchemaPostRendererRegexp.ReplaceAllString(line, "${1}"+PostRendererKCLType+"${2}") + } + nb.WriteString(line + "\n") + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to scan kcl schema: %w", err) + } + if _, err := nb.WriteTo(w); err != nil { + return fmt.Errorf("failed to write to KCL schema: %w", err) + } + + return nil +} + +// Configuration that can be defined in `charts.k`, in addition to those +// specified in `helm.ChartBase`. +type ChartConfig struct { + // Schema generator to use for the Values schema. + SchemaGenerator jsonschema.GeneratorType `json:"schemaGenerator,omitempty"` + // Path to the schema to use, when relevant for the selected schemaGenerator. + SchemaPath string `json:"schemaPath,omitempty"` +} + +func (c *ChartConfig) GenerateKCL(w io.Writer) error { + r, err := newSchemaReflector() + if err != nil { + return fmt.Errorf("failed to create schema reflector: %w", err) + } + js := r.Reflect(reflect.TypeOf(ChartConfig{})) + if cv, ok := js.Properties.Get("schemaGenerator"); ok { + cv.Enum = jsonschema.GeneratorTypeEnum + } + + b := &bytes.Buffer{} + err = jsonschema.ReflectedSchemaToKCL(js, b) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) + } + nb := &bytes.Buffer{} + scanner := bufio.NewScanner(b) + for scanner.Scan() { + line := scanner.Text() + line = inheritChartBase(line) + nb.WriteString(line + "\n") + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to scan kcl schema: %w", err) + } + if _, err := nb.WriteTo(w); err != nil { + return fmt.Errorf("failed to write to KCL schema: %w", err) + } + + return nil +} + +func newSchemaReflector() (*jsonschema.Reflector, error) { + r := jsonschema.NewReflector() + err := r.AddGoComments("github.com/MacroPower/kclipper", "./pkg/helmmodels/pluginmodule") + if err != nil { + return nil, fmt.Errorf("failed to add go comments: %w", err) + } + + return r, nil +} + +func inheritChartBase(line string) string { + if SchemaDefinitionRegexp.MatchString(line) { + return SchemaDefinitionRegexp.ReplaceAllString(line, "schema ${1}(ChartBase):") + } + return line +} diff --git a/pkg/helmmodels/pluginmodule/chart_test.go b/pkg/helmmodels/pluginmodule/chart_test.go new file mode 100644 index 0000000..5be51f8 --- /dev/null +++ b/pkg/helmmodels/pluginmodule/chart_test.go @@ -0,0 +1,40 @@ +package pluginmodule_test + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/MacroPower/kclipper/pkg/helmmodels/pluginmodule" +) + +func TestGenerateHelmModule(t *testing.T) { + t.Parallel() + + os.Chdir("../../../") + + b := &bytes.Buffer{} + + cb := pluginmodule.ChartBase{} + err := cb.GenerateKCL(b) + require.NoError(t, err) + assert.NotEmpty(t, b.String()) + //assert.Equal(t, "", b.String()) + + b.Truncate(0) + cc := pluginmodule.ChartConfig{} + err = cc.GenerateKCL(b) + require.NoError(t, err) + assert.NotEmpty(t, b.String()) + //assert.Equal(t, "", b.String()) + + b.Truncate(0) + c := pluginmodule.Chart{} + err = c.GenerateKCL(b) + require.NoError(t, err) + assert.NotEmpty(t, b.String()) + //assert.Equal(t, "", b.String()) +} diff --git a/pkg/helmutil/add.go b/pkg/helmutil/add.go index 85e0b1a..7d8683e 100644 --- a/pkg/helmutil/add.go +++ b/pkg/helmutil/add.go @@ -12,14 +12,13 @@ import ( "kcl-lang.io/kcl-go" "github.com/MacroPower/kclipper/pkg/helm" - "github.com/MacroPower/kclipper/pkg/helmmodels" + helmmodels "github.com/MacroPower/kclipper/pkg/helmmodels/chartmodule" "github.com/MacroPower/kclipper/pkg/jsonschema" ) var ( SchemaInvalidDocRegexp = regexp.MustCompile(`(\s+\S.*)r"""(.*)"""(.*)`) SchemaDefaultRegexp = regexp.MustCompile(`(\s+\S+:\s+\S+(\s+\|\s+\S+)*)(\s+=.+)`) - SchemaValuesRegexp = regexp.MustCompile(`(\s+values\??\s*:\s+)(.*)`) ) const initialMainContents = `import helm @@ -115,23 +114,7 @@ func (c *ChartPkg) generateAndWriteChartKCL(hc helmmodels.Chart, chartDir string if err := hc.GenerateKCL(kclChart); err != nil { return fmt.Errorf("failed to generate chart.k: %w", err) } - - kclChartFixed := &bytes.Buffer{} - kclChartScanner := bufio.NewScanner(kclChart) - for kclChartScanner.Scan() { - line := kclChartScanner.Text() - if line == "schema Chart:" { - line = "import helm\n\nschema Chart(helm.Chart):" - } else if SchemaValuesRegexp.MatchString(line) { - line = SchemaValuesRegexp.ReplaceAllString(line, "${1}Values | ${2}") - } - kclChartFixed.WriteString(line + "\n") - } - if err := kclChartScanner.Err(); err != nil { - return fmt.Errorf("failed to scan kcl schema: %w", err) - } - - if err := os.WriteFile(path.Join(chartDir, "chart.k"), kclChartFixed.Bytes(), 0o600); err != nil { + if err := os.WriteFile(path.Join(chartDir, "chart.k"), kclChart.Bytes(), 0o600); err != nil { return fmt.Errorf("failed to write chart.k: %w", err) } return nil diff --git a/pkg/helmutil/add_test.go b/pkg/helmutil/add_test.go index b21d0b2..e044c6d 100644 --- a/pkg/helmutil/add_test.go +++ b/pkg/helmutil/add_test.go @@ -10,7 +10,7 @@ import ( "kcl-lang.io/cli/pkg/options" "kcl-lang.io/kcl-go" - "github.com/MacroPower/kclipper/pkg/helmmodels" + helmmodels "github.com/MacroPower/kclipper/pkg/helmmodels/chartmodule" "github.com/MacroPower/kclipper/pkg/helmtest" "github.com/MacroPower/kclipper/pkg/helmutil" "github.com/MacroPower/kclipper/pkg/jsonschema" @@ -42,7 +42,9 @@ func TestHelmChartAdd(t *testing.T) { TargetRevision: "6.7.1", SchemaValidator: jsonschema.HelmValidatorType, }, - SchemaGenerator: jsonschema.AutoGeneratorType, + HelmChartConfig: helmmodels.HelmChartConfig{ + SchemaGenerator: jsonschema.AutoGeneratorType, + }, }, }, "app-template": { @@ -52,8 +54,10 @@ func TestHelmChartAdd(t *testing.T) { RepoURL: "https://bjw-s.github.io/helm-charts/", TargetRevision: "3.6.0", }, - SchemaGenerator: jsonschema.ChartPathGeneratorType, - SchemaPath: "charts/common/values.schema.json", + HelmChartConfig: helmmodels.HelmChartConfig{ + SchemaGenerator: jsonschema.ChartPathGeneratorType, + SchemaPath: "charts/common/values.schema.json", + }, }, }, } diff --git a/pkg/helmutil/set.go b/pkg/helmutil/set.go index 3c70c68..ec88c89 100644 --- a/pkg/helmutil/set.go +++ b/pkg/helmutil/set.go @@ -8,7 +8,7 @@ import ( "kcl-lang.io/kcl-go" - "github.com/MacroPower/kclipper/pkg/helmmodels" + helmmodels "github.com/MacroPower/kclipper/pkg/helmmodels/chartmodule" ) func (c *ChartPkg) Set(chart string, keyValueOverrides string) error { diff --git a/pkg/helmutil/update.go b/pkg/helmutil/update.go index 838fb77..c32c60c 100644 --- a/pkg/helmutil/update.go +++ b/pkg/helmutil/update.go @@ -8,7 +8,7 @@ import ( "kcl-lang.io/cli/pkg/options" "kcl-lang.io/kcl-go/pkg/kcl" - "github.com/MacroPower/kclipper/pkg/helmmodels" + helmmodels "github.com/MacroPower/kclipper/pkg/helmmodels/chartmodule" ) // Update loads the chart configurations defined in charts.k and calls Add to diff --git a/pkg/jsonschema/jsonschema_kcl.go b/pkg/jsonschema/jsonschema_kcl.go index 8639d34..cc74dc4 100644 --- a/pkg/jsonschema/jsonschema_kcl.go +++ b/pkg/jsonschema/jsonschema_kcl.go @@ -4,11 +4,11 @@ import ( "bytes" "fmt" + helmschema "github.com/dadav/helm-schema/pkg/schema" "gopkg.in/yaml.v3" "kcl-lang.io/kcl-go/pkg/tools/gen" "github.com/MacroPower/kclipper/pkg/kclutil" - helmschema "github.com/dadav/helm-schema/pkg/schema" ) // ConvertToKCLSchema converts a JSON schema to a KCL schema. diff --git a/pkg/jsonschema/reflector.go b/pkg/jsonschema/reflector.go index 40cf45b..438faff 100644 --- a/pkg/jsonschema/reflector.go +++ b/pkg/jsonschema/reflector.go @@ -1,22 +1,53 @@ package jsonschema import ( + "fmt" + "io" + "reflect" + invopopjsonschema "github.com/invopop/jsonschema" + "kcl-lang.io/kcl-go/pkg/tools/gen" + + "github.com/MacroPower/kclipper/pkg/kclutil" ) type Reflector struct { - r *invopopjsonschema.Reflector + Reflector *invopopjsonschema.Reflector } func NewReflector() *Reflector { return &Reflector{ - r: &invopopjsonschema.Reflector{ + Reflector: &invopopjsonschema.Reflector{ DoNotReference: true, ExpandedStruct: true, }, } } -func (r *Reflector) Reflect(v interface{}) *invopopjsonschema.Schema { - return r.r.Reflect(v) +func (r *Reflector) AddGoComments(pkg, path string) error { + err := r.Reflector.AddGoComments(pkg, path, invopopjsonschema.WithFullComment()) + if err != nil { + return fmt.Errorf("failed to add go comments from '%s': %w", pkg, err) + } + return nil +} + +func (r *Reflector) Reflect(t reflect.Type) *invopopjsonschema.Schema { + return r.Reflector.ReflectFromType(t) +} + +func ReflectedSchemaToKCL(r *invopopjsonschema.Schema, w io.Writer) error { + jsBytes, err := r.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal json schema: %w", err) + } + + if err := kclutil.Gen.GenKcl(w, "chart", jsBytes, &gen.GenKclOptions{ + Mode: gen.ModeJsonSchema, + CastingOption: gen.OriginalName, + }); err != nil { + return fmt.Errorf("failed to generate kcl schema: %w", err) + } + + return nil }