github.com/octohelm/cuemod@v0.9.4/pkg/cueify/crd/extractor.go (about) 1 package helm 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "os" 8 "path/filepath" 9 "sort" 10 "strconv" 11 "strings" 12 13 cuetoken "cuelang.org/go/cue/token" 14 "k8s.io/apimachinery/pkg/util/yaml" 15 16 cueast "cuelang.org/go/cue/ast" 17 "github.com/octohelm/cuemod/pkg/cueify/core" 18 "github.com/pkg/errors" 19 20 apiextensions_v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 21 ) 22 23 func init() { 24 core.Register(&Extractor{}) 25 } 26 27 // Extractor from helm charts 28 // 29 // Targets: 30 // * gen values to value check 31 // * gen templates 32 type Extractor struct { 33 } 34 35 func (Extractor) Name() string { 36 return "crd" 37 } 38 39 // never detect 40 func (Extractor) Detect(ctx context.Context, src string) (bool, map[string]string) { 41 return false, nil 42 } 43 44 func (e *Extractor) Extract(ctx context.Context, src string) (files []*cueast.File, err error) { 45 crdFiles, err := filepath.Glob(filepath.Join(src, "*.yaml")) 46 if err != nil { 47 return nil, errors.Wrapf(err, "find crd.yaml failed from %s", src) 48 } 49 50 for i := range crdFiles { 51 data, err := os.ReadFile(crdFiles[i]) 52 if err != nil { 53 return nil, err 54 } 55 56 if trimmedContent := strings.TrimSpace(string(data)); trimmedContent != "" { 57 decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(trimmedContent), 4096) 58 59 for { 60 crd := apiextensions_v1.CustomResourceDefinition{} 61 62 if err := decoder.Decode(&crd); err != nil { 63 if err == io.EOF { 64 break 65 } 66 return nil, errors.Wrapf(err, "invalid crd failed: %s\n%s", crdFiles[i], trimmedContent) 67 } 68 69 if crd.Spec.Group == "" { 70 continue 71 } 72 73 cueFile, err := e.fileFromCRD(&crd) 74 if err != nil { 75 return nil, err 76 } 77 78 files = append(files, cueFile) 79 } 80 } 81 82 } 83 84 return 85 } 86 87 func (e *Extractor) fileFromCRD(crd *apiextensions_v1.CustomResourceDefinition) (*cueast.File, error) { 88 f := &cueast.File{} 89 f.Filename = crd.Name + "_gen.cue" 90 f.Decls = []cueast.Decl{ 91 &cueast.Package{Name: cueast.NewIdent("crd")}, 92 } 93 94 decl := func(d cueast.Decl) { 95 f.Decls = append(f.Decls, d) 96 } 97 98 for _, v := range crd.Spec.Versions { 99 if s, ok := v.Schema.OpenAPIV3Schema.Properties["kind"]; ok { 100 s.Enum = []apiextensions_v1.JSON{{Raw: []byte(strconv.Quote(crd.Spec.Names.Kind))}} 101 v.Schema.OpenAPIV3Schema.Properties["kind"] = s 102 v.Schema.OpenAPIV3Schema.Required = append(v.Schema.OpenAPIV3Schema.Required, "kind") 103 } 104 105 if s, ok := v.Schema.OpenAPIV3Schema.Properties["apiVersion"]; ok { 106 s.Enum = []apiextensions_v1.JSON{{Raw: []byte(strconv.Quote(crd.Spec.Group + "/" + v.Name))}} 107 v.Schema.OpenAPIV3Schema.Properties["apiVersion"] = s 108 v.Schema.OpenAPIV3Schema.Required = append(v.Schema.OpenAPIV3Schema.Required, "apiVersion") 109 } 110 111 if s, ok := v.Schema.OpenAPIV3Schema.Properties["metadata"]; ok { 112 s.Properties = map[string]apiextensions_v1.JSONSchemaProps{ 113 "name": {Type: "string"}, 114 "namespace": {Type: "string"}, 115 "labels": { 116 Type: "object", 117 AdditionalProperties: &apiextensions_v1.JSONSchemaPropsOrBool{ 118 Schema: &apiextensions_v1.JSONSchemaProps{ 119 Type: "string", 120 }, 121 }, 122 }, 123 "annotations": { 124 Type: "object", 125 AdditionalProperties: &apiextensions_v1.JSONSchemaPropsOrBool{ 126 Schema: &apiextensions_v1.JSONSchemaProps{ 127 Type: "string", 128 }, 129 }, 130 }, 131 } 132 133 v.Schema.OpenAPIV3Schema.Properties["metadata"] = s 134 } 135 136 decl(&cueast.Field{ 137 Label: cueast.NewIdent(v.Name), 138 Value: &cueast.StructLit{ 139 Elts: []cueast.Decl{ 140 &cueast.Field{ 141 Label: cueast.NewIdent("#" + crd.Spec.Names.Kind), 142 Value: e.fromJSONSchema(v.Schema.OpenAPIV3Schema), 143 }, 144 }, 145 }}) 146 } 147 148 return f, nil 149 } 150 151 func (e Extractor) fromJSONSchema(s *apiextensions_v1.JSONSchemaProps) cueast.Expr { 152 if len(s.AnyOf) > 0 { 153 items := make([]cueast.Expr, len(s.AnyOf)) 154 155 for i := range items { 156 items[i] = e.fromJSONSchema(&s.AnyOf[i]) 157 } 158 159 return cueast.NewBinExpr(cuetoken.OR, items...) 160 } 161 162 if len(s.Enum) > 0 { 163 items := make([]cueast.Expr, len(s.Enum)) 164 165 for i := range items { 166 items[i] = &cueast.BasicLit{ 167 // TODO handle struct value 168 Value: string(s.Enum[i].Raw), 169 } 170 } 171 172 return cueast.NewBinExpr(cuetoken.OR, items...) 173 } 174 175 switch s.Type { 176 case "object": 177 if len(s.Properties) == 0 && s.AdditionalProperties == nil { 178 s.AdditionalProperties = &apiextensions_v1.JSONSchemaPropsOrBool{Allows: true} 179 } 180 181 if s.AdditionalProperties != nil { 182 f := &cueast.Field{ 183 Label: cueast.NewList(cueast.NewIdent("string")), 184 } 185 186 if s.AdditionalProperties.Allows { 187 f.Value = any() 188 } 189 190 if s.AdditionalProperties.Schema != nil { 191 f.Value = e.fromJSONSchema(s.AdditionalProperties.Schema) 192 } 193 194 cueast.SetRelPos(f, cuetoken.Blank) 195 196 s := cueast.NewStruct(f) 197 s.Lbrace = cuetoken.Blank.Pos() 198 s.Rbrace = cuetoken.Blank.Pos() 199 200 return s 201 202 } 203 204 fields := make([]string, 0) 205 required := map[string]bool{} 206 207 for f := range s.Properties { 208 fields = append(fields, f) 209 } 210 211 for _, f := range s.Required { 212 required[f] = true 213 } 214 215 sort.Strings(fields) 216 217 cueFields := make([]interface{}, 0) 218 219 for _, f := range fields { 220 p := s.Properties[f] 221 222 field := &cueast.Field{Label: cueast.NewString(f), Value: e.fromJSONSchema(&p)} 223 224 if p.Description != "" { 225 addComments(field, &cueast.CommentGroup{Doc: true, List: []*cueast.Comment{ 226 toCueComment(p.Description), 227 }}) 228 } 229 230 if _, ok := required[f]; !ok { 231 field.Token = cuetoken.COLON 232 field.Optional = cuetoken.Blank.Pos() 233 } 234 235 cueFields = append(cueFields, field) 236 } 237 238 s := cueast.NewStruct(cueFields...) 239 return s 240 case "string": 241 return cueast.NewIdent("string") 242 case "integer": 243 switch s.Format { 244 case "int", "int8", "int16", "int32", "int64": 245 return cueast.NewIdent(s.Format) 246 } 247 return cueast.NewIdent("int") 248 case "number": 249 switch s.Format { 250 case "float": 251 return cueast.NewIdent("float32") 252 } 253 return cueast.NewIdent("float64") 254 case "boolean": 255 return cueast.NewIdent("bool") 256 case "array": 257 if s.Items == nil { 258 return cueast.NewList(&cueast.Ellipsis{ 259 Type: any(), 260 }) 261 } 262 263 if s.Items.Schema != nil { 264 elem := e.fromJSONSchema(s.Items.Schema) 265 if elem == nil { 266 return nil 267 } 268 return cueast.NewList(&cueast.Ellipsis{ 269 Type: elem, 270 }) 271 } 272 273 items := make([]cueast.Expr, len(s.Items.JSONSchemas)) 274 275 for i := range items { 276 items[i] = e.fromJSONSchema(&s.Items.JSONSchemas[i]) 277 } 278 279 return cueast.NewList() 280 } 281 282 return any() 283 } 284 285 func any() cueast.Expr { 286 return cueast.NewIdent("_") 287 } 288 289 func addComments(node cueast.Node, comments ...*cueast.CommentGroup) { 290 for i := range comments { 291 cg := comments[i] 292 if cg == nil { 293 continue 294 } 295 cueast.AddComment(node, comments[i]) 296 } 297 } 298 299 func toCueComment(d string) *cueast.Comment { 300 lines := strings.Split(d, "\n") 301 302 c := &cueast.Comment{} 303 304 buf := bytes.NewBuffer(nil) 305 306 for i := range lines { 307 if i > 0 { 308 buf.WriteString("\n") 309 } 310 311 buf.WriteString("// ") 312 buf.WriteString(lines[i]) 313 } 314 315 c.Text = buf.String() 316 317 return c 318 }