Skip to content
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ go.work.sum
# .vscode/

bin/
validation/
/validation
27 changes: 26 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,49 @@ go 1.25.3

require (
github.com/deckhouse/deckhouse/pkg/log v0.1.1-0.20251230144142-2bad7c3d1edf
github.com/go-openapi/spec v0.19.8
github.com/go-openapi/strfmt v0.19.5
github.com/go-openapi/validate v0.19.12
github.com/gookit/color v1.5.2
github.com/hashicorp/go-multierror v1.1.1
github.com/name212/govalue v1.0.2
github.com/stretchr/testify v1.9.0
github.com/werf/logboek v0.5.5
gopkg.in/yaml.v3 v3.0.1
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/yaml v1.6.0
)

require (
github.com/DataDog/gostackparse v0.7.0 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-openapi/analysis v0.19.10 // indirect
github.com/go-openapi/errors v0.19.7 // indirect
github.com/go-openapi/jsonpointer v0.19.3 // indirect
github.com/go-openapi/jsonreference v0.19.3 // indirect
github.com/go-openapi/loads v0.19.5 // indirect
github.com/go-openapi/runtime v0.19.16 // indirect
github.com/go-openapi/swag v0.19.9 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/mailru/easyjson v0.7.1 // indirect
github.com/mitchellh/mapstructure v1.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go.mongodb.org/mongo-driver v1.5.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)

// Remove 'in body' from errors, fix for Go 1.16 (https://github.com/go-openapi/validate/pull/138).
replace github.com/go-openapi/validate => github.com/flant/go-openapi-validate v0.19.12-flant.0
215 changes: 214 additions & 1 deletion go.sum

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions pkg/yaml/split.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2026 Flant JSC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package yaml

import (
"io"
"regexp"
"strings"
)

var yamlSplitRegexp = regexp.MustCompile(`(?:^|\s*\n)---\s*`)

func SplitYAML(s string) []string {
return yamlSplitRegexp.Split(strings.TrimSpace(s), -1)
}

func SplitYAMLBytes(s []byte) []string {
return SplitYAML(string(s))
}

func SplitYAMLReader(reader io.Reader) ([]string, error) {
content, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return SplitYAML(string(content)), nil
}
36 changes: 36 additions & 0 deletions pkg/yaml/unmarshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2026 Flant JSC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package yaml

import (
"fmt"
"reflect"

"gopkg.in/yaml.v3"
)

func Unmarshal[T any](data []byte) (T, error) {
var result T
err := yaml.Unmarshal(data, &result)
if err != nil {
return result, fmt.Errorf("failed to unmarshal to %s: %w", reflect.TypeFor[T]().String(), err)
}

return result, nil
}

func UnmarshalString[T any](data string) (T, error) {
return Unmarshal[T]([]byte(data))
}
65 changes: 65 additions & 0 deletions pkg/yaml/unmarshal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 Flant JSC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package yaml

import (
"testing"

"github.com/stretchr/testify/require"
)

type testType struct {
StringValue string `yaml:"string"`
IntValue int `yaml:"int"`
Sub testSubType `yaml:"sub"`
}

type testSubType struct {
SliceValue []string `yaml:"slice"`
}

func TestUnmarshal(t *testing.T) {
doc := `
string: str
int: 42
sub:
slice:
- "first"
- "second"
- "third"
`

assertResult := func(t *testing.T, result testType) {
require.Equal(t, "str", result.StringValue)
require.Equal(t, 42, result.IntValue)
require.Len(t, result.Sub.SliceValue, 3)
require.Equal(t, "first", result.Sub.SliceValue[0])
require.Equal(t, "second", result.Sub.SliceValue[1])
require.Equal(t, "third", result.Sub.SliceValue[2])
}

t.Run("val", func(t *testing.T) {
res, err := UnmarshalString[testType](doc)
require.NoError(t, err)
assertResult(t, res)
})

t.Run("pointer", func(t *testing.T) {
res, err := UnmarshalString[*testType](doc)
require.NoError(t, err)
require.NotNil(t, res)
assertResult(t, *res)
})
}
130 changes: 130 additions & 0 deletions pkg/yaml/validation/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2026 Flant JSC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package validation

import (
"fmt"
"strings"
)

type ErrorKind int

const (
ErrKindChangesValidationFailed ErrorKind = iota + 1
ErrKindValidationFailed
ErrKindInvalidYAML
ErrDocumentValidationFailed
ErrSchemaNotFound
)

func (k ErrorKind) Error() string {
return k.String()
}

func (k ErrorKind) String() string {
switch k {
case ErrKindChangesValidationFailed:
return "ChangesValidationFailed"
case ErrKindValidationFailed:
return "DocumentKindValidationFailed"
case ErrKindInvalidYAML:
return "InvalidYAML"
case ErrDocumentValidationFailed:
return "DocumentValidationFailed"
case ErrSchemaNotFound:
return "SchemaNotFound"
default:
return "unknown"
}
}

type ValidationError struct {
Kind ErrorKind
Errors []Error
}

func (v *ValidationError) Append(kind ErrorKind, e Error) {
if v.Kind < kind {
v.Kind = kind
}
v.Errors = append(v.Errors, e)
}

func (v *ValidationError) Error() string {
if v == nil {
return ""
}
errs := make([]string, 0, len(v.Errors))
for _, e := range v.Errors {
b := strings.Builder{}
if e.Index != nil {
b.WriteString(fmt.Sprintf("[%d]", *e.Index))
}

if e.Group != "" {
b.WriteString(fmt.Sprintf("%s/%s, Kind=%s", e.Group, e.Version, e.Kind))
}
if e.Name != "" {
b.WriteString(fmt.Sprintf(" %q", e.Name))
}
if b.Len() != 0 {
b.WriteString(": ")
}
b.WriteString(strings.Join(e.Messages, "; "))

errs = append(errs, b.String())
}

return fmt.Sprintf("%s: %s", v.Kind, strings.Join(errs, "\n"))
}

func (v *ValidationError) ErrorOrNil() error {
if v == nil {
return nil
}
if len(v.Errors) == 0 {
return nil
}

return v
}

type Error struct {
Index *int
Group string
Version string
Kind string
Name string
Messages []string
}

type namedIndex struct {
Kind string `json:"kind"`
Version string `json:"apiVersion"`
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
}

func (i *namedIndex) IsValid() bool {
return i.Kind != "" && i.Version != ""
}

func (i *namedIndex) String() string {
if i.Metadata.Name != "" {
return fmt.Sprintf("%s, %s", i.Kind, i.Version)
}
return fmt.Sprintf("%s, %s, metadata.name: %q", i.Kind, i.Version, i.Metadata.Name)
}
Loading