Skip to content

Commit 1d87b49

Browse files
authored
[AppManifest] Add generated function to associate a kind and version to its go resource.Kind (#775)
To build an apiserver runner, the go types for an app must be registered in the scheme prior to the point of the loopback interface creation to provide as a kubeConfig to the `NewApp` function. This creates a slight chicken-and-egg problem, where the app is the only thing that knows the specific go types for the kinds it manages, but needs a kubeConfig to be initialized for `ManagedKinds()` to be called by the runner. The AppManifest data (`app.ManifestData`) has all the information on the kinds managed by the app, _excepting_ the go types it uses, as the manifest data is assumed to be file-like, and can be loaded from disk or an apiserver as well. This PR is a proposal for a solution to this problem, by adding a generated function alongside the generated manifest that associates a kind (string) and version (string) to the app's corresponding generated `resource.Kind` implementation. The apiserver runner config can than consume that function in its `New` method, and allow it to make the associations based on the manifest for the app prior to loading the app itself. If an app author does not use the codegen, they can simply implement this function as well if using the apiserver runner.
1 parent 4eea7b8 commit 1d87b49

File tree

12 files changed

+241
-19
lines changed

12 files changed

+241
-19
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
cp ../grafana-app-sdk/codegen/cuekind/testing/*.cue kinds/
7171
mkdir -p cmp && cp -R ../grafana-app-sdk/codegen/testing/golden_generated/* cmp/
7272
mv cmp/manifest/*.txt cmp/crd/
73-
cp cmp/manifest/go/*.txt cmp/go/groupbygroup/
73+
cp cmp/manifest/go/groupbygroup/*.txt cmp/go/groupbygroup/
7474
find ./cmp -iname '*.txt' -exec bash -c 'mv -- "$1" "${1%.txt}"' bash {} \;
7575
- name: Generate code
7676
run: |
@@ -80,6 +80,7 @@ jobs:
8080
grafana-app-sdk generate --grouping=group --gogenpath=pkg/gen2 --tsgenpath=ts/gen2 --defencoding=yaml --manifest=customManifest
8181
grafana-app-sdk generate --grouping=group --gogenpath=pkg/gen2 --tsgenpath=ts/gen2 --defencoding=yaml --manifest=testManifest
8282
sed -i 's/^package gen2/package groupbygroup/g' pkg/gen2/*.go
83+
sed -i 's/codegen\-tests\/pkg\/gen2/codegen\-tests\/pkg\/generated/g' pkg/gen2/*.go
8384
diff pkg/gen1/customkind cmp/go/groupbykind/customkind > diff.txt
8485
sed -i '/^Common subdirectories/d' diff.txt
8586
difflines=$(wc -l diff.txt | awk '{ print $1 }')

cmd/grafana-app-sdk/generate.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Allowed values are 'group' and 'kind'. Dictates the packaging of go kinds, where
4343
generateCmd.Flags().Lookup("postprocess").NoOptDefVal = "true"
4444
generateCmd.Flags().Bool("noschemasinmanifest", false, "Whether to exclude kind schemas from the generated app manifest. This flag exists to allow for codegen with recursive types in CUE until github.com/grafana/grafana-app-sdk/issues/460 is resolved.")
4545
generateCmd.Flags().Lookup("noschemasinmanifest").NoOptDefVal = "true"
46+
generateCmd.Flags().String("gomodule", "", `module name found in go.mod. If absent it will be inferred from ./go.mod`)
47+
generateCmd.Flags().String("gomodgenpath", "", `This argument is used as a relative path for generated go code from the go module root. It only needs to be present if gogenpath is an absolute path, or is not a relative path from the go module root.`)
4648

4749
// Don't show "usage" information when an error is returned form the command,
4850
// because our errors are not command-usage-based
@@ -105,11 +107,32 @@ func generateCmdFunc(cmd *cobra.Command, _ []string) error {
105107
if err != nil {
106108
return err
107109
}
110+
goModule, err := cmd.Flags().GetString("gomodule")
111+
if err != nil {
112+
return err
113+
}
114+
goModGenPath, err := cmd.Flags().GetString("gomodgenpath")
115+
if err != nil {
116+
return err
117+
}
118+
119+
if goModule == "" {
120+
goModule, err = getGoModule("go.mod")
121+
if err != nil {
122+
return fmt.Errorf("unable to load go module from ./go.mod: %w. Use --gomodule to set a value", err)
123+
}
124+
}
125+
126+
if goModGenPath == "" {
127+
goModGenPath = goGenPath
128+
}
108129

109130
var files codejen.Files
110131
switch format {
111132
case FormatCUE:
112133
files, err = generateKindsCue(os.DirFS(sourcePath), kindGenConfig{
134+
GoModuleName: goModule,
135+
GoModuleGenBasePath: goModGenPath,
113136
GoGenBasePath: goGenPath,
114137
TSGenBasePath: tsGenPath,
115138
CRDEncoding: encType,
@@ -158,6 +181,8 @@ func generateCmdFunc(cmd *cobra.Command, _ []string) error {
158181
}
159182

160183
type kindGenConfig struct {
184+
GoModuleName string
185+
GoModuleGenBasePath string
161186
GoGenBasePath string
162187
TSGenBasePath string
163188
CRDEncoding string
@@ -217,7 +242,7 @@ func generateKindsCue(modFS fs.FS, cfg kindGenConfig, selectors ...string) (code
217242
}
218243

219244
// Manifest
220-
goManifestFiles, err := generatorForManifest.Generate(cuekind.ManifestGoGenerator(filepath.Base(cfg.GoGenBasePath), cfg.ManifestIncludeSchemas), selectors...)
245+
goManifestFiles, err := generatorForManifest.Generate(cuekind.ManifestGoGenerator(filepath.Base(cfg.GoGenBasePath), cfg.ManifestIncludeSchemas, cfg.GoModuleName, cfg.GoModuleGenBasePath, cfg.GroupKinds), selectors...)
221246
if err != nil {
222247
return nil, err
223248
}

codegen/cuekind/generators.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,14 @@ func ManifestGenerator(encoder jennies.ManifestOutputEncoder, extension string,
133133
return g
134134
}
135135

136-
func ManifestGoGenerator(pkg string, includeSchemas bool) *codejen.JennyList[codegen.AppManifest] {
136+
func ManifestGoGenerator(pkg string, includeSchemas bool, projectRepo, goGenPath string, groupKinds bool) *codejen.JennyList[codegen.AppManifest] {
137137
g := codejen.JennyListWithNamer[codegen.AppManifest](namerFuncManifest)
138138
g.Append(&jennies.ManifestGoGenerator{
139139
Package: pkg,
140140
IncludeSchemas: includeSchemas,
141+
ProjectRepo: projectRepo,
142+
CodegenPath: goGenPath,
143+
GroupByKind: !groupKinds,
141144
})
142145
return g
143146
}

codegen/cuekind/generators_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,28 @@ func TestManifestGoGenerator(t *testing.T) {
130130
parser, err := NewParser()
131131
require.Nil(t, err)
132132

133-
t.Run("resource", func(t *testing.T) {
133+
t.Run("group by group", func(t *testing.T) {
134134
kinds, err := parser.ManifestParser(true).Parse(os.DirFS(TestCUEDirectory), "testManifest")
135135
require.Nil(t, err)
136-
files, err := ManifestGoGenerator("groupbygroup", true).Generate(kinds...)
136+
files, err := ManifestGoGenerator("groupbygroup", true, "codegen-tests", "pkg/generated", true).Generate(kinds...)
137+
require.Nil(t, err)
138+
// Check number of files generated
139+
// 5 -> object, spec, metadata, status, schema
140+
assert.Len(t, files, 1)
141+
// Check content against the golden files
142+
compareToGolden(t, files, "manifest/go/groupbygroup")
143+
})
144+
145+
t.Run("group by kind", func(t *testing.T) {
146+
kinds, err := parser.ManifestParser(true).Parse(os.DirFS(TestCUEDirectory), "customManifest")
147+
require.Nil(t, err)
148+
files, err := ManifestGoGenerator("groupbykind", true, "codegen-tests", "pkg/generated", false).Generate(kinds...)
137149
require.Nil(t, err)
138150
// Check number of files generated
139151
// 5 -> object, spec, metadata, status, schema
140152
assert.Len(t, files, 1)
141153
// Check content against the golden files
142-
compareToGolden(t, files, "manifest/go")
154+
compareToGolden(t, files, "manifest/go/groupbykind")
143155
})
144156
}
145157

codegen/jennies/manifest.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func (m *ManifestGenerator) Generate(appManifest codegen.AppManifest) (codejen.F
7777

7878
type ManifestGoGenerator struct {
7979
Package string
80+
ProjectRepo string
81+
CodegenPath string
82+
GroupByKind bool
8083
IncludeSchemas bool
8184
}
8285

@@ -101,8 +104,11 @@ func (g *ManifestGoGenerator) Generate(appManifest codegen.AppManifest) (codejen
101104

102105
buf := bytes.Buffer{}
103106
err = templates.WriteManifestGoFile(templates.ManifestGoFileMetadata{
104-
Package: g.Package,
105-
ManifestData: *manifestData,
107+
Package: g.Package,
108+
Repo: g.ProjectRepo,
109+
CodegenPath: g.CodegenPath,
110+
KindsAreGrouped: !g.GroupByKind,
111+
ManifestData: *manifestData,
106112
}, &buf)
107113
if err != nil {
108114
return nil, err

codegen/templates/manifest_go.tmpl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import (
99
"encoding/json"
1010

1111
"github.com/grafana/grafana-app-sdk/app"
12+
"github.com/grafana/grafana-app-sdk/resource"
1213
"k8s.io/kube-openapi/pkg/spec3"
1314
"k8s.io/kube-openapi/pkg/validation/spec"
15+
16+
{{ range .Packages }}{{ . }}
17+
{{ end }}
1418
)
1519

1620
{{ define "schema" }}
@@ -190,3 +194,14 @@ func RemoteManifest() app.Manifest {
190194
return app.NewAPIServerManifest("{{ .ManifestData.AppName }}")
191195
}
192196

197+
var kindVersionToGoType = map[string]resource.Kind { {{ range .ManifestData.Kinds }}{{$k:=.}}{{ range .Versions }}
198+
"{{ $k.Kind }}/{{ .Name }}": {{ if $.KindsAreGrouped }}{{ $.ToPackageName .Name}}.{{ $.GoKindName $k.Kind }}Kind(){{ else }}{{ $.KindToPackageName $k.Kind }}{{ $.ToPackageName .Name }}.Kind(){{ end }},{{ end }}{{ end }}
199+
}
200+
201+
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
202+
// If there is no association for the provided Kind and Version, exists will return false.
203+
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
204+
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
205+
return goType, exists
206+
}
207+

codegen/templates/templates.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"path/filepath"
89
"regexp"
10+
"slices"
911
"strings"
1012
"text/template"
1113

@@ -388,8 +390,11 @@ func WriteOperatorConfig(out io.Writer) error {
388390
}
389391

390392
type ManifestGoFileMetadata struct {
391-
Package string
392-
ManifestData app.ManifestData
393+
Package string
394+
Repo string
395+
CodegenPath string
396+
KindsAreGrouped bool
397+
ManifestData app.ManifestData
393398
}
394399

395400
func (ManifestGoFileMetadata) ToAdmissionOperationName(input app.AdmissionOperation) string {
@@ -423,6 +428,45 @@ func (ManifestGoFileMetadata) ToPackageName(input string) string {
423428
return ToPackageName(input)
424429
}
425430

431+
func (ManifestGoFileMetadata) KindToPackageName(input string) string {
432+
return ToPackageName(strings.ToLower(input))
433+
}
434+
435+
func (ManifestGoFileMetadata) GroupToPackageName(input string) string {
436+
return ToPackageName(strings.Split(input, ".")[0])
437+
}
438+
439+
func (ManifestGoFileMetadata) GoKindName(kind string) string {
440+
if len(kind) > 0 {
441+
return strings.ToUpper(kind[:1]) + kind[1:]
442+
}
443+
return strings.ToUpper(kind)
444+
}
445+
446+
func (m ManifestGoFileMetadata) Packages() []string {
447+
pkgs := make([]string, 0)
448+
if m.KindsAreGrouped {
449+
gvs := make(map[string]string)
450+
for _, k := range m.ManifestData.Kinds {
451+
for _, v := range k.Versions {
452+
gvs[fmt.Sprintf("%s/%s", m.GroupToPackageName(m.ManifestData.Group), ToPackageName(v.Name))] = ToPackageName(v.Name)
453+
}
454+
}
455+
for pkg, alias := range gvs {
456+
pkgs = append(pkgs, fmt.Sprintf("%s \"%s\"", alias, filepath.Join(m.Repo, m.CodegenPath, pkg)))
457+
}
458+
} else {
459+
for _, k := range m.ManifestData.Kinds {
460+
for _, v := range k.Versions {
461+
pkgs = append(pkgs, fmt.Sprintf("%s%s \"%s\"", m.KindToPackageName(k.Kind), ToPackageName(v.Name), filepath.Join(m.Repo, m.CodegenPath, m.KindToPackageName(k.Kind), ToPackageName(v.Name))))
462+
}
463+
}
464+
}
465+
// Sort for consistent output
466+
slices.Sort(pkgs)
467+
return pkgs
468+
}
469+
426470
func WriteManifestGoFile(metadata ManifestGoFileMetadata, out io.Writer) error {
427471
return templateManifestGoFile.Execute(out, metadata)
428472
}

codegen/testing/golden_generated/manifest/go/customapp_manifest.go.txt renamed to codegen/testing/golden_generated/manifest/go/groupbygroup/customapp_manifest.go.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ package groupbygroup
77

88
import (
99
"encoding/json"
10+
"fmt"
1011

1112
"github.com/grafana/grafana-app-sdk/app"
13+
"github.com/grafana/grafana-app-sdk/resource"
14+
15+
v0_0 "codegen-tests/pkg/generated/customapp/v0_0"
16+
v1_0 "codegen-tests/pkg/generated/customapp/v1_0"
1217
)
1318

1419
var (
@@ -50,3 +55,15 @@ func LocalManifest() app.Manifest {
5055
func RemoteManifest() app.Manifest {
5156
return app.NewAPIServerManifest("custom-app")
5257
}
58+
59+
var kindVersionToGoType = map[string]resource.Kind{
60+
"CustomKind/v0-0": v0_0.CustomKindKind(),
61+
"CustomKind/v1-0": v1_0.CustomKindKind(),
62+
}
63+
64+
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
65+
// If there is no association for the provided Kind and Version, exists will return false.
66+
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
67+
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
68+
return goType, exists
69+
}

codegen/testing/golden_generated/manifest/go/testapp_manifest.go.txt renamed to codegen/testing/golden_generated/manifest/go/groupbygroup/testapp_manifest.go.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ package groupbygroup
77

88
import (
99
"encoding/json"
10+
"fmt"
1011

1112
"github.com/grafana/grafana-app-sdk/app"
13+
"github.com/grafana/grafana-app-sdk/resource"
1214
"k8s.io/kube-openapi/pkg/spec3"
1315
"k8s.io/kube-openapi/pkg/validation/spec"
16+
17+
v1 "codegen-tests/pkg/generated/testapp/v1"
18+
v2 "codegen-tests/pkg/generated/testapp/v2"
19+
v3 "codegen-tests/pkg/generated/testapp/v3"
1420
)
1521

1622
var (
@@ -268,3 +274,17 @@ func LocalManifest() app.Manifest {
268274
func RemoteManifest() app.Manifest {
269275
return app.NewAPIServerManifest("test-app")
270276
}
277+
278+
var kindVersionToGoType = map[string]resource.Kind{
279+
"TestKind/v1": v1.TestKindKind(),
280+
"TestKind/v2": v2.TestKindKind(),
281+
"TestKind/v3": v3.TestKindKind(),
282+
"TestKind2/v1": v1.TestKind2Kind(),
283+
}
284+
285+
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
286+
// If there is no association for the provided Kind and Version, exists will return false.
287+
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
288+
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
289+
return goType, exists
290+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// This file is generated by grafana-app-sdk
3+
// DO NOT EDIT
4+
//
5+
6+
package groupbykind
7+
8+
import (
9+
"encoding/json"
10+
"fmt"
11+
12+
"github.com/grafana/grafana-app-sdk/app"
13+
"github.com/grafana/grafana-app-sdk/resource"
14+
15+
customkindv0_0 "codegen-tests/pkg/generated/customkind/v0_0"
16+
customkindv1_0 "codegen-tests/pkg/generated/customkind/v1_0"
17+
)
18+
19+
var (
20+
rawSchemaCustomKindv0_0 = []byte(`{"spec":{"properties":{"deprecatedField":{"type":"string"},"field1":{"type":"string"}},"required":["field1","deprecatedField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`)
21+
versionSchemaCustomKindv0_0 app.VersionSchema
22+
_ = json.Unmarshal(rawSchemaCustomKindv0_0, &versionSchemaCustomKindv0_0)
23+
rawSchemaCustomKindv1_0 = []byte(`{"spec":{"properties":{"boolField":{"default":false,"type":"boolean"},"enum":{"default":"default","enum":["default","val2","val3","val4","val1"],"type":"string"},"field1":{"type":"string"},"floatField":{"format":"double","type":"number"},"i32":{"maximum":123456,"minimum":-2147483648,"type":"integer"},"i64":{"maximum":9223372036854775807,"minimum":123456,"type":"integer"},"inner":{"properties":{"innerField1":{"type":"string"},"innerField2":{"items":{"type":"string"},"type":"array"},"innerField3":{"items":{"properties":{"details":{"type":"object","x-kubernetes-preserve-unknown-fields":true},"name":{"type":"string"}},"required":["name","details"],"type":"object"},"type":"array"},"innerField4":{"items":{"type":"object","x-kubernetes-preserve-unknown-fields":true},"type":"array"}},"required":["innerField1","innerField2","innerField3","innerField4"],"type":"object"},"map":{"additionalProperties":{"properties":{"details":{"type":"object","x-kubernetes-preserve-unknown-fields":true},"group":{"type":"string"}},"required":["group","details"],"type":"object"},"type":"object"},"timestamp":{"format":"date-time","type":"string"},"union":{"oneOf":[{"allOf":[{"required":["group"]},{"not":{"anyOf":[{"required":["group","details"]}]}}]},{"required":["group","details"]}],"properties":{"details":{"type":"object","x-kubernetes-preserve-unknown-fields":true},"group":{"type":"string"},"options":{"items":{"type":"string"},"type":"array"}},"type":"object"}},"required":["field1","inner","union","map","timestamp","enum","i32","i64","boolField","floatField"],"type":"object"},"status":{"properties":{"additionalFields":{"description":"additionalFields is reserved for future use","type":"object","x-kubernetes-preserve-unknown-fields":true},"operatorStates":{"additionalProperties":{"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"description":"details contains any extra information that is operator-specific","type":"object","x-kubernetes-preserve-unknown-fields":true},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"},"statusField1":{"type":"string"}},"required":["statusField1"],"type":"object","x-kubernetes-preserve-unknown-fields":true}}`)
24+
versionSchemaCustomKindv1_0 app.VersionSchema
25+
_ = json.Unmarshal(rawSchemaCustomKindv1_0, &versionSchemaCustomKindv1_0)
26+
)
27+
28+
var appManifestData = app.ManifestData{
29+
AppName: "custom-app",
30+
Group: "customapp.ext.grafana.com",
31+
Kinds: []app.ManifestKind{
32+
{
33+
Kind: "CustomKind",
34+
Scope: "Namespaced",
35+
Conversion: false,
36+
Versions: []app.ManifestKindVersion{
37+
{
38+
Name: "v0-0",
39+
Schema: &versionSchemaCustomKindv0_0,
40+
},
41+
42+
{
43+
Name: "v1-0",
44+
Schema: &versionSchemaCustomKindv1_0,
45+
},
46+
},
47+
},
48+
},
49+
}
50+
51+
func LocalManifest() app.Manifest {
52+
return app.NewEmbeddedManifest(appManifestData)
53+
}
54+
55+
func RemoteManifest() app.Manifest {
56+
return app.NewAPIServerManifest("custom-app")
57+
}
58+
59+
var kindVersionToGoType = map[string]resource.Kind{
60+
"CustomKind/v0-0": customkindv0_0.Kind(),
61+
"CustomKind/v1-0": customkindv1_0.Kind(),
62+
}
63+
64+
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
65+
// If there is no association for the provided Kind and Version, exists will return false.
66+
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
67+
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
68+
return goType, exists
69+
}

0 commit comments

Comments
 (0)