generated from kubernetes/kubernetes-template-project
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
65 changed files
with
14,422 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package cmd | ||
|
||
import ( | ||
"cmp" | ||
"fmt" | ||
"os" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/goccy/go-yaml" | ||
"github.com/goccy/go-yaml/parser" | ||
"golang.org/x/exp/maps" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
// convert the given map of filenames to validation errors into a lint output format: '%f:%l:%c: %m' | ||
// %f - file, %l - line, %c - column, %m - message | ||
func lintMarshal(details map[string][]metav1.Status) ([]byte, error) { | ||
const ( | ||
nilValue = "<nil>" | ||
) | ||
files := maps.Keys(details) | ||
slices.Sort(files) | ||
|
||
results := []string{} | ||
DETAILS: | ||
for _, file := range files { | ||
status := details[file] | ||
causes := make(map[string][]metav1.StatusCause) | ||
for _, s := range status { | ||
if s.Status == metav1.StatusSuccess { | ||
continue DETAILS // only lint errors | ||
} | ||
for _, c := range s.Details.Causes { | ||
if c.Field == nilValue { | ||
continue // no field to lookup/annotate | ||
} | ||
key := string(c.Type) | ||
causes[key] = append(causes[key], c) | ||
} | ||
} | ||
if len(causes) == 0 { | ||
continue // nothing to do, no causes deemed problematic | ||
} | ||
b, err := os.ReadFile(file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
// group causes by position, so that we can group them together in the same output line | ||
errors := make(map[Position][]metav1.StatusCause) | ||
for _, items := range causes { | ||
for _, c := range items { | ||
path, err := yaml.PathString(fmt.Sprintf("$.%s", c.Field)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
position, err := getPosition(path, b) | ||
if err != nil { | ||
return nil, err | ||
} | ||
errors[position] = append(errors[position], c) | ||
} | ||
} | ||
keys := maps.Keys(errors) | ||
slices.SortFunc(keys, func(i, j Position) int { | ||
return cmp.Or( | ||
cmp.Compare(i.Line, j.Line), | ||
cmp.Compare(i.Column, j.Column), | ||
) | ||
}) | ||
for _, position := range keys { | ||
causes := errors[position] | ||
messages := make(map[string][]string) | ||
for _, c := range causes { | ||
messages[c.Field] = append(messages[c.Field], fmt.Sprintf("(reason: %q; %s)", c.Type, c.Message)) | ||
} | ||
fieldMessages := []string{} | ||
for field, msgs := range messages { | ||
fieldMessages = append(fieldMessages, fmt.Sprintf("field %q: %s", field, strings.Join(msgs, ", "))) | ||
} | ||
le := LintError{ | ||
File: file, | ||
Line: position.Line, | ||
Column: position.Column, | ||
Message: strings.Join(fieldMessages, ", "), | ||
} | ||
results = append(results, le.String()) | ||
} | ||
} | ||
return []byte(strings.Join(results, "\n")), nil | ||
} | ||
|
||
type Position struct { | ||
Line int | ||
Column int | ||
} | ||
|
||
type Reason struct { | ||
Type string | ||
Message string | ||
} | ||
|
||
type LintError struct { | ||
File string | ||
Line int | ||
Column int | ||
Message string | ||
} | ||
|
||
func (e LintError) String() string { | ||
return fmt.Sprintf("%s:%d:%d: %s", e.File, e.Line, e.Column, e.Message) | ||
} | ||
|
||
func getPosition(p *yaml.Path, source []byte) (Position, error) { | ||
file, err := parser.ParseBytes([]byte(source), 0) | ||
if err != nil { | ||
return Position{}, err | ||
} | ||
node, err := p.FilterFile(file) | ||
if err != nil { | ||
return Position{}, err | ||
} | ||
return Position{ | ||
Line: node.GetToken().Position.Line, | ||
Column: node.GetToken().Position.Column, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package cmd | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
func TestLintMarshal(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
input map[string][]metav1.Status | ||
expected string | ||
}{ | ||
{ | ||
name: "empty", | ||
input: map[string][]metav1.Status{}, | ||
expected: ``, | ||
}, | ||
{ | ||
name: "success", | ||
input: map[string][]metav1.Status{ | ||
"file.yaml": { | ||
{Status: metav1.StatusSuccess, Reason: "valid"}, | ||
}, | ||
}, | ||
expected: ``, | ||
}, | ||
{ | ||
name: "single error, single cause", | ||
input: map[string][]metav1.Status{ | ||
"../../testcases/manifests/configmap.yaml": { | ||
{Status: metav1.StatusFailure, Reason: "invalid", Details: &metav1.StatusDetails{ | ||
Causes: []metav1.StatusCause{ | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.name", | ||
Message: "name is required or invalid somehow", | ||
}, | ||
}, | ||
}}, | ||
}, | ||
}, | ||
expected: `../../testcases/manifests/configmap.yaml:8:9: field "metadata.name": (reason: "FailureType"; name is required or invalid somehow)`, | ||
}, | ||
{ | ||
name: "single error with ignored success", | ||
input: map[string][]metav1.Status{ | ||
"../../testcases/manifests/configmap.yaml": { | ||
{Status: metav1.StatusSuccess, Reason: "valid"}, | ||
}, | ||
"../../testcases/manifests/apiservice.yaml": { | ||
{Status: metav1.StatusFailure, Reason: "invalid", Details: &metav1.StatusDetails{ | ||
Causes: []metav1.StatusCause{ | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.name", | ||
Message: "name is required or invalid somehow but specific to apiservices", | ||
}, | ||
}, | ||
}}, | ||
}, | ||
}, | ||
expected: `../../testcases/manifests/apiservice.yaml:14:9: field "metadata.name": (reason: "FailureType"; name is required or invalid somehow but specific to apiservices)`, | ||
}, | ||
{ | ||
name: "multiple errors, multiple causes", | ||
input: map[string][]metav1.Status{ | ||
"../../testcases/manifests/configmap.yaml": { | ||
{Status: metav1.StatusFailure, Reason: "invalid", Details: &metav1.StatusDetails{ | ||
Causes: []metav1.StatusCause{ | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.name", | ||
Message: "name is required or invalid somehow 1x1", | ||
}, | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.finalizers", | ||
Message: "something wrong with finalizers 1x2", | ||
}, | ||
}, | ||
}}, | ||
}, | ||
"../../testcases/manifests/apiservice.yaml": { | ||
{Status: metav1.StatusFailure, Reason: "invalid", Details: &metav1.StatusDetails{ | ||
Causes: []metav1.StatusCause{ | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.name", | ||
Message: "name is required or invalid somehow 2x1", | ||
}, | ||
{ | ||
Type: "FailureType", | ||
Field: "metadata.name", | ||
Message: "name is required or invalid somehow 2x2", | ||
}, | ||
}, | ||
}}, | ||
}, | ||
}, | ||
expected: strings.Join([]string{ | ||
`../../testcases/manifests/apiservice.yaml:14:9: field "metadata.name": (reason: "FailureType"; name is required or invalid somehow 2x1), (reason: "FailureType"; name is required or invalid somehow 2x2)`, | ||
`../../testcases/manifests/configmap.yaml:8:9: field "metadata.name": (reason: "FailureType"; name is required or invalid somehow 1x1)`, | ||
`../../testcases/manifests/configmap.yaml:10:3: field "metadata.finalizers": (reason: "FailureType"; something wrong with finalizers 1x2)`, | ||
}, "\n"), | ||
}, | ||
{ | ||
name: "single error of complex field", | ||
input: map[string][]metav1.Status{ | ||
"../../testcases/manifests/error_x_list_map_duplicate_key.yaml": { | ||
{Status: metav1.StatusFailure, Reason: "invalid", Details: &metav1.StatusDetails{ | ||
Causes: []metav1.StatusCause{ | ||
{ | ||
Type: "FieldValueDuplicate", | ||
Field: "spec.containers[0].ports[2]", | ||
Message: `Duplicate value: map[string]interface{}{"key":"value"}`, | ||
}, | ||
}, | ||
}}, | ||
}, | ||
}, | ||
expected: `../../testcases/manifests/error_x_list_map_duplicate_key.yaml:51:19: field "spec.containers[0].ports[2]": (reason: "FieldValueDuplicate"; Duplicate value: map[string]interface{}{"key":"value"})`, | ||
}, | ||
} | ||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
actual, err := lintMarshal(tc.input) | ||
require.NoError(t, err) | ||
require.Equal(t, tc.expected, string(actual)) | ||
}) | ||
} | ||
} |
Oops, something went wrong.