diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index fe4f12a9504..39e9d270168 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -3,6 +3,7 @@ package appregistry import ( "context" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/services/apiserver" @@ -17,6 +18,7 @@ var ( type Service struct { runner *runner.APIGroupRunner + log log.Logger } // ProvideRegistryServiceSink is an entry point for each service that will force initialization @@ -42,12 +44,14 @@ func ProvideRegistryServiceSink( if err != nil { return nil, err } - return &Service{runner: runner}, nil + return &Service{runner: runner, log: log.New("app-registry")}, nil } func (s *Service) Run(ctx context.Context) error { + s.log.Debug("initializing app registry") if err := s.runner.Init(ctx); err != nil { return err } + s.log.Info("app registry initialized") return s.runner.Run(ctx) } diff --git a/pkg/services/apiserver/builder/runner/admission.go b/pkg/services/apiserver/builder/runner/admission.go new file mode 100644 index 00000000000..91c873789b8 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/admission.go @@ -0,0 +1,108 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apiserver/pkg/admission" +) + +func (b *appBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + if b.app == nil { + return errors.New("app is nil") + } + + req, err := b.translateAdmissionAttributes(a) + if err != nil { + return err + } + + resp, err := b.app.Mutate(ctx, req) + if err != nil { + if errors.Is(err, app.ErrNotImplemented) { + return nil + } + return err + } + + obj := a.GetObject() + if obj == nil { + return errors.New("object is nil") + } + if resp.UpdatedObject == nil { + return errors.New("updated object is nil") + } + reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(resp.UpdatedObject).Elem()) + + return nil +} + +func (b *appBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + req, err := b.translateAdmissionAttributes(a) + if err != nil { + return err + } + err = b.app.Validate(ctx, req) + if err != nil { + if errors.Is(err, app.ErrNotImplemented) { + return nil + } + return err + } + return nil +} + +func (b *appBuilder) translateAdmissionAttributes(a admission.Attributes) (*app.AdmissionRequest, error) { + extra := make(map[string]any) + for k, v := range a.GetUserInfo().GetExtra() { + extra[k] = any(v) + } + + var action resource.AdmissionAction + switch a.GetOperation() { + case admission.Create: + action = resource.AdmissionActionCreate + case admission.Update: + action = resource.AdmissionActionUpdate + case admission.Delete: + action = resource.AdmissionActionDelete + case admission.Connect: + action = resource.AdmissionActionConnect + default: + return nil, fmt.Errorf("unknown admission operation: %v", a.GetOperation()) + } + + var ( + obj resource.Object + oldObj resource.Object + ) + + if a.GetObject() != nil { + obj = a.GetObject().(resource.Object) + } + + if a.GetOldObject() != nil { + oldObj = a.GetOldObject().(resource.Object) + } + + req := app.AdmissionRequest{ + Action: action, + Kind: a.GetKind().Kind, + Group: a.GetKind().Group, + Version: a.GetKind().Version, + UserInfo: resource.AdmissionUserInfo{ + UID: a.GetUserInfo().GetUID(), + Username: a.GetUserInfo().GetName(), + Groups: a.GetUserInfo().GetGroups(), + Extra: extra, + }, + Object: obj, + OldObject: oldObj, + } + + return &req, nil +} diff --git a/pkg/services/apiserver/builder/runner/admission_test.go b/pkg/services/apiserver/builder/runner/admission_test.go new file mode 100644 index 00000000000..f9b6fec3244 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/admission_test.go @@ -0,0 +1,175 @@ +package runner + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + + examplev1 "github.com/grafana/grafana/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1" +) + +func TestBuilderAdmission_Validate(t *testing.T) { + exampleObj := &examplev1.Example{ + Spec: examplev1.ExampleSpec{ + A: "test", + }, + } + gvk := schema.GroupVersionKind{ + Group: examplev1.ExampleKind().Group(), + Version: examplev1.ExampleKind().Version(), + Kind: examplev1.ExampleKind().Kind(), + } + gvr := gvk.GroupVersion().WithResource(examplev1.ExampleKind().Plural()) + defaultAttributes := admission.NewAttributesRecord(exampleObj, nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, &user.DefaultInfo{}) + + tests := []struct { + name string + app app.App + req *app.AdmissionRequest + wantErr bool + }{ + { + name: "validator exists - success", + app: &mockApp{ + validateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + return nil + }, + }, + wantErr: false, + }, + { + name: "validator exists - error", + app: &mockApp{ + validateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + return errors.New("error") + }, + }, + wantErr: true, + }, + { + name: "validator not set - success", + app: &mockApp{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := appBuilder{app: tt.app} + err := b.Validate(context.Background(), defaultAttributes, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestBuilderAdmission_Mutate(t *testing.T) { + gvk := schema.GroupVersionKind{ + Group: examplev1.ExampleKind().Group(), + Version: examplev1.ExampleKind().Version(), + Kind: examplev1.ExampleKind().Kind(), + } + gvr := gvk.GroupVersion().WithResource(examplev1.ExampleKind().Plural()) + getAttributes := func() admission.Attributes { + exampleObj := &examplev1.Example{ + Spec: examplev1.ExampleSpec{ + A: "test", + }, + } + return admission.NewAttributesRecord(exampleObj, nil, gvk, "default", "foo", gvr, "", admission.Create, nil, false, &user.DefaultInfo{}) + } + tests := []struct { + name string + app app.App + attributes admission.Attributes + expected examplev1.Example + wantErr bool + }{ + { + name: "mutator exists - success", + app: &mockApp{ + mutateFunc: func(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + return &app.MutatingResponse{ + UpdatedObject: &examplev1.Example{ + Spec: examplev1.ExampleSpec{ + A: "test", + B: "mutated", + }, + }, + }, nil + }, + }, + attributes: getAttributes(), + expected: examplev1.Example{ + Spec: examplev1.ExampleSpec{ + A: "test", + B: "mutated", + }, + }, + wantErr: false, + }, + { + name: "mutator exists - error", + app: &mockApp{ + mutateFunc: func(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + return nil, errors.New("error") + }, + }, + attributes: getAttributes(), + wantErr: true, + }, + { + name: "mutator not set - no modification", + app: &mockApp{}, + attributes: getAttributes(), + expected: examplev1.Example{ + Spec: examplev1.ExampleSpec{ + A: "test", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := appBuilder{app: tt.app} + err := b.Mutate(context.Background(), tt.attributes, nil) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, &tt.expected, tt.attributes.GetObject()) + } + }) + } +} + +type mockApp struct { + app.App + mutateFunc func(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) + validateFunc func(ctx context.Context, req *app.AdmissionRequest) error +} + +func (m *mockApp) Mutate(ctx context.Context, req *app.AdmissionRequest) (*app.MutatingResponse, error) { + if m.mutateFunc == nil { + return nil, app.ErrNotImplemented + } + return m.mutateFunc(ctx, req) +} + +func (m *mockApp) Validate(ctx context.Context, req *app.AdmissionRequest) error { + if m.validateFunc == nil { + return app.ErrNotImplemented + } + return m.validateFunc(ctx, req) +} diff --git a/pkg/services/apiserver/builder/runner/builder.go b/pkg/services/apiserver/builder/runner/builder.go index 94cdde20709..2857eae8666 100644 --- a/pkg/services/apiserver/builder/runner/builder.go +++ b/pkg/services/apiserver/builder/runner/builder.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/builder" ) -var _ builder.APIGroupBuilder = (*AppBuilder)(nil) +var _ AppBuilder = (*appBuilder)(nil) type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.LegacyStorage @@ -30,28 +30,35 @@ type AppBuilderConfig struct { groupVersion schema.GroupVersion } -type AppBuilder struct { +type AppBuilder interface { + builder.APIGroupBuilder + builder.APIGroupMutation + builder.APIGroupValidation + SetApp(app app.App) +} + +type appBuilder struct { app app.App config AppBuilderConfig } -func NewAppBuilder(appBuilderConfig AppBuilderConfig) (AppBuilder, error) { - return AppBuilder{ +func NewAppBuilder(appBuilderConfig AppBuilderConfig) (*appBuilder, error) { + return &appBuilder{ config: appBuilderConfig, }, nil } -func (b *AppBuilder) setApp(app app.App) { +func (b *appBuilder) SetApp(app app.App) { b.app = app } // GetGroupVersion implements APIGroupBuilder.GetGroupVersion -func (b *AppBuilder) GetGroupVersion() schema.GroupVersion { +func (b *appBuilder) GetGroupVersion() schema.GroupVersion { return b.config.groupVersion } // InstallSchema implements APIGroupBuilder.InstallSchema -func (b *AppBuilder) InstallSchema(scheme *runtime.Scheme) error { +func (b *appBuilder) InstallSchema(scheme *runtime.Scheme) error { gv := b.GetGroupVersion() for _, kind := range b.config.ManagedKinds { scheme.AddKnownTypeWithName(gv.WithKind(kind.Kind()), kind.ZeroValue()) @@ -61,7 +68,7 @@ func (b *AppBuilder) InstallSchema(scheme *runtime.Scheme) error { } // UpdateAPIGroupInfo implements APIGroupBuilder.UpdateAPIGroupInfo -func (b *AppBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { +func (b *appBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { for _, kind := range b.config.ManagedKinds { version := kind.GroupVersionKind().Version if _, ok := apiGroupInfo.VersionedResourcesStorageMap[version]; !ok { @@ -77,7 +84,7 @@ func (b *AppBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI return nil } -func (b *AppBuilder) getStorage(resourceInfo utils.ResourceInfo, opts builder.APIGroupOptions) (grafanarest.Storage, error) { +func (b *appBuilder) getStorage(resourceInfo utils.ResourceInfo, opts builder.APIGroupOptions) (grafanarest.Storage, error) { store, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter) if err != nil { return nil, err @@ -91,17 +98,17 @@ func (b *AppBuilder) getStorage(resourceInfo utils.ResourceInfo, opts builder.AP } // GetOpenAPIDefinitions implements APIGroupBuilder.GetOpenAPIDefinitions -func (b *AppBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { +func (b *appBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { return b.config.OpenAPIDefGetter } // GetAPIRoutes implements APIGroupBuilder.GetAPIRoutes -func (b *AppBuilder) GetAPIRoutes() *builder.APIRoutes { +func (b *appBuilder) GetAPIRoutes() *builder.APIRoutes { // TODO: The API routes are not yet exposed by the app.App interface. return nil } // GetAuthorizer implements APIGroupBuilder.GetAuthorizer -func (b AppBuilder) GetAuthorizer() authorizer.Authorizer { +func (b *appBuilder) GetAuthorizer() authorizer.Authorizer { return b.config.Authorizer } diff --git a/pkg/services/apiserver/builder/runner/runner.go b/pkg/services/apiserver/builder/runner/runner.go index 5648f027721..453d28aba96 100644 --- a/pkg/services/apiserver/builder/runner/runner.go +++ b/pkg/services/apiserver/builder/runner/runner.go @@ -53,14 +53,13 @@ func (r *APIGroupRunner) Init(ctx context.Context) error { if restConfig == nil { return fmt.Errorf("rest config is nil") } - for i, g := range r.groups { - customCfg := g.builders[0].config.CustomConfig + for i := range r.groups { appConfig := app.Config{ KubeConfig: *restConfig, - ManifestData: *g.provider.Manifest().ManifestData, - SpecificConfig: customCfg, + ManifestData: *r.groups[i].provider.Manifest().ManifestData, + SpecificConfig: r.groups[i].customConfig, } - app, err := g.provider.NewApp(appConfig) + app, err := r.groups[i].provider.NewApp(appConfig) if err != nil { return err } @@ -78,9 +77,10 @@ func (r *APIGroupRunner) GetBuilders() []AppBuilder { } type appBuilderGroup struct { - builders []AppBuilder - provider app.Provider - app app.App + builders []AppBuilder + provider app.Provider + app app.App + customConfig any } func newAppBuilderGroup(cfg RunnerConfig, provider app.Provider) (appBuilderGroup, error) { @@ -105,19 +105,22 @@ func newAppBuilderGroup(cfg RunnerConfig, provider app.Provider) (appBuilderGrou gv: kinds, } confCopy.groupVersion = gv + if confCopy.CustomConfig == nil { + group.customConfig = confCopy.CustomConfig + } b, err := NewAppBuilder(confCopy) if err != nil { return group, err } group.builders = append(group.builders, b) - cfg.APIRegistrar.RegisterAPI(&b) + cfg.APIRegistrar.RegisterAPI(b) } return group, nil } func (g *appBuilderGroup) setApp(app app.App) { g.app = app - for i := range g.builders { - g.builders[i].setApp(app) + for _, b := range g.builders { + b.SetApp(app) } } diff --git a/pkg/services/apiserver/builder/runner/testdata/app/Makefile b/pkg/services/apiserver/builder/runner/testdata/app/Makefile new file mode 100644 index 00000000000..f704166e2ef --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/Makefile @@ -0,0 +1,3 @@ +.PHONY: generate +generate: + @grafana-app-sdk generate -g ./pkg/apis --kindgrouping=group --postprocess --crdencoding none \ No newline at end of file diff --git a/pkg/services/apiserver/builder/runner/testdata/app/kinds/cue.mod/module.cue b/pkg/services/apiserver/builder/runner/testdata/app/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..b4423cc7bdd --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/kinds/cue.mod/module.cue @@ -0,0 +1 @@ +module: "github.com/grafana/grafana/pkg/services/apiserver/builder/runner/testdata/app/kinds" diff --git a/pkg/services/apiserver/builder/runner/testdata/app/kinds/example.cue b/pkg/services/apiserver/builder/runner/testdata/app/kinds/example.cue new file mode 100644 index 00000000000..20801fbe9e4 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/kinds/example.cue @@ -0,0 +1,25 @@ +package core + +externalName: { + kind: "Example" + group: "example" + apiResource: { + groupOverride: "example.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "Examples" + current: "v1" + versions: { + "v1": { + schema: { + spec: { + a: string + b: string + } + } + } + } +} diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_codec_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_codec_gen.go new file mode 100644 index 00000000000..4c76ee17516 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// ExampleJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type ExampleJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*ExampleJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*ExampleJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &ExampleJSONCodec{} diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_metadata_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_metadata_gen.go new file mode 100644 index 00000000000..56f9f1435ca --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_metadata_gen.go @@ -0,0 +1,32 @@ +package v1 + +import ( + "time" +) + +// ExampleMetadata defines model for ExampleMetadata. +type ExampleMetadata struct { + CreatedBy string `json:"createdBy"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + Generation int64 `json:"generation"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` + UpdateTimestamp time.Time `json:"updateTimestamp"` + UpdatedBy string `json:"updatedBy"` +} + +// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field. +// It is not exhaustive and only includes fields which may be relevant to a kind's implementation, +// As it is also intended to be generic enough to function with any API Server. +type ExampleKubeObjectMetadata struct { + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + Generation int64 `json:"generation"` + Labels map[string]string `json:"labels"` + ResourceVersion string `json:"resourceVersion"` + Uid string `json:"uid"` +} diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_object_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_object_gen.go new file mode 100644 index 00000000000..3038a4bffac --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_object_gen.go @@ -0,0 +1,266 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Example struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec ExampleSpec `json:"spec"` + ExampleStatus ExampleStatus `json:"status"` +} + +func (o *Example) GetSpec() any { + return o.Spec +} + +func (o *Example) SetSpec(spec any) error { + cast, ok := spec.(ExampleSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Example) GetSubresources() map[string]any { + return map[string]any{ + "status": o.ExampleStatus, + } +} + +func (o *Example) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.ExampleStatus, true + default: + return nil, false + } +} + +func (o *Example) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(ExampleStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type ExampleStatus", value) + } + o.ExampleStatus = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Example) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Example) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Example) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Example) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Example) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Example) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Example) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Example) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Example) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Example) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Example) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Example) DeepCopyObject() runtime.Object { + return o.Copy() +} + +// Interface compliance compile-time check +var _ resource.Object = &Example{} + +// +k8s:openapi-gen=true +type ExampleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []Example `json:"items"` +} + +func (o *ExampleList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *ExampleList) Copy() resource.ListObject { + cpy := &ExampleList{ + TypeMeta: o.TypeMeta, + Items: make([]Example, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Example); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *ExampleList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *ExampleList) SetItems(items []resource.Object) { + o.Items = make([]Example, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Example) + } +} + +// Interface compliance compile-time check +var _ resource.ListObject = &ExampleList{} diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_schema_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_schema_gen.go new file mode 100644 index 00000000000..afca406c4f6 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaExample = resource.NewSimpleSchema("example.grafana.app", "v1", &Example{}, &ExampleList{}, resource.WithKind("Example"), + resource.WithPlural("examples"), resource.WithScope(resource.NamespacedScope)) + kindExample = resource.Kind{ + Schema: schemaExample, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &ExampleJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func ExampleKind() resource.Kind { + return kindExample +} + +// Schema returns a resource.SimpleSchema representation of Example +func ExampleSchema() *resource.SimpleSchema { + return schemaExample +} + +// Interface compliance checks +var _ resource.Schema = kindExample diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_spec_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_spec_gen.go new file mode 100644 index 00000000000..f57482555b4 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_spec_gen.go @@ -0,0 +1,8 @@ +package v1 + +// ExampleSpec defines model for ExampleSpec. +// +k8s:openapi-gen=true +type ExampleSpec struct { + A string `json:"a"` + B string `json:"b"` +} diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_status_gen.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_status_gen.go new file mode 100644 index 00000000000..85bd8505a10 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_status_gen.go @@ -0,0 +1,70 @@ +package v1 + +// Defines values for ExampleOperatorStateState. +const ( + ExampleOperatorStateStateFailed ExampleOperatorStateState = "failed" + ExampleOperatorStateStateInProgress ExampleOperatorStateState = "in_progress" + ExampleOperatorStateStateSuccess ExampleOperatorStateState = "success" +) + +// Defines values for ExamplestatusOperatorStateState. +const ( + ExamplestatusOperatorStateStateFailed ExamplestatusOperatorStateState = "failed" + ExamplestatusOperatorStateStateInProgress ExamplestatusOperatorStateState = "in_progress" + ExamplestatusOperatorStateStateSuccess ExamplestatusOperatorStateState = "success" +) + +// ExampleOperatorState defines model for ExampleOperatorState. +// +k8s:openapi-gen=true +type ExampleOperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ExampleOperatorStateState `json:"state"` +} + +// ExampleOperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +// +k8s:openapi-gen=true +type ExampleOperatorStateState string + +// ExampleStatus defines model for ExampleStatus. +// +k8s:openapi-gen=true +type ExampleStatus struct { + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` + + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]ExamplestatusOperatorState `json:"operatorStates,omitempty"` +} + +// ExamplestatusOperatorState defines model for Examplestatus.#OperatorState. +// +k8s:openapi-gen=true +type ExamplestatusOperatorState struct { + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` + + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State ExamplestatusOperatorStateState `json:"state"` +} + +// ExamplestatusOperatorStateState state describes the state of the lastEvaluation. +// It is limited to three possible states for machine evaluation. +// +k8s:openapi-gen=true +type ExamplestatusOperatorStateState string diff --git a/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/manifest.go b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/manifest.go new file mode 100644 index 00000000000..9f9c754cc03 --- /dev/null +++ b/pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/manifest.go @@ -0,0 +1,50 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + + "github.com/grafana/grafana-app-sdk/app" +) + +var ( + rawSchemaExamplev1 = []byte(`{"spec":{"properties":{"a":{"type":"string"},"b":{"type":"string"}},"required":["a","b"],"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","x-kubernetes-preserve-unknown-fields":true}}`) + versionSchemaExamplev1 app.VersionSchema + _ = json.Unmarshal(rawSchemaExamplev1, &versionSchemaExamplev1) +) + +var appManifestData = app.ManifestData{ + AppName: "example", + Group: "example.grafana.app", + Kinds: []app.ManifestKind{ + { + Kind: "Example", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v1", + Schema: &versionSchemaExamplev1, + }, + }, + }, + }, +} + +func jsonToMap(j string) map[string]any { + m := make(map[string]any) + json.Unmarshal([]byte(j), &j) + return m +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("example") +}