k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/api_linter.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package generators
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"sort"
    25  
    26  	"k8s.io/kube-openapi/pkg/generators/rules"
    27  
    28  	"k8s.io/gengo/v2/generator"
    29  	"k8s.io/gengo/v2/types"
    30  	"k8s.io/klog/v2"
    31  )
    32  
    33  const apiViolationFileType = "api-violation"
    34  
    35  type apiViolationFile struct {
    36  	// Since our file actually is unrelated to the package structure, use a
    37  	// path that hasn't been mangled by the framework.
    38  	unmangledPath string
    39  }
    40  
    41  func (a apiViolationFile) AssembleFile(f *generator.File, path string) error {
    42  	path = a.unmangledPath
    43  	klog.V(2).Infof("Assembling file %q", path)
    44  	if path == "-" {
    45  		_, err := io.Copy(os.Stdout, &f.Body)
    46  		return err
    47  	}
    48  
    49  	output, err := os.Create(path)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	defer output.Close()
    54  	_, err = io.Copy(output, &f.Body)
    55  	return err
    56  }
    57  
    58  func (a apiViolationFile) VerifyFile(f *generator.File, path string) error {
    59  	if path == "-" {
    60  		// Nothing to verify against.
    61  		return nil
    62  	}
    63  	path = a.unmangledPath
    64  
    65  	formatted := f.Body.Bytes()
    66  	existing, err := os.ReadFile(path)
    67  	if err != nil {
    68  		return fmt.Errorf("unable to read file %q for comparison: %v", path, err)
    69  	}
    70  	if bytes.Compare(formatted, existing) == 0 {
    71  		return nil
    72  	}
    73  
    74  	// Be nice and find the first place where they differ
    75  	// (Copied from gengo's default file type)
    76  	i := 0
    77  	for i < len(formatted) && i < len(existing) && formatted[i] == existing[i] {
    78  		i++
    79  	}
    80  	eDiff, fDiff := existing[i:], formatted[i:]
    81  	if len(eDiff) > 100 {
    82  		eDiff = eDiff[:100]
    83  	}
    84  	if len(fDiff) > 100 {
    85  		fDiff = fDiff[:100]
    86  	}
    87  	return fmt.Errorf("output for %q differs; first existing/expected diff: \n  %q\n  %q", path, string(eDiff), string(fDiff))
    88  }
    89  
    90  func newAPIViolationGen() *apiViolationGen {
    91  	return &apiViolationGen{
    92  		linter: newAPILinter(),
    93  	}
    94  }
    95  
    96  type apiViolationGen struct {
    97  	generator.GoGenerator
    98  
    99  	linter *apiLinter
   100  }
   101  
   102  func (v *apiViolationGen) FileType() string { return apiViolationFileType }
   103  func (v *apiViolationGen) Filename() string {
   104  	return "this file is ignored by the file assembler"
   105  }
   106  
   107  func (v *apiViolationGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
   108  	klog.V(5).Infof("validating API rules for type %v", t)
   109  	if err := v.linter.validate(t); err != nil {
   110  		return err
   111  	}
   112  	return nil
   113  }
   114  
   115  // Finalize prints the API rule violations to report file (if specified from
   116  // arguments) or stdout (default)
   117  func (v *apiViolationGen) Finalize(c *generator.Context, w io.Writer) error {
   118  	// NOTE: we don't return error here because we assume that the report file will
   119  	// get evaluated afterwards to determine if error should be raised. For example,
   120  	// you can have make rules that compare the report file with existing known
   121  	// violations (whitelist) and determine no error if no change is detected.
   122  	v.linter.report(w)
   123  	return nil
   124  }
   125  
   126  // apiLinter is the framework hosting multiple API rules and recording API rule
   127  // violations
   128  type apiLinter struct {
   129  	// API rules that implement APIRule interface and output API rule violations
   130  	rules      []APIRule
   131  	violations []apiViolation
   132  }
   133  
   134  // newAPILinter creates an apiLinter object with API rules in package rules. Please
   135  // add APIRule here when new API rule is implemented.
   136  func newAPILinter() *apiLinter {
   137  	return &apiLinter{
   138  		rules: []APIRule{
   139  			&rules.NamesMatch{},
   140  			&rules.OmitEmptyMatchCase{},
   141  			&rules.ListTypeMissing{},
   142  		},
   143  	}
   144  }
   145  
   146  // apiViolation uniquely identifies single API rule violation
   147  type apiViolation struct {
   148  	// Name of rule from APIRule.Name()
   149  	rule string
   150  
   151  	packageName string
   152  	typeName    string
   153  
   154  	// Optional: name of field that violates API rule. Empty fieldName implies that
   155  	// the entire type violates the rule.
   156  	field string
   157  }
   158  
   159  // apiViolations implements sort.Interface for []apiViolation based on the fields: rule,
   160  // packageName, typeName and field.
   161  type apiViolations []apiViolation
   162  
   163  func (a apiViolations) Len() int      { return len(a) }
   164  func (a apiViolations) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   165  func (a apiViolations) Less(i, j int) bool {
   166  	if a[i].rule != a[j].rule {
   167  		return a[i].rule < a[j].rule
   168  	}
   169  	if a[i].packageName != a[j].packageName {
   170  		return a[i].packageName < a[j].packageName
   171  	}
   172  	if a[i].typeName != a[j].typeName {
   173  		return a[i].typeName < a[j].typeName
   174  	}
   175  	return a[i].field < a[j].field
   176  }
   177  
   178  // APIRule is the interface for validating API rule on Go types
   179  type APIRule interface {
   180  	// Validate evaluates API rule on type t and returns a list of field names in
   181  	// the type that violate the rule. Empty field name [""] implies the entire
   182  	// type violates the rule.
   183  	Validate(t *types.Type) ([]string, error)
   184  
   185  	// Name returns the name of APIRule
   186  	Name() string
   187  }
   188  
   189  // validate runs all API rules on type t and records any API rule violation
   190  func (l *apiLinter) validate(t *types.Type) error {
   191  	for _, r := range l.rules {
   192  		klog.V(5).Infof("validating API rule %v for type %v", r.Name(), t)
   193  		fields, err := r.Validate(t)
   194  		if err != nil {
   195  			return err
   196  		}
   197  		for _, field := range fields {
   198  			l.violations = append(l.violations, apiViolation{
   199  				rule:        r.Name(),
   200  				packageName: t.Name.Package,
   201  				typeName:    t.Name.Name,
   202  				field:       field,
   203  			})
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  // report prints any API rule violation to writer w and returns error if violation exists
   210  func (l *apiLinter) report(w io.Writer) error {
   211  	sort.Sort(apiViolations(l.violations))
   212  	for _, v := range l.violations {
   213  		fmt.Fprintf(w, "API rule violation: %s,%s,%s,%s\n", v.rule, v.packageName, v.typeName, v.field)
   214  	}
   215  	if len(l.violations) > 0 {
   216  		return fmt.Errorf("API rule violations exist")
   217  	}
   218  	return nil
   219  }