Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/definitions/app-manifest-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"spec": {
"appName": "app-manifest",
"appDisplayName": "",
"appDisplayName": "app-manifest",
"group": "apps.grafana.app",
"versions": [
{
Expand Down
1 change: 1 addition & 0 deletions app/manifestdata/appmanifest_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (

var appManifestData = app.ManifestData{
AppName: "app-manifest",
AppDisplayName: "app-manifest",
Group: "apps.grafana.app",
PreferredVersion: "v1alpha2",
Versions: []app.ManifestVersion{
Expand Down
4 changes: 4 additions & 0 deletions benchmark/benchmark_mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,7 @@ func (m *mockClientGeneratorWithK8sClient) ClientFor(kind resource.Kind) (resour
k8s.DefaultClientConfig(),
)
}

func (m *mockClientGeneratorWithK8sClient) GetCustomRouteClient(gv schema.GroupVersion, defaultNamespace string) (resource.CustomRouteClient, error) {
return nil, nil
}
2 changes: 1 addition & 1 deletion codegen/cuekind/generators.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func ManifestGoGenerator(pkg string, includeSchemas bool, projectRepo, goGenPath
return apiserver.ToOpenAPIName(path)
},
},
&jennies.ResourceClientJenny{
&jennies.ClientJenny{
GroupByKind: !groupKinds,
})
return g
Expand Down
4 changes: 2 additions & 2 deletions codegen/cuekind/generators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ func TestManifestGoGenerator(t *testing.T) {
files, err := ManifestGoGenerator("manifestdata", true, "codegen-tests", "pkg/generated", "manifestdata", true).Generate(kinds...)
require.NoError(t, err)
// Check number of files generated
// 14 -> manifest file (1), then the custom route response+query+body for reconcile (3), response body and wrapper+query+body for search in v3 (4), request, response, and wrapper for /foobar in v3 (3), +1 client per version (3)
require.Len(t, files, 14, "should be 14 files generated, got %d", len(files))
// 15 -> manifest file (1), then the custom route response+query+body for reconcile (3), response body and wrapper+query+body for search in v3 (4), request, response, and wrapper for /foobar in v3 (3), the resource clients for v1-v3 (3), and the version-level client for v3 routes (1)
require.Len(t, files, 15, "should be 15 files generated, got %d", len(files))
// Check content against the golden files
for _, file := range files {
compareToGolden(t, codejen.Files{file}, "go/groupbygroup")
Expand Down
126 changes: 102 additions & 24 deletions codegen/jennies/goclients.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jennies

import (
"bytes"
"errors"
"fmt"
"go/format"
"path/filepath"
Expand All @@ -15,19 +16,37 @@ import (
"github.com/grafana/grafana-app-sdk/codegen/templates"
)

type ResourceClientJenny struct {
type ClientJenny struct {
// GroupByKind determines whether kinds are grouped by GroupVersionKind or just GroupVersion.
// If GroupByKind is true, generated paths are <kind>/<version>/<file>, instead of the default <version>/<file>.
// When GroupByKind is false, subresource types (such as spec and status) are prefixed with the kind name,
// i.e. generating FooSpec instead of Spec for kind.Name() = "Foo" and Depth=1
GroupByKind bool
}

func (*ResourceClientJenny) JennyName() string {
return "ResourceClientJenny"
func (*ClientJenny) JennyName() string {
return "ClientJenny"
}

func (r *ResourceClientJenny) Generate(appManifest codegen.AppManifest) (codejen.Files, error) {
func (r *ClientJenny) Generate(appManifest codegen.AppManifest) (codejen.Files, error) {
files := make(codejen.Files, 0)

groupVersionFiles, err := r.generateCustomRouteClients(appManifest)
if err != nil {
return nil, err
}
files = append(files, groupVersionFiles...)

resourceFiles, err := r.generateResourceClients(appManifest)
if err != nil {
return nil, err
}
files = append(files, resourceFiles...)

return files, nil
}

func (r *ClientJenny) generateResourceClients(appManifest codegen.AppManifest) (codejen.Files, error) {
files := make(codejen.Files, 0)
for version, kind := range codegen.VersionedKinds(appManifest) {
if !kind.Codegen.Go.Enabled {
Expand Down Expand Up @@ -61,23 +80,13 @@ func (r *ResourceClientJenny) Generate(appManifest codegen.AppManifest) (codejen
KindName: exportField(kind.Kind),
KindPrefix: prefix,
Subresources: subresources,
CustomRoutes: make([]templates.GoResourceClientCustomRoute, 0),
}
for cpath, methods := range kind.Routes {
for method, route := range methods {
if route.Name == "" {
route.Name = defaultRouteName(method, cpath)
}
crmd, err := r.getCustomRouteInfo(route)
if err != nil {
return nil, err
}
crmd.Path = cpath
crmd.Method = method
md.CustomRoutes = append(md.CustomRoutes, crmd)
}
}
slices.SortFunc(md.CustomRoutes, func(a, b templates.GoResourceClientCustomRoute) int {

md.CustomRoutes, err = getCustomRoutes(kind.Routes)
if err != nil {
return nil, fmt.Errorf("getting custom routes for kind %s: %w", kind.Kind, err)
}
slices.SortFunc(md.CustomRoutes, func(a, b templates.GoClientCustomRoute) int {
return strings.Compare(a.TypeName, b.TypeName)
})

Expand Down Expand Up @@ -105,24 +114,93 @@ func (r *ResourceClientJenny) Generate(appManifest codegen.AppManifest) (codejen
return files, nil
}

func (*ResourceClientJenny) getCustomRouteInfo(customRoute codegen.CustomRoute) (templates.GoResourceClientCustomRoute, error) {
md := templates.GoResourceClientCustomRoute{
func (r *ClientJenny) generateCustomRouteClients(appManifest codegen.AppManifest) (codejen.Files, error) {
files := make(codejen.Files, 0)
for _, version := range appManifest.Versions() {
md := templates.GoCustomRouteClientMetadata{
PackageName: ToPackageName(version.Name()),
Group: appManifest.Properties().FullGroup,
Version: version.Name(),
}

var err error
md.NamespacedRoutes, err = getCustomRoutes(version.Routes().Namespaced)
if err != nil {
return nil, err
}

md.ClusterRoutes, err = getCustomRoutes(version.Routes().Cluster)
if err != nil {
return nil, err
}

if len(md.NamespacedRoutes) == 0 && len(md.ClusterRoutes) == 0 {
continue
}

b := bytes.Buffer{}
err = templates.WriteGoCustomRouteClient(md, &b)
if err != nil {
return nil, err
}
formatted, err := format.Source(b.Bytes())
if err != nil {
return nil, err
}
formatted, err = imports.Process("", formatted, &imports.Options{
Comments: true,
})
if err != nil {
return nil, err
}
files = append(files, codejen.File{
RelativePath: filepath.Join(ToPackageName(appManifest.Properties().Group), ToPackageName(version.Name()), "client_gen.go"),
Data: formatted,
From: []codejen.NamedJenny{r},
})
}
return files, nil
}

func getCustomRouteInfo(customRoute codegen.CustomRoute) (templates.GoClientCustomRoute, error) {
md := templates.GoClientCustomRoute{
TypeName: toExportedFieldName(customRoute.Name),
HasParams: customRoute.Request.Query.Exists(),
HasBody: customRoute.Request.Body.Exists(),
}
if md.HasParams {
md.ParamValues = make([]templates.GoResourceClientParamValues, 0)
md.ParamValues = make([]templates.GoCustomRouteParamValues, 0)
it, err := customRoute.Request.Query.Fields()
if err != nil {
return md, err
}
for it.Next() {
md.ParamValues = append(md.ParamValues, templates.GoResourceClientParamValues{
md.ParamValues = append(md.ParamValues, templates.GoCustomRouteParamValues{
Key: it.Selector().String(),
FieldName: exportField(it.Selector().String()),
})
}
}
return md, nil
}

func getCustomRoutes(routeMap map[string]map[string]codegen.CustomRoute) ([]templates.GoClientCustomRoute, error) {
var errs error
routes := make([]templates.GoClientCustomRoute, 0)
for cpath, methods := range routeMap {
for method, route := range methods {
if route.Name == "" {
route.Name = defaultRouteName(method, cpath)
}
crmd, err := getCustomRouteInfo(route)
if err != nil {
errs = errors.Join(errs, err)
continue
}
crmd.Path = cpath
crmd.Method = method
routes = append(routes, crmd)
}
}
return routes, errs
}
96 changes: 96 additions & 0 deletions codegen/templates/client.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package {{.PackageName}}

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana-app-sdk/resource"
)

type CustomRouteClient struct {
resource.CustomRouteClient
}

func NewCustomRouteClient(client resource.CustomRouteClient) *CustomRouteClient {
return &CustomRouteClient{client}
}

func NewCustomRouteClientFromGenerator(generator resource.ClientGenerator, defaultNamespace string) (*CustomRouteClient, error) {
client, err := generator.GetCustomRouteClient(schema.GroupVersion{
Group: "{{.Group}}",
Version: "{{.Version}}",
}, defaultNamespace)
if err != nil {
return nil, err
}
return NewCustomRouteClient(client), nil
}
{{ range .NamespacedRoutes }}
type {{.TypeName}}Request struct { {{ if .HasParams }}
Params {{.TypeName}}RequestParams{{ end }}{{ if .HasBody }}
Body {{.TypeName}}RequestBody{{ end }}
Headers http.Header
}

func (c *CustomRouteClient) {{.TypeName}}(ctx context.Context, namespace string, request {{.TypeName}}Request) (*{{.TypeName}}Response, error) { {{ if .HasParams }}
params := url.Values{}{{ range .ParamValues }}
params.Set("{{.Key}}", fmt.Sprintf("%v", request.Params.{{.FieldName}})){{ end }}{{ end }}{{ if .HasBody }}
body, err := json.Marshal(request.Body)
if err != nil {
return nil, fmt.Errorf("unable to marshal body to JSON: %w", err)
}{{ end }}
resp, err := c.NamespacedRequest(ctx, namespace, resource.CustomRouteRequestOptions{
Path: "{{.Path}}",
Verb: "{{.Method}}",{{ if .HasParams }}
Query: params,{{ end }}{{ if .HasBody }}
Body: io.NopCloser(bytes.NewReader(body)),{{ end }}
Headers: request.Headers,
})
if err != nil {
return nil, err
}
cast := {{.TypeName}}Response{}
err = json.Unmarshal(resp, &cast)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal response bytes into {{.TypeName}}Response: %w", err)
}
return &cast, nil
}
{{ end }}{{ range .ClusterRoutes }}
type {{.TypeName}}Request struct { {{ if .HasParams }}
Params {{.TypeName}}RequestParams{{ end }}{{ if .HasBody }}
Body {{.TypeName}}RequestBody{{ end }}
Headers http.Header
}

func (c *CustomRouteClient) {{.TypeName}}(ctx context.Context, request {{.TypeName}}Request) (*{{.TypeName}}Response, error) { {{ if .HasParams }}
params := url.Values{}{{ range .ParamValues }}
params.Set("{{.Key}}", fmt.Sprintf("%v", request.Params.{{.FieldName}})){{ end }}{{ end }}{{ if .HasBody }}
body, err := json.Marshal(request.Body)
if err != nil {
return nil, fmt.Errorf("unable to marshal body to JSON: %w", err)
}{{ end }}
resp, err := c.ClusteredRequest(ctx, resource.CustomRouteRequestOptions{
Path: "{{.Path}}",
Verb: "{{.Method}}",{{ if .HasParams }}
Query: params,{{ end }}{{ if .HasBody }}
Body: io.NopCloser(bytes.NewReader(body)),{{ end }}
Headers: request.Headers,
})
if err != nil {
return nil, err
}
cast := {{.TypeName}}Response{}
err = json.Unmarshal(resp, &cast)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal response bytes into {{.TypeName}}Response: %w", err)
}
return &cast, nil
}
{{ end }}
Loading
Loading