/* Copyright 2018 The Kubernetes Authors. 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 generators import ( "bytes" "fmt" "io" "io/ioutil" "os" "sort" "k8s.io/kube-openapi/pkg/generators/rules" "k8s.io/gengo/generator" "k8s.io/gengo/types" "k8s.io/klog" ) const apiViolationFileType = "api-violation" type apiViolationFile struct { // Since our file actually is unrelated to the package structure, use a // path that hasn't been mangled by the framework. unmangledPath string } func (a apiViolationFile) AssembleFile(f *generator.File, path string) error { path = a.unmangledPath klog.V(2).Infof("Assembling file %q", path) if path == "-" { _, err := io.Copy(os.Stdout, &f.Body) return err } output, err := os.Create(path) if err != nil { return err } defer output.Close() _, err = io.Copy(output, &f.Body) return err } func (a apiViolationFile) VerifyFile(f *generator.File, path string) error { if path == "-" { // Nothing to verify against. return nil } path = a.unmangledPath formatted := f.Body.Bytes() existing, err := ioutil.ReadFile(path) if err != nil { return fmt.Errorf("unable to read file %q for comparison: %v", path, err) } if bytes.Compare(formatted, existing) == 0 { return nil } // Be nice and find the first place where they differ // (Copied from gengo's default file type) i := 0 for i < len(formatted) && i < len(existing) && formatted[i] == existing[i] { i++ } eDiff, fDiff := existing[i:], formatted[i:] if len(eDiff) > 100 { eDiff = eDiff[:100] } if len(fDiff) > 100 { fDiff = fDiff[:100] } return fmt.Errorf("output for %q differs; first existing/expected diff: \n %q\n %q", path, string(eDiff), string(fDiff)) } func newAPIViolationGen() *apiViolationGen { return &apiViolationGen{ linter: newAPILinter(), } } type apiViolationGen struct { generator.DefaultGen linter *apiLinter } func (v *apiViolationGen) FileType() string { return apiViolationFileType } func (v *apiViolationGen) Filename() string { return "this file is ignored by the file assembler" } func (v *apiViolationGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { klog.V(5).Infof("validating API rules for type %v", t) if err := v.linter.validate(t); err != nil { return err } return nil } // Finalize prints the API rule violations to report file (if specified from // arguments) or stdout (default) func (v *apiViolationGen) Finalize(c *generator.Context, w io.Writer) error { // NOTE: we don't return error here because we assume that the report file will // get evaluated afterwards to determine if error should be raised. For example, // you can have make rules that compare the report file with existing known // violations (whitelist) and determine no error if no change is detected. v.linter.report(w) return nil } // apiLinter is the framework hosting multiple API rules and recording API rule // violations type apiLinter struct { // API rules that implement APIRule interface and output API rule violations rules []APIRule violations []apiViolation } // newAPILinter creates an apiLinter object with API rules in package rules. Please // add APIRule here when new API rule is implemented. func newAPILinter() *apiLinter { return &apiLinter{ rules: []APIRule{ &rules.NamesMatch{}, &rules.OmitEmptyMatchCase{}, }, } } // apiViolation uniquely identifies single API rule violation type apiViolation struct { // Name of rule from APIRule.Name() rule string packageName string typeName string // Optional: name of field that violates API rule. Empty fieldName implies that // the entire type violates the rule. field string } // apiViolations implements sort.Interface for []apiViolation based on the fields: rule, // packageName, typeName and field. type apiViolations []apiViolation func (a apiViolations) Len() int { return len(a) } func (a apiViolations) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a apiViolations) Less(i, j int) bool { if a[i].rule != a[j].rule { return a[i].rule < a[j].rule } if a[i].packageName != a[j].packageName { return a[i].packageName < a[j].packageName } if a[i].typeName != a[j].typeName { return a[i].typeName < a[j].typeName } return a[i].field < a[j].field } // APIRule is the interface for validating API rule on Go types type APIRule interface { // Validate evaluates API rule on type t and returns a list of field names in // the type that violate the rule. Empty field name [""] implies the entire // type violates the rule. Validate(t *types.Type) ([]string, error) // Name returns the name of APIRule Name() string } // validate runs all API rules on type t and records any API rule violation func (l *apiLinter) validate(t *types.Type) error { for _, r := range l.rules { klog.V(5).Infof("validating API rule %v for type %v", r.Name(), t) fields, err := r.Validate(t) if err != nil { return err } for _, field := range fields { l.violations = append(l.violations, apiViolation{ rule: r.Name(), packageName: t.Name.Package, typeName: t.Name.Name, field: field, }) } } return nil } // report prints any API rule violation to writer w and returns error if violation exists func (l *apiLinter) report(w io.Writer) error { sort.Sort(apiViolations(l.violations)) for _, v := range l.violations { fmt.Fprintf(w, "API rule violation: %s,%s,%s,%s\n", v.rule, v.packageName, v.typeName, v.field) } if len(l.violations) > 0 { return fmt.Errorf("API rule violations exist") } return nil }