K8s: APIGroupBuilder runner admission support (#95705)

This commit is contained in:
Todd Treece
2024-11-05 16:57:20 -05:00
committed by GitHub
parent 6d8ee5fde0
commit cd9fcd08aa
15 changed files with 838 additions and 24 deletions

View File

@ -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)
}

View 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
}

View 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)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
.PHONY: generate
generate:
@grafana-app-sdk generate -g ./pkg/apis --kindgrouping=group --postprocess --crdencoding none

View File

@ -0,0 +1 @@
module: "github.com/grafana/grafana/pkg/services/apiserver/builder/runner/testdata/app/kinds"

View 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
}
}
}
}
}

View 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{}

View 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"`
}

View 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{}

View 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

View 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"`
}

View 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

View 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")
}