diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index b6ba87a902b..b0e1626a678 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -92,6 +92,10 @@ type GrafanaMetaAccessor interface { GetOriginTimestamp() (*time.Time, error) GetSpec() (any, error) + SetSpec(any) error + + GetStatus() (any, error) + SetStatus(any) error // Find a title in the object // This will reflect the object and try to get: @@ -504,6 +508,36 @@ func (m *grafanaMetaAccessor) GetSpec() (spec any, err error) { return } +func (m *grafanaMetaAccessor) SetSpec(s any) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error setting spec") + } + }() + m.r.FieldByName("Spec").Set(reflect.ValueOf(s)) + return +} + +func (m *grafanaMetaAccessor) GetStatus() (status any, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error reading status") + } + }() + status = m.r.FieldByName("Status").Interface() + return +} + +func (m *grafanaMetaAccessor) SetStatus(s any) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error setting status") + } + }() + m.r.FieldByName("Status").Set(reflect.ValueOf(s)) + return +} + func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string { // look for Spec.Title or Spec.Name spec := m.r.FieldByName("Spec") diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index cd4c090a84b..16b21f60555 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -14,18 +14,22 @@ require ( k8s.io/component-base v0.31.0 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -37,6 +41,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -56,8 +61,14 @@ require ( go.etcd.io/etcd/api/v3 v3.5.14 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.28.0 // indirect @@ -78,7 +89,7 @@ require ( k8s.io/api v0.31.0 // indirect k8s.io/client-go v0.31.0 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index ffa91e73824..2df0f83d0b3 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -35,6 +35,7 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= diff --git a/pkg/apiserver/registry/generic/store.go b/pkg/apiserver/registry/generic/store.go new file mode 100644 index 00000000000..3cc50bd5445 --- /dev/null +++ b/pkg/apiserver/registry/generic/store.go @@ -0,0 +1,57 @@ +package generic + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" +) + +// NewStatusREST makes a RESTStorage for status that has more limited options. +// It is based on the original REST so that we can share the same underlying store +func NewStatusREST(store *genericregistry.Store, strategy rest.UpdateResetFieldsStrategy) *StatusREST { + statusStore := *store + statusStore.CreateStrategy = nil + statusStore.DeleteStrategy = nil + statusStore.UpdateStrategy = strategy + statusStore.ResetFieldsStrategy = strategy + return &StatusREST{store: &statusStore} +} + +// StatusREST implements the REST endpoint for changing the status of an DataPlaneService. +type StatusREST struct { + store *genericregistry.Store +} + +var _ = rest.Patcher(&StatusREST{}) + +// New creates a new DataPlaneService object. +func (r *StatusREST) New() runtime.Object { + return r.store.NewFunc() +} + +// Destroy cleans up resources on shutdown. +func (r *StatusREST) Destroy() { + // Given that underlying store is shared with REST, + // we don't destroy it here explicitly. +} + +// Get retrieves the object from the storage. It is required to support Patch. +func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + return r.store.Get(ctx, name, options) +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + // We are explicitly setting forceAllowCreate to false in the call to the underlying storage because + // subresources should never allow create on update. + return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) +} + +// GetResetFields implements rest.ResetFieldsStrategy +func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + return r.store.GetResetFields() +} diff --git a/pkg/apiserver/registry/generic/strategy.go b/pkg/apiserver/registry/generic/strategy.go index 95bf1424e04..2628292fbee 100644 --- a/pkg/apiserver/registry/generic/strategy.go +++ b/pkg/apiserver/registry/generic/strategy.go @@ -3,57 +3,155 @@ package generic import ( "context" + "github.com/grafana/grafana/pkg/apimachinery/utils" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) type genericStrategy struct { runtime.ObjectTyper names.NameGenerator + + gv schema.GroupVersion } // NewStrategy creates and returns a genericStrategy instance. -func NewStrategy(typer runtime.ObjectTyper) genericStrategy { - return genericStrategy{typer, names.SimpleNameGenerator} +func NewStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion) *genericStrategy { + return &genericStrategy{typer, names.SimpleNameGenerator, gv} } // NamespaceScoped returns true because all Generic resources must be within a namespace. -func (genericStrategy) NamespaceScoped() bool { +func (g *genericStrategy) NamespaceScoped() bool { return true } -func (genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {} +func (g *genericStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + fields := map[fieldpath.APIVersion]*fieldpath.Set{ + fieldpath.APIVersion(g.gv.String()): fieldpath.NewSet( + fieldpath.MakePathOrDie("status"), + ), + } + return fields +} -func (genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {} +func (g *genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {} -func (genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { +func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + oldMeta, err := utils.MetaAccessor(old) + if err != nil { + return + } + + status, err := oldMeta.GetStatus() + if err != nil { + return + } + + newMeta, err := utils.MetaAccessor(obj) + if err != nil { + return + } + + _ = newMeta.SetStatus(status) +} + +func (g *genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { return field.ErrorList{} } // WarningsOnCreate returns warnings for the creation of the given object. -func (genericStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { return nil } +func (g *genericStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} -func (genericStrategy) AllowCreateOnUpdate() bool { +func (g *genericStrategy) AllowCreateOnUpdate() bool { return true } -func (genericStrategy) AllowUnconditionalUpdate() bool { +func (g *genericStrategy) AllowUnconditionalUpdate() bool { return true } -func (genericStrategy) Canonicalize(obj runtime.Object) {} +func (g *genericStrategy) Canonicalize(obj runtime.Object) {} -func (genericStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { +func (g *genericStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { return field.ErrorList{} } // WarningsOnUpdate returns warnings for the given update. -func (genericStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { +func (g *genericStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +type genericStatusStrategy struct { + runtime.ObjectTyper + names.NameGenerator + + gv schema.GroupVersion +} + +// NewStatusStrategy creates a new genericStatusStrategy. +func NewStatusStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion) *genericStatusStrategy { + return &genericStatusStrategy{typer, names.SimpleNameGenerator, gv} +} + +func (g *genericStatusStrategy) NamespaceScoped() bool { + return true +} + +func (g *genericStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { + fields := map[fieldpath.APIVersion]*fieldpath.Set{ + fieldpath.APIVersion(g.gv.String()): fieldpath.NewSet( + fieldpath.MakePathOrDie("spec"), + fieldpath.MakePathOrDie("metadata"), + ), + } + + return fields +} + +func (g *genericStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + oldMeta, err := utils.MetaAccessor(old) + if err != nil { + return + } + + newMeta, err := utils.MetaAccessor(obj) + if err != nil { + return + } + newMeta.SetAnnotations(oldMeta.GetAnnotations()) + newMeta.SetLabels(oldMeta.GetLabels()) + newMeta.SetFinalizers(oldMeta.GetFinalizers()) + newMeta.SetOwnerReferences(oldMeta.GetOwnerReferences()) +} + +func (g *genericStatusStrategy) AllowCreateOnUpdate() bool { + return false +} + +func (g *genericStatusStrategy) AllowUnconditionalUpdate() bool { + return false +} + +// Canonicalize normalizes the object after validation. +func (g *genericStatusStrategy) Canonicalize(obj runtime.Object) { +} + +// ValidateUpdate validates an update of genericStatusStrategy. +func (g *genericStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return field.ErrorList{} +} + +// WarningsOnUpdate returns warnings for the given update. +func (g *genericStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { return nil } diff --git a/pkg/registry/apis/alerting/notifications/receiver/storage.go b/pkg/registry/apis/alerting/notifications/receiver/storage.go index 640292006e9..9647bbd4e84 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/storage.go @@ -35,7 +35,7 @@ func NewStorage( tableConverter: resourceInfo.TableConverter(), } if optsGetter != nil && dualWriteBuilder != nil { - strategy := grafanaregistry.NewStrategy(scheme) + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) s := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/storage.go index 8dfd8bf1b6f..3bf20b03ddd 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/storage.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/storage.go @@ -41,7 +41,7 @@ func NewStorage( tableConverter: resourceInfo.TableConverter(), } if optsGetter != nil && dualWriteBuilder != nil { - strategy := grafanaregistry.NewStrategy(scheme) + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) s := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 52dd42be2c3..411fb53df66 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -47,7 +47,7 @@ func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter ge defaultOpts.StorageConfig.Config, ) - strategy := grafanaregistry.NewStrategy(scheme) + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/dashboard/storage.go b/pkg/registry/apis/dashboard/storage.go index bc50691d5f0..8d92a64cd77 100644 --- a/pkg/registry/apis/dashboard/storage.go +++ b/pkg/registry/apis/dashboard/storage.go @@ -16,8 +16,8 @@ type storage struct { } func newStorage(scheme *runtime.Scheme) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) resourceInfo := dashboard.DashboardResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/folders/storage.go b/pkg/registry/apis/folders/storage.go index 3d20ee20f89..21f98e93842 100644 --- a/pkg/registry/apis/folders/storage.go +++ b/pkg/registry/apis/folders/storage.go @@ -5,7 +5,6 @@ import ( "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -17,16 +16,15 @@ type storage struct { } func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, legacy *legacyStorage) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) - resource := v0alpha1.FolderResourceInfo store := &genericregistry.Store{ - NewFunc: resource.NewFunc, - NewListFunc: resource.NewListFunc, + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()), KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()), PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: resource.GroupResource(), + DefaultQualifiedResource: resourceInfo.GroupResource(), SingularQualifiedResource: resourceInfo.SingularGroupResource(), TableConvertor: legacy.tableConverter, diff --git a/pkg/registry/apis/peakq/storage.go b/pkg/registry/apis/peakq/storage.go index 0f87698399b..3953ba3e360 100644 --- a/pkg/registry/apis/peakq/storage.go +++ b/pkg/registry/apis/peakq/storage.go @@ -17,9 +17,9 @@ type storage struct { } func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) - resourceInfo := peakq.QueryTemplateResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) + store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/playlist/storage.go b/pkg/registry/apis/playlist/storage.go index ead063391b7..e97939ea75f 100644 --- a/pkg/registry/apis/playlist/storage.go +++ b/pkg/registry/apis/playlist/storage.go @@ -17,7 +17,7 @@ type storage struct { } func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, legacy *legacyStorage) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) resource := playlist.PlaylistResourceInfo store := &genericregistry.Store{ diff --git a/pkg/registry/apis/scope/storage.go b/pkg/registry/apis/scope/storage.go index 49a3c94d847..2f20a7552dd 100644 --- a/pkg/registry/apis/scope/storage.go +++ b/pkg/registry/apis/scope/storage.go @@ -22,9 +22,8 @@ type storage struct { } func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) - resourceInfo := scope.ScopeResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, @@ -46,9 +45,9 @@ func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGette } func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) - resourceInfo := scope.ScopeDashboardBindingResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) + store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, @@ -66,13 +65,14 @@ func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic. if err := store.CompleteWithOptions(options); err != nil { return nil, err } + return &storage{Store: store}, nil } func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) - resourceInfo := scope.ScopeNodeResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) + store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc, diff --git a/pkg/registry/apis/service/storage.go b/pkg/registry/apis/service/storage.go index 04497ac64aa..2ed49bb53f6 100644 --- a/pkg/registry/apis/service/storage.go +++ b/pkg/registry/apis/service/storage.go @@ -17,9 +17,9 @@ type storage struct { } func newStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) { - strategy := grafanaregistry.NewStrategy(scheme) - resourceInfo := service.ExternalNameResourceInfo + strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion()) + store := &genericregistry.Store{ NewFunc: resourceInfo.NewFunc, NewListFunc: resourceInfo.NewListFunc,