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  }