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 }