mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 04:31:36 +08:00
K8s: APIGroupBuilder runner admission support (#95705)
This commit is contained in:
@ -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)
|
||||
}
|
||||
|
108
pkg/services/apiserver/builder/runner/admission.go
Normal file
108
pkg/services/apiserver/builder/runner/admission.go
Normal file
@ -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
|
||||
}
|
175
pkg/services/apiserver/builder/runner/admission_test.go
Normal file
175
pkg/services/apiserver/builder/runner/admission_test.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
3
pkg/services/apiserver/builder/runner/testdata/app/Makefile
vendored
Normal file
3
pkg/services/apiserver/builder/runner/testdata/app/Makefile
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.PHONY: generate
|
||||
generate:
|
||||
@grafana-app-sdk generate -g ./pkg/apis --kindgrouping=group --postprocess --crdencoding none
|
1
pkg/services/apiserver/builder/runner/testdata/app/kinds/cue.mod/module.cue
vendored
Normal file
1
pkg/services/apiserver/builder/runner/testdata/app/kinds/cue.mod/module.cue
vendored
Normal file
@ -0,0 +1 @@
|
||||
module: "github.com/grafana/grafana/pkg/services/apiserver/builder/runner/testdata/app/kinds"
|
25
pkg/services/apiserver/builder/runner/testdata/app/kinds/example.cue
vendored
Normal file
25
pkg/services/apiserver/builder/runner/testdata/app/kinds/example.cue
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_codec_gen.go
vendored
Normal file
28
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_codec_gen.go
vendored
Normal file
@ -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{}
|
32
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_metadata_gen.go
vendored
Normal file
32
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_metadata_gen.go
vendored
Normal file
@ -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"`
|
||||
}
|
266
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_object_gen.go
vendored
Normal file
266
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_object_gen.go
vendored
Normal file
@ -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{}
|
34
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_schema_gen.go
vendored
Normal file
34
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_schema_gen.go
vendored
Normal file
@ -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
|
8
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_spec_gen.go
vendored
Normal file
8
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_spec_gen.go
vendored
Normal file
@ -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"`
|
||||
}
|
70
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_status_gen.go
vendored
Normal file
70
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/example/v1/example_status_gen.go
vendored
Normal file
@ -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
|
50
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/manifest.go
vendored
Normal file
50
pkg/services/apiserver/builder/runner/testdata/app/pkg/apis/manifest.go
vendored
Normal file
@ -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")
|
||||
}
|
Reference in New Issue
Block a user