Skip to content

Commit

Permalink
feat(PL-2701): unify cue schema with values
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmdm committed May 8, 2024
1 parent d63b4a7 commit ba521a7
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 81 deletions.
5 changes: 1 addition & 4 deletions cmd/joy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@ import (
"runtime/debug"

"github.com/spf13/cobra"
flag "github.com/spf13/pflag"

"github.com/nestoca/joy/internal"

"github.com/nestoca/joy/internal/config"

flag "github.com/spf13/pflag"

"github.com/nestoca/joy/internal/help"
)

Expand Down
6 changes: 2 additions & 4 deletions cmd/joy/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import (
"path/filepath"
"testing"

cp "github.com/otiai10/copy"

"github.com/go-git/go-git/v5/plumbing"

"github.com/acarl005/stripansi"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
cp "github.com/otiai10/copy"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"

Expand Down
3 changes: 1 addition & 2 deletions cmd/joy/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import (
"fmt"
"os"

"github.com/nestoca/joy/internal"

"github.com/TwiN/go-color"
"github.com/nestoca/survey/v2"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"

"github.com/nestoca/joy/internal"
"github.com/nestoca/joy/internal/config"
"github.com/nestoca/joy/internal/dependencies"
"github.com/nestoca/joy/internal/git"
Expand Down
6 changes: 2 additions & 4 deletions internal/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import (
"regexp"
"strings"

"github.com/nestoca/joy/internal/style"

"github.com/nestoca/joy/internal"

"github.com/spf13/cobra"

"github.com/nestoca/joy/internal"
"github.com/nestoca/joy/internal/config"
"github.com/nestoca/joy/internal/style"
)

const allCommandsKey = "~all"
Expand Down
57 changes: 54 additions & 3 deletions internal/release/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import (
"fmt"
"html/template"
"io"
"os"
"regexp"
"slices"
"strings"

"github.com/Masterminds/sprig/v3"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
cueerrors "cuelang.org/go/cue/errors"

"github.com/Masterminds/sprig/v3"
"github.com/TwiN/go-color"
"github.com/davidmdm/x/xerr"
"github.com/nestoca/survey/v2"
"gopkg.in/yaml.v3"

Expand Down Expand Up @@ -71,7 +76,7 @@ type RenderReleaseParams struct {
}

func RenderRelease(ctx context.Context, params RenderReleaseParams) error {
values, err := HydrateValues(params.Release, params.ValueMapping)
values, err := HydrateValues(params.Release, params.Chart, params.ValueMapping)
if err != nil {
return fmt.Errorf("hydrating values: %w", err)
}
Expand Down Expand Up @@ -158,7 +163,7 @@ func getReleaseViaPrompt(releases []*cross.Release, env string) (*v1alpha1.Relea
return candidateReleases[idx], nil
}

func HydrateValues(release *v1alpha1.Release, mappings *config.ValueMapping) (map[string]any, error) {
func HydrateValues(release *v1alpha1.Release, chart *helm.ChartFS, mappings *config.ValueMapping) (map[string]any, error) {
params := struct {
Release *v1alpha1.Release
Environment *v1alpha1.Environment
Expand Down Expand Up @@ -200,6 +205,44 @@ func HydrateValues(release *v1alpha1.Release, mappings *config.ValueMapping) (ma
return nil, err
}

result, err = unifyValues(result, chart)
if err != nil {
return nil, fmt.Errorf("unifying with chart schema: %w", err)
}

return result, nil
}

func unifyValues(values map[string]any, chart *helm.ChartFS) (map[string]any, error) {
if chart == nil {
return values, nil
}

rawSchema, err := chart.ReadFile("values.cue")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return values, nil
}
return nil, fmt.Errorf("reading values.cue: %w", err)
}

runtime := cuecontext.New()

schema := runtime.
CompileBytes(rawSchema).
LookupPath(cue.MakePath(cue.Def("#values")))

unified := schema.Unify(runtime.Encode(values))

if err := unified.Validate(cue.Final(), cue.Concrete(true)); err != nil {
return nil, xerr.MultiErrFrom("validating values", AsErrorList(cueerrors.Errors(err))...)
}

var result map[string]any
if err := unified.Decode(&result); err != nil {
return nil, fmt.Errorf("decoding value: %w", err)
}

return result, nil
}

Expand Down Expand Up @@ -414,3 +457,11 @@ func IsNotFoundError(err error) bool {
var notfound NotFoundError
return errors.Is(err, notfound)
}

func AsErrorList[T error](list []T) []error {
result := make([]error, len(list))
for i, err := range list {
result[i] = err
}
return result
}
71 changes: 71 additions & 0 deletions internal/release/render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"bytes"
"context"
"errors"
"os"
"strings"
"testing"

"github.com/davidmdm/x/xfs"
"github.com/stretchr/testify/require"

"github.com/nestoca/joy/api/v1alpha1"
Expand Down Expand Up @@ -844,3 +847,71 @@ func TestHydrateObjectValues(t *testing.T) {
})
}
}

func TestSchemaUnification(t *testing.T) {
cases := []struct {
Name string
ReadFileFunc func(string) ([]byte, error)
Values map[string]any
ExpectedValues map[string]any
ExpectedError string
}{
{
Name: "applies schema default",
ReadFileFunc: func(name string) ([]byte, error) {
return []byte(`#values: { color: "r" | "g" | *"b" }`), nil
},
Values: map[string]any{},
ExpectedValues: map[string]any{"color": "b"},
},
{
Name: "fails schema validation",
ReadFileFunc: func(name string) ([]byte, error) {
return []byte(`#values: { color: "r" | "g" | *"b" }`), nil
},
Values: map[string]any{"color": "cyan", "enabled": true},
ExpectedError: strings.Join(
[]string{
"unifying with chart schema: validating values:",
" - #values.color: 3 errors in empty disjunction:",
` - #values.color: conflicting values "b" and "cyan"`,
` - #values.color: conflicting values "g" and "cyan"`,
` - #values.color: conflicting values "r" and "cyan"`,
` - #values.enabled: field not allowed`,
},
"\n",
),
},
{
Name: "no schema file",
ReadFileFunc: func(name string) ([]byte, error) {
return nil, os.ErrNotExist
},
Values: map[string]any{"color": "red"},
ExpectedValues: map[string]any{"color": "red"},
},
}

for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
mockFS := &xfs.FSMock{ReadFileFunc: tc.ReadFileFunc}

result, err := HydrateValues(
&v1alpha1.Release{
Spec: v1alpha1.ReleaseSpec{Values: tc.Values},
Environment: &v1alpha1.Environment{},
},
&helm.ChartFS{FS: mockFS},
&config.ValueMapping{},
)

if tc.ExpectedError != "" {
require.EqualError(t, err, tc.ExpectedError)
return
}

require.NoError(t, err)
require.Equal(t, tc.ExpectedValues, result)
})
}
}
55 changes: 0 additions & 55 deletions internal/release/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package validate

import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
cueerrors "cuelang.org/go/cue/errors"

"github.com/davidmdm/x/xerr"
"golang.org/x/mod/semver"

Expand Down Expand Up @@ -67,10 +61,6 @@ func ValidateRelease(ctx context.Context, params ValidateReleaseParams) error {
}
}

if err := validateSchema(params.Release, params.ValueMapping, params.Chart); err != nil {
return err
}

renderOpts := render.RenderReleaseParams{
Release: params.Release,
Chart: params.Chart,
Expand All @@ -91,48 +81,3 @@ func ValidateRelease(ctx context.Context, params ValidateReleaseParams) error {

return nil
}

func validateSchema(release *v1alpha1.Release, mappings *config.ValueMapping, chart *helm.ChartFS) error {
if release.Spec.Values == nil {
return nil
}

hydratedValues, err := render.HydrateValues(release, mappings)
if err != nil {
return fmt.Errorf("hydrating values: %w", err)
}

schemaData, err := chart.ReadFile("values.cue")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("reading schema file: %w", err)
}

runtime := cuecontext.New()

schema := runtime.
CompileBytes(schemaData).
LookupPath(cue.MakePath(cue.Def("#values")))

values := runtime.Encode(hydratedValues)

validationErr := schema.Unify(values).Validate(cue.Concrete(true))

if errs := cueerrors.Errors(validationErr); len(errs) == 1 {
return errs[0]
} else if len(errs) > 1 {
return xerr.MultiErrFrom("", AsErrorList(errs)...)
}

return nil
}

func AsErrorList[T error](list []T) []error {
result := make([]error, len(list))
for i, err := range list {
result[i] = err
}
return result
}
13 changes: 8 additions & 5 deletions internal/release/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,26 @@ func TestValidateRelease(t *testing.T) {
ChartFS: &xfs.FSMock{
ReadFileFunc: func(string) ([]byte, error) { return []byte(`#values: { hello: string }`), nil },
},
ExpectedErr: "#values.hello: conflicting values string and true (mismatched types string and bool)",
ExpectedErr: "hydrating values: unifying with chart schema: validating values: #values.hello: conflicting values string and true (mismatched types string and bool)",
},
{
Name: "values missing from spec",
Release: &v1alpha1.Release{Spec: v1alpha1.ReleaseSpec{Values: map[string]any{}}, Environment: &allowPullRequest},
ChartFS: &xfs.FSMock{
ReadFileFunc: func(string) ([]byte, error) { return []byte(`#values: { hello: string }`), nil },
},
ExpectedErr: "#values.hello: incomplete value string",
ExpectedErr: "hydrating values: unifying with chart schema: validating values: #values.hello: incomplete value string",
},
{
Name: "multiple errors",
Release: &v1alpha1.Release{Spec: v1alpha1.ReleaseSpec{Values: map[string]any{"one": "one", "two": "two"}}, Environment: &allowPullRequest},
ChartFS: &xfs.FSMock{
ReadFileFunc: func(string) ([]byte, error) { return []byte(`#values: { one: 1, two: 2 }`), nil },
},
ExpectedErr: "error:\n - #values.one: conflicting values 1 and \"one\" (mismatched types int and string)\n - #values.two: conflicting values 2 and \"two\" (mismatched types int and string)",
ExpectedErr: "" +
"hydrating values: unifying with chart schema: validating values:\n" +
" - #values.one: conflicting values 1 and \"one\" (mismatched types int and string)\n" +
" - #values.two: conflicting values 2 and \"two\" (mismatched types int and string)",
},
{
Name: "render fails",
Expand Down Expand Up @@ -92,7 +95,7 @@ func TestValidateRelease(t *testing.T) {
ChartFS: &xfs.FSMock{
ReadFileFunc: func(string) ([]byte, error) { return nil, errors.New("disk corrupted") },
},
ExpectedErr: "reading schema file: disk corrupted",
ExpectedErr: "hydrating values: unifying with chart schema: reading values.cue: disk corrupted",
},
{
Name: "standard version with disallow promotion from pull requests",
Expand Down Expand Up @@ -140,7 +143,7 @@ func TestValidateRelease(t *testing.T) {
ReadFileFunc: func(string) ([]byte, error) { return []byte(`#values: { hello: "world" }`), nil },
DirNameFunc: func() string { return "." },
},
ExpectedErr: `#values.hello: conflicting values "narnia" and "world"`,
ExpectedErr: `hydrating values: unifying with chart schema: validating values: #values.hello: conflicting values "narnia" and "world"`,
},
}

Expand Down
Loading

0 comments on commit ba521a7

Please sign in to comment.