1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 18:38:40 +00:00

fix policy command (#8404)

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2023-09-15 09:57:32 +02:00 committed by GitHub
parent e43b78c6c7
commit a5e6a7fa33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 475 additions and 53 deletions

View file

@ -2,6 +2,7 @@ package fix
import (
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/fix/policy"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/fix/test"
"github.com/spf13/cobra"
)
@ -19,6 +20,7 @@ func Command() *cobra.Command {
},
}
cmd.AddCommand(
policy.Command(),
test.Command(),
)
return cmd

View file

@ -0,0 +1,26 @@
package policy
import (
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command"
"github.com/spf13/cobra"
)
func Command() *cobra.Command {
var options options
cmd := &cobra.Command{
Use: "policy [dir]...",
Short: command.FormatDescription(true, websiteUrl, true, description...),
Long: command.FormatDescription(false, websiteUrl, true, description...),
Example: command.FormatExamples(examples...),
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := options.validate(args...); err != nil {
return err
}
return options.execute(cmd.OutOrStdout(), args...)
},
}
cmd.Flags().BoolVar(&options.save, "save", false, "Save fixed file")
return cmd
}

View file

@ -0,0 +1,65 @@
package policy
import (
"bytes"
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommand(t *testing.T) {
cmd := Command()
assert.NotNil(t, cmd)
err := cmd.Execute()
assert.Error(t, err)
}
func TestCommandInvalidFileName(t *testing.T) {
cmd := Command()
assert.NotNil(t, cmd)
cmd.SetArgs([]string{"foo", "-f", ""})
err := cmd.Execute()
assert.Error(t, err)
}
func TestCommandWithInvalidArg(t *testing.T) {
cmd := Command()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetErr(b)
err := cmd.Execute()
assert.Error(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
expected := `Error: requires at least 1 arg(s), only received 0`
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
}
func TestCommandWithInvalidFlag(t *testing.T) {
cmd := Command()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetErr(b)
cmd.SetArgs([]string{"--xxx"})
err := cmd.Execute()
assert.Error(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
expected := `Error: unknown flag: --xxx`
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
}
func TestCommandHelp(t *testing.T) {
cmd := Command()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"--help"})
err := cmd.Execute()
assert.NoError(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(out), cmd.Long))
}

View file

@ -0,0 +1,19 @@
package policy
// TODO
var websiteUrl = ``
var description = []string{
`Fix inconsistencies and deprecated usage in Kyverno policy files.`,
}
var examples = [][]string{
{
`# Fix Kyverno policy files`,
`KYVERNO_EXPERIMENTAL=true kyverno fix policy .`,
},
{
`# Fix Kyverno policy files and save them back`,
`KYVERNO_EXPERIMENTAL=true kyverno fix policy . --save`,
},
}

View file

@ -0,0 +1,161 @@
package policy
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/fix"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy"
gitutils "github.com/kyverno/kyverno/pkg/utils/git"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)
type options struct {
save bool
}
func (o options) validate(dirs ...string) error {
if len(dirs) == 0 {
return errors.New("at least one directory is required")
}
return nil
}
func find(path string) ([]string, error) {
var files []string
err := filepath.Walk(path, func(file string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if gitutils.IsYaml(info) {
files = append(files, file)
}
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
func (o options) execute(out io.Writer, dirs ...string) error {
for _, dir := range dirs {
files, err := find(dir)
if err != nil {
return err
}
for _, file := range files {
o.processFile(out, file)
}
}
fmt.Fprintln(out, "Done.")
return nil
}
func (o options) processFile(out io.Writer, path string) {
policies, vaps, err := policy.LoadWithLoader(policy.KubectlValidateLoader, nil, "", path)
if err != nil {
return
}
if len(policies) == 0 {
return
}
var fixed []kyvernov1.PolicyInterface
for _, policy := range policies {
copy := policy.CreateDeepCopy()
fmt.Fprintf(out, "Processing file (%s)...\n", path)
messages, err := fix.FixPolicy(copy)
for _, warning := range messages {
fmt.Fprintln(out, " WARNING:", warning)
}
if err != nil {
fmt.Fprintln(out, " ERROR:", err)
return
}
fixed = append(fixed, copy)
}
needsSave := !reflect.DeepEqual(policies, fixed)
if o.save && needsSave {
fmt.Fprintf(out, " Saving file (%s)...", path)
fmt.Fprintln(out)
var yamlBytes []byte
for _, policy := range fixed {
untyped, err := kubeutils.ObjToUnstructured(policy)
if err != nil {
fmt.Fprintf(out, " ERROR: converting to yaml: %s", err)
fmt.Fprintln(out)
return
}
// prune some fields
unstructured.RemoveNestedField(untyped.UnstructuredContent(), "status")
unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "creationTimestamp")
unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "generation")
unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "uid")
rules, ok, err := unstructured.NestedFieldNoCopy(untyped.UnstructuredContent(), "spec", "rules")
if !ok || err != nil {
return
}
for _, rule := range rules.([]interface{}) {
rule := rule.(map[string]interface{})
unstructured.RemoveNestedField(rule, "exclude", "resources")
unstructured.RemoveNestedField(rule, "match", "resources")
if item, _, _ := unstructured.NestedMap(rule, "generate", "clone"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "generate", "clone")
}
if item, _, _ := unstructured.NestedMap(rule, "generate", "cloneList"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "generate", "cloneList")
}
if item, _, _ := unstructured.NestedMap(rule, "generate"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "generate")
}
if item, _, _ := unstructured.NestedMap(rule, "mutate"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "mutate")
}
if item, _, _ := unstructured.NestedMap(rule, "exclude"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "exclude")
}
if item, _, _ := unstructured.NestedMap(rule, "match"); len(item) == 0 {
unstructured.RemoveNestedField(rule, "match")
}
}
jsonBytes, err := untyped.MarshalJSON()
if err != nil {
fmt.Fprintf(out, " ERROR: converting to yaml: %s", err)
fmt.Fprintln(out)
return
}
finalBytes, err := yaml.JSONToYAML(jsonBytes)
if err != nil {
fmt.Fprintf(out, " ERROR: converting to yaml: %s", err)
fmt.Fprintln(out)
return
}
yamlBytes = append(yamlBytes, []byte("---\n")...)
yamlBytes = append(yamlBytes, finalBytes...)
}
for _, vap := range vaps {
finalBytes, err := yaml.Marshal(vap)
if err != nil {
fmt.Fprintf(out, " ERROR: converting to yaml: %s", err)
fmt.Fprintln(out)
return
}
yamlBytes = append(yamlBytes, []byte("---\n")...)
yamlBytes = append(yamlBytes, finalBytes...)
}
if err := os.WriteFile(path, yamlBytes, os.ModePerm); err != nil {
fmt.Fprintf(out, " ERROR: saving file (%s): %s", path, err)
fmt.Fprintln(out)
return
}
fmt.Fprintln(out, " OK")
}
}

View file

@ -0,0 +1,82 @@
package fix
import (
"fmt"
"reflect"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
apiutils "github.com/kyverno/kyverno/pkg/utils/api"
)
func FixPolicy(policy kyvernov1.PolicyInterface) ([]string, error) {
var messages []string
spec := policy.GetSpec()
if spec.ValidationFailureAction.Enforce() {
spec.ValidationFailureAction = kyvernov1.Enforce
} else {
spec.ValidationFailureAction = kyvernov1.Audit
}
for i := range spec.Rules {
rule := &spec.Rules[i]
if !reflect.DeepEqual(rule.MatchResources.ResourceDescription, kyvernov1.ResourceDescription{}) || !reflect.DeepEqual(rule.MatchResources.UserInfo, kyvernov1.UserInfo{}) {
messages = append(messages, "match uses old syntax, moving to any")
rule.MatchResources.Any = append(rule.MatchResources.Any, kyvernov1.ResourceFilter{
ResourceDescription: rule.MatchResources.ResourceDescription,
UserInfo: rule.MatchResources.UserInfo,
})
rule.MatchResources.ResourceDescription = kyvernov1.ResourceDescription{}
rule.MatchResources.UserInfo = kyvernov1.UserInfo{}
}
if !reflect.DeepEqual(rule.ExcludeResources.ResourceDescription, kyvernov1.ResourceDescription{}) || !reflect.DeepEqual(rule.ExcludeResources.UserInfo, kyvernov1.UserInfo{}) {
messages = append(messages, "exclude uses old syntax, moving to any")
rule.ExcludeResources.Any = append(rule.ExcludeResources.Any, kyvernov1.ResourceFilter{
ResourceDescription: rule.ExcludeResources.ResourceDescription,
UserInfo: rule.ExcludeResources.UserInfo,
})
rule.ExcludeResources.ResourceDescription = kyvernov1.ResourceDescription{}
rule.ExcludeResources.UserInfo = kyvernov1.UserInfo{}
}
preconditions := rule.GetAnyAllConditions()
if preconditions != nil {
cond, err := apiutils.ApiextensionsJsonToKyvernoConditions(preconditions)
if err != nil {
return messages, err
}
var newCond *kyvernov1.AnyAllConditions
switch typedValue := cond.(type) {
case kyvernov1.AnyAllConditions:
newCond = &typedValue
case []kyvernov1.Condition: // backwards compatibility
newCond = &kyvernov1.AnyAllConditions{
AllConditions: typedValue,
}
default:
return messages, fmt.Errorf("unknown preconditions type: %T", typedValue)
}
fixCondition := func(c *kyvernov1.Condition) {
switch c.Operator {
case "Equal":
messages = append(messages, "condition uses old operator `Equal`, updating")
c.Operator = "Equals"
case "NotEqual":
messages = append(messages, "condition uses old operator `NotEqual`, updating")
c.Operator = "NotEquals"
case "In":
messages = append(messages, "condition uses old operator `In`, updating")
c.Operator = "AllIn"
case "NotIn":
messages = append(messages, "condition uses old operator `NotIn`, updating")
c.Operator = "AnyNotIn"
}
}
for c := range newCond.AnyConditions {
fixCondition(&newCond.AnyConditions[c])
}
for c := range newCond.AllConditions {
fixCondition(&newCond.AllConditions[c])
}
rule.SetAnyAllConditions(newCond)
}
}
return messages, nil
}

View file

@ -32,15 +32,67 @@ var (
clusterPolicyV2 = schema.GroupVersion(kyvernov2beta1.GroupVersion).WithKind("ClusterPolicy")
vapV1 = v1alpha1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy")
client = openapiclient.NewComposite(
openapiclient.NewHardcodedBuiltins("1.27"),
openapiclient.NewHardcodedBuiltins("1.28"),
openapiclient.NewLocalCRDFiles(data.Crds(), data.CrdsFolder),
)
LegacyLoader = yamlutils.GetPolicy
KubectlValidateLoader = kubectlValidateLoader
defaultLoader = func(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
if experimental.UseKubectlValidate() {
return KubectlValidateLoader(bytes)
} else {
return LegacyLoader(bytes)
}
}
)
func getPolicies(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
if !experimental.UseKubectlValidate() {
return yamlutils.GetPolicy(bytes)
type loader = func([]byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error)
func Load(fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
return LoadWithLoader(nil, fs, resourcePath, paths...)
}
func LoadWithLoader(loader loader, fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
if loader == nil {
loader = defaultLoader
}
var pols []kyvernov1.PolicyInterface
var vaps []v1alpha1.ValidatingAdmissionPolicy
for _, path := range paths {
if source.IsStdin(path) {
p, v, err := stdinLoad(loader)
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else if fs != nil {
p, v, err := gitLoad(loader, fs, filepath.Join(resourcePath, path))
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else if source.IsHttp(path) {
p, v, err := httpLoad(loader, path)
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else {
p, v, err := fsLoad(loader, path)
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
}
}
return pols, vaps, nil
}
func kubectlValidateLoader(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
var policies []kyvernov1.PolicyInterface
var validatingAdmissionPolicies []v1alpha1.ValidatingAdmissionPolicy
documents, err := yamlutils.SplitDocuments(bytes)
@ -81,44 +133,7 @@ func getPolicies(bytes []byte) ([]kyvernov1.PolicyInterface, []v1alpha1.Validati
return policies, validatingAdmissionPolicies, nil
}
func Load(fs billy.Filesystem, resourcePath string, paths ...string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
var pols []kyvernov1.PolicyInterface
var vaps []v1alpha1.ValidatingAdmissionPolicy
for _, path := range paths {
if source.IsStdin(path) {
p, v, err := stdinLoad()
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else if fs != nil {
p, v, err := gitLoad(fs, filepath.Join(resourcePath, path))
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else if source.IsHttp(path) {
p, v, err := httpLoad(path)
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
} else {
p, v, err := fsLoad(path)
if err != nil {
return nil, nil, err
}
pols = append(pols, p...)
vaps = append(vaps, v...)
}
}
return pols, vaps, nil
}
func fsLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
func fsLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
var pols []kyvernov1.PolicyInterface
var vaps []v1alpha1.ValidatingAdmissionPolicy
fi, err := os.Stat(filepath.Clean(path))
@ -131,7 +146,7 @@ func fsLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmi
return nil, nil, err
}
for _, file := range files {
p, v, err := fsLoad(filepath.Join(path, file.Name()))
p, v, err := fsLoad(loader, filepath.Join(path, file.Name()))
if err != nil {
return nil, nil, err
}
@ -143,7 +158,7 @@ func fsLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmi
if err != nil {
return nil, nil, err
}
p, v, err := getPolicies(fileBytes)
p, v, err := loader(fileBytes)
if err != nil {
return nil, nil, err
}
@ -153,7 +168,7 @@ func fsLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmi
return pols, vaps, nil
}
func httpLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
func httpLoad(loader loader, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
// We accept here that a random URL might be called based on user provided input.
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, path, nil)
if err != nil {
@ -171,10 +186,10 @@ func httpLoad(path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAd
if err != nil {
return nil, nil, fmt.Errorf("failed to process %v: %v", path, err)
}
return getPolicies(fileBytes)
return loader(fileBytes)
}
func gitLoad(fs billy.Filesystem, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
func gitLoad(loader loader, fs billy.Filesystem, path string) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
file, err := fs.Open(path)
if err != nil {
return nil, nil, err
@ -183,14 +198,14 @@ func gitLoad(fs billy.Filesystem, path string) ([]kyvernov1.PolicyInterface, []v
if err != nil {
return nil, nil, err
}
return getPolicies(fileBytes)
return loader(fileBytes)
}
func stdinLoad() ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
func stdinLoad(loader loader) ([]kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, error) {
policyStr := ""
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
policyStr = policyStr + scanner.Text() + "\n"
}
return getPolicies([]byte(policyStr))
return loader([]byte(policyStr))
}

View file

@ -5,7 +5,6 @@ import (
"github.com/go-git/go-billy/v5"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/experimental"
"github.com/stretchr/testify/assert"
"k8s.io/api/admissionregistration/v1alpha1"
)
@ -88,8 +87,7 @@ func TestLoadWithKubectlValidate(t *testing.T) {
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(experimental.KubectlValidateEnv, "true")
policies, vaps, err := Load(tt.fs, tt.resourcePath, tt.paths...)
policies, vaps, err := LoadWithLoader(KubectlValidateLoader, tt.fs, tt.resourcePath, tt.paths...)
if (err != nil) != tt.wantErr {
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
return

View file

@ -49,5 +49,6 @@ kyverno fix [flags]
### SEE ALSO
* [kyverno](kyverno.md) - Kubernetes Native Policy Management.
* [kyverno fix policy](kyverno_fix_policy.md) - Fix inconsistencies and deprecated usage in Kyverno policy files.
* [kyverno fix test](kyverno_fix_test.md) - Fix inconsistencies and deprecated usage in Kyverno test files.

View file

@ -0,0 +1,53 @@
## kyverno fix policy
Fix inconsistencies and deprecated usage in Kyverno policy files.
### Synopsis
Fix inconsistencies and deprecated usage in Kyverno policy files.
NOTE: This is an experimental command, use `KYVERNO_EXPERIMENTAL=true` to enable it.
```
kyverno fix policy [dir]... [flags]
```
### Examples
```
# Fix Kyverno policy files
KYVERNO_EXPERIMENTAL=true kyverno fix policy .
# Fix Kyverno policy files and save them back
KYVERNO_EXPERIMENTAL=true kyverno fix policy . --save
```
### Options
```
-h, --help help for policy
--save Save fixed file
```
### Options inherited from parent commands
```
--add_dir_header If true, adds the file directory to the header of the log messages
--alsologtostderr log to standard error as well as files (no effect when -logtostderr=true)
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory (no effect when -logtostderr=true)
--log_file string If non-empty, use this log file (no effect when -logtostderr=true)
--log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files (no effect when -logtostderr=true)
--stderrthreshold severity logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false) (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
### SEE ALSO
* [kyverno fix](kyverno_fix.md) - Fix inconsistencies and deprecated usage of Kyverno resources.