package policy import ( "bufio" "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" "github.com/go-git/go-billy/v5" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1" policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/data" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/experimental" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/source" "github.com/kyverno/kyverno/ext/resource/convert" resourceloader "github.com/kyverno/kyverno/ext/resource/loader" extyaml "github.com/kyverno/kyverno/ext/yaml" "github.com/kyverno/kyverno/pkg/utils/git" "github.com/pkg/errors" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "sigs.k8s.io/kubectl-validate/pkg/openapiclient" ) var ( policyV1 = kyvernov1.SchemeGroupVersion.WithKind("Policy") policyV2 = kyvernov2beta1.SchemeGroupVersion.WithKind("Policy") clusterPolicyV1 = kyvernov1.SchemeGroupVersion.WithKind("ClusterPolicy") clusterPolicyV2 = kyvernov2beta1.SchemeGroupVersion.WithKind("ClusterPolicy") vapV1 = admissionregistrationv1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy") vapBindingV1 = admissionregistrationv1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicyBinding") vpV1alpha1 = policiesv1alpha1.SchemeGroupVersion.WithKind("ValidatingPolicy") LegacyLoader = legacyLoader KubectlValidateLoader = kubectlValidateLoader defaultLoader = func(path string, bytes []byte) (*LoaderResults, error) { if experimental.UseKubectlValidate() { return KubectlValidateLoader(path, bytes) } else { return LegacyLoader(path, bytes) } } ) type LoaderError struct { Path string Error error } type LoaderResults struct { Policies []kyvernov1.PolicyInterface VAPs []admissionregistrationv1.ValidatingAdmissionPolicy VAPBindings []admissionregistrationv1.ValidatingAdmissionPolicyBinding ValidatingPolicies []policiesv1alpha1.ValidatingPolicy NonFatalErrors []LoaderError } func (l *LoaderResults) merge(results *LoaderResults) { if results == nil { return } l.Policies = append(l.Policies, results.Policies...) l.VAPs = append(l.VAPs, results.VAPs...) l.VAPBindings = append(l.VAPBindings, results.VAPBindings...) l.ValidatingPolicies = append(l.ValidatingPolicies, results.ValidatingPolicies...) l.NonFatalErrors = append(l.NonFatalErrors, results.NonFatalErrors...) } func (l *LoaderResults) addError(path string, err error) { l.NonFatalErrors = append(l.NonFatalErrors, LoaderError{ Path: path, Error: err, }) } type loader = func(string, []byte) (*LoaderResults, error) func Load(fs billy.Filesystem, resourcePath string, paths ...string) (*LoaderResults, error) { return LoadWithLoader(nil, fs, resourcePath, paths...) } func LoadWithLoader(loader loader, fs billy.Filesystem, resourcePath string, paths ...string) (*LoaderResults, error) { if loader == nil { loader = defaultLoader } aggregateResults := &LoaderResults{} for _, path := range paths { var err error var results *LoaderResults if source.IsStdin(path) { results, err = stdinLoad(loader) } else if fs != nil { results, err = gitLoad(loader, fs, filepath.Join(resourcePath, path)) } else if source.IsHttp(path) { results, err = httpLoad(loader, path) } else { results, err = fsLoad(loader, path) } if err != nil { return nil, err } aggregateResults.merge(results) } // It's hard to use apply with the fake client, so disable all server side // https://github.com/kubernetes/kubernetes/issues/99953 for _, policy := range aggregateResults.Policies { policy.GetSpec().UseServerSideApply = false } return aggregateResults, nil } func kubectlValidateLoader(path string, content []byte) (*LoaderResults, error) { documents, err := extyaml.SplitDocuments(content) if err != nil { return nil, err } results := &LoaderResults{} crds, err := data.Crds() if err != nil { return nil, err } factory, err := resourceloader.New(openapiclient.NewComposite( openapiclient.NewHardcodedBuiltins("1.30"), openapiclient.NewLocalCRDFiles(crds), )) if err != nil { return nil, err } for _, document := range documents { gvk, untyped, err := factory.Load(document) if err != nil { msg := err.Error() if strings.Contains(msg, "Invalid value: value provided for unknown field") { return nil, err } // skip non-Kubernetes YAMLs and invalid types results.addError(path, err) continue } switch gvk { case policyV1, policyV2: typed, err := convert.To[kyvernov1.Policy](untyped) if err != nil { return nil, err } results.Policies = append(results.Policies, typed) case clusterPolicyV1, clusterPolicyV2: typed, err := convert.To[kyvernov1.ClusterPolicy](untyped) if err != nil { return nil, err } results.Policies = append(results.Policies, typed) case vapV1: typed, err := convert.To[admissionregistrationv1.ValidatingAdmissionPolicy](untyped) if err != nil { return nil, err } results.VAPs = append(results.VAPs, *typed) case vapBindingV1: typed, err := convert.To[admissionregistrationv1.ValidatingAdmissionPolicyBinding](untyped) if err != nil { return nil, err } results.VAPBindings = append(results.VAPBindings, *typed) case vpV1alpha1: typed, err := convert.To[policiesv1alpha1.ValidatingPolicy](untyped) if err != nil { return nil, err } results.ValidatingPolicies = append(results.ValidatingPolicies, *typed) default: return nil, fmt.Errorf("policy type not supported %s", gvk) } } return results, nil } func fsLoad(loader loader, path string) (*LoaderResults, error) { fi, err := os.Stat(filepath.Clean(path)) if err != nil { return nil, err } if strings.HasPrefix(fi.Name(), ".") { // skip hidden files and dirs return nil, err } aggregateResults := &LoaderResults{} if fi.IsDir() { files, err := os.ReadDir(path) if err != nil { return nil, errors.Wrapf(err, "failed to read %s", path) } for _, file := range files { results, err := fsLoad(loader, filepath.Join(path, file.Name())) if err != nil { return nil, errors.Wrapf(err, "failed to load %s", path) } aggregateResults.merge(results) } } else if git.IsYaml(fi) { fileBytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, errors.Wrapf(err, "failed to read file %s", path) } results, err := loader(path, fileBytes) if err != nil { return nil, errors.Wrapf(err, "failed to load file %s", path) } aggregateResults.merge(results) } return aggregateResults, nil } func httpLoad(loader loader, path string) (*LoaderResults, 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 { return nil, fmt.Errorf("failed to process %v: %v", path, err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to process %v: %v", path, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to process %v: %v", path, err) } fileBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to process %v: %v", path, err) } return loader(path, fileBytes) } func gitLoad(loader loader, fs billy.Filesystem, path string) (*LoaderResults, error) { file, err := fs.Open(path) if err != nil { return nil, err } fileBytes, err := io.ReadAll(file) if err != nil { return nil, err } return loader(path, fileBytes) } func stdinLoad(loader loader) (*LoaderResults, error) { policyStr := "" scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { policyStr = policyStr + scanner.Text() + "\n" } return loader("-", []byte(policyStr)) }