Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: Add new AdmissionControl service (experimental for now) #983

Merged
merged 39 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
366c6cd
add admission service
ryantxu May 10, 2024
5b1a3ad
tabs
ryantxu May 10, 2024
479ee40
use k8s names
ryantxu May 10, 2024
0a0e017
more adapters
ryantxu May 13, 2024
8debcd4
more adapters
ryantxu May 13, 2024
4f7ef3c
more adapters
ryantxu May 13, 2024
53b94e1
better tests
ryantxu May 13, 2024
20b1338
add plugin context
ryantxu May 13, 2024
80f737d
Merge remote-tracking branch 'origin/main' into validate-settings
ryantxu May 17, 2024
e42def7
add instance settings callback
ryantxu May 17, 2024
bb04333
add instance settings callback
ryantxu May 17, 2024
c0a3f78
add function for fake client
ryantxu May 17, 2024
f850b5f
dooh
ryantxu May 17, 2024
35a409b
Merge remote-tracking branch 'origin/main' into validate-settings
ryantxu May 20, 2024
fc6110a
create and update functions
ryantxu May 20, 2024
823af31
now with storage interface
ryantxu May 20, 2024
43b23be
now with storage interface
ryantxu May 20, 2024
1e20bd7
fix lint
ryantxu May 20, 2024
d793710
now as admission
ryantxu May 21, 2024
186c771
fix lint
ryantxu May 21, 2024
88f4b29
update command
ryantxu May 21, 2024
9aee73f
add converters
ryantxu May 21, 2024
61197ea
add converters
ryantxu May 21, 2024
66df1d5
fix lint
ryantxu May 21, 2024
9686768
storage >> admission
ryantxu May 21, 2024
0a5fbac
Use GVK not GVKR
ryantxu May 21, 2024
218525d
proto helpers
ryantxu May 21, 2024
68e4b33
cleanup
ryantxu May 21, 2024
416574f
Merge remote-tracking branch 'origin/main' into validate-settings
ryantxu May 22, 2024
a437721
lint
ryantxu May 22, 2024
be57ff5
Update backend/admission.go
ryantxu May 23, 2024
fcce0f6
use different response structs
ryantxu May 23, 2024
3662d46
more cleanup
ryantxu May 23, 2024
9f22d16
added better conversion tests
ryantxu May 23, 2024
a5f8757
test lint
ryantxu May 23, 2024
62dae98
Merge remote-tracking branch 'origin/main' into validate-settings
ryantxu May 23, 2024
fd2f15b
better comments
ryantxu May 23, 2024
9b8dfd5
add response status to validate method
ryantxu May 24, 2024
48f12a0
Update backend/admission.go
ryantxu May 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions backend/admission.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a comment here but I don't know if I ended up not sending it: Should this be defined in the experimental package? Since it's experimental and it can/will include breaking changes.

Copy link
Member Author

@ryantxu ryantxu May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there is any way to do that -- since it all has to hook into the grpc bits.

For the "stream" handler -- we had a similar process. It evolved a lot for ~6months before it becoming stable (and finally removing the warning in this PR -- doooh)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this particular file could be 🤔 but that is not too helpful given that the grpc ones can not

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marefr suggested it could be implemented like the other funky grpc plugins, but I fear that would make working with datasources annoyingly difficult. The approach proposed here lets us iterate on a good solution using our existing infrastructure, and only affects people who implement the new AdmissionControl service

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I am not talking about the grpc stuff but only these files / structs. From the plugin / grafana perspective, people would need to import this as experimental.AdmissionHandler which reflects better its status vs backend.AdmissionHandler

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hymmm -- tried it, but it gets pretty silly with circular references -- the adapters are private and need to be in the backend package; everything needs access to PluginContext. There are fixes, but all are ugly. Would require something like moving (or duplicating?) PluginContext out of backend and making the adapters/serve.go public 🤷🏻

What about naming it ExperimentalAdmissionHandler? If that lets us move forward then seems ok to me.

I'll add more comments screaming it is experimental but open to suggestions

Copy link
Contributor

@andresmgot andresmgot May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, then it's fine. Thanks for checking.

Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package backend

import (
"context"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

// AdmissionHandler is an EXPERIMENTAL service that allows checking objects before they are saved
// This is modeled after the kubernetes model for admission controllers
// Since grafana 11.1, this feature is under active development and will continue to evolve in 2024
// This may also be replaced with a more native kubernetes solution that does not work with existing tooling
type AdmissionHandler interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. One thing that would be nice/useful is if a plugin doesn't have to implement all of these and return not implemented/custom errors - better if SDK can provide good defaults if not implemented. For example always return allowed = 1 for validation if not implemented. Then there's no risk of calling validate when creating/updating a datasource and not having to check for unimplemented errors.

type ValidateAdmissionHandler interface {
	// ValidateAdmission is a simple yes|no check if an object can be saved
	ValidateAdmission(context.Context, *AdmissionRequest) (*ValidationResponse, error)
}

// ValidateAdmissionFunc implements ValidateAdmissionHandler.
type ValidateAdmissionFunc func(context.Context, *AdmissionRequest) (*ValidationResponse, error)

func (f ValidateAdmissionFunc) ValidateAdmission(ctx context.Context, req *AdmissionRequest) (*ValidationResponse, error) {
	return f(ctx, req)
}

type MutateAdmissionHandler interface {
	// MutateAdmission converts the input into an object that can be saved, or rejects the request
	MutateAdmission(context.Context, *AdmissionRequest) (*MutationResponse, error)
}

// MutateAdmissionFunc implements MutateAdmissionHandler.
type MutateAdmissionFunc func(context.Context, *AdmissionRequest) (*MutationResponse, error)

func (f MutateAdmissionFunc) MutateAdmission(ctx context.Context, req *AdmissionRequest) (*MutationResponse, error) {
	return f(ctx, req)
}

type ConvertObjectHandler interface {
	// ConvertObject is called to covert objects between different versions
	ConvertObject(context.Context, *ConversionRequest) (*ConversionResponse, error)
}

// ConvertObjectFunc implements ConvertObjectHandler.
type ConvertObjectFunc func(context.Context, *ConversionRequest) (*ConversionResponse, error)

func (f ConvertObjectFunc) ConvertObject(ctx context.Context, req *ConversionRequest) (*ConversionResponse, error) {
	return f(ctx, req)
}

type AdmissionHandler interface {
	ValidateAdmissionHandler
	MutateAdmissionHandler
	ConvertObjectHandler
}

// to be used in ManageOpts
type AdmissionHandlers struct {
  ValidateAdmission ValidateAdmissionHandler
  MutateAdmission MutateAdmissionHandler
  ConvertObject ConvertObjectHandler
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting -- lets find an approach in a follow up PR. Right now the clients will implement everything so you do not know if an implementation exists or not. The Conversion service will only be needed if multiple versions exist -- so that is a good canidate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In k8s land you subscribe to webhooks which clearly indicates which one of validate/mutate your interest in so something similar from plugin dev perspective was what I had in mind

// ValidateAdmission is a simple yes|no check if an object can be saved
ValidateAdmission(context.Context, *AdmissionRequest) (*ValidationResponse, error)
// MutateAdmission converts the input into an object that can be saved, or rejects the request
MutateAdmission(context.Context, *AdmissionRequest) (*MutationResponse, error)
// ConvertObject is called to covert objects between different versions
ConvertObject(context.Context, *ConversionRequest) (*ConversionResponse, error)
}

type ValidateAdmissionFunc func(context.Context, *AdmissionRequest) (*ValidationResponse, error)
type MutateAdmissionFunc func(context.Context, *AdmissionRequest) (*MutationResponse, error)
type ConvertObjectFunc func(context.Context, *ConversionRequest) (*ConversionResponse, error)

// Operation is the type of resource operation being checked for admission control
// https://github.com/kubernetes/kubernetes/blob/v1.30.0/pkg/apis/admission/types.go#L158
type AdmissionRequestOperation int32

const (
AdmissionRequestCreate AdmissionRequestOperation = 0
AdmissionRequestUpdate AdmissionRequestOperation = 1
AdmissionRequestDelete AdmissionRequestOperation = 2
)

// String textual representation of the operation.
func (o AdmissionRequestOperation) String() string {
return pluginv2.AdmissionRequest_Operation(o).String()
}

// Identify the Object properties
type GroupVersionKind struct {
Group string `json:"group,omitempty"`
Version string `json:"version,omitempty"`
Kind string `json:"kind,omitempty"`
}

type AdmissionRequest struct {
// NOTE: this may not include populated instance settings depending on the request
PluginContext PluginContext `json:"pluginContext,omitempty"`
// The requested operation
Operation AdmissionRequestOperation `json:"operation,omitempty"`
// The object kind
Kind GroupVersionKind `json:"kind,omitempty"`
// Object is the object in the request. This includes the full metadata envelope.
ObjectBytes []byte `json:"object_bytes,omitempty"`
// OldObject is the object as it currently exists in storage. This includes the full metadata envelope.
OldObjectBytes []byte `json:"old_object_bytes,omitempty"`
}

// ConversionRequest supports converting an object from on version to another
type ConversionRequest struct {
// NOTE: this may not include app or datasource instance settings depending on the request
PluginContext PluginContext `json:"pluginContext,omitempty"`
// The object kind
Kind GroupVersionKind `json:"kind,omitempty"`
// Object is the object in the request. This includes the full metadata envelope.
ObjectBytes []byte `json:"object_bytes,omitempty"`
// Target converted version
TargetVersion string `json:"target_version,omitempty"`
}

// Basic request to say if the validation was successful or not
type ValidationResponse struct {
// Allowed indicates whether or not the admission request was permitted.
Allowed bool `json:"allowed,omitempty"`
// Result contains extra details into why an admission request was denied.
// This field IS NOT consulted in any way if "Allowed" is "true".
// +optional
Result *StatusResult `json:"result,omitempty"`
// warnings is a list of warning messages to return to the requesting API client.
// Warning messages describe a problem the client making the API request should correct or be aware of.
// Limit warnings to 120 characters if possible.
// Warnings over 256 characters and large numbers of warnings may be truncated.
// +optional
Warnings []string `json:"warnings,omitempty"`
}

type MutationResponse struct {
// Allowed indicates whether or not the admission request was permitted.
Allowed bool `json:"allowed,omitempty"`
// Result contains extra details into why an admission request was denied.
// This field IS NOT consulted in any way if "Allowed" is "true".
// +optional
Result *StatusResult `json:"result,omitempty"`
// warnings is a list of warning messages to return to the requesting API client.
// Warning messages describe a problem the client making the API request should correct or be aware of.
// Limit warnings to 120 characters if possible.
// Warnings over 256 characters and large numbers of warnings may be truncated.
// +optional
Warnings []string `json:"warnings,omitempty"`
// Mutated object bytes (when requested)
// +optional
ObjectBytes []byte `json:"object_bytes,omitempty"`
}

type ConversionResponse struct {
// Allowed indicates whether or not the admission request was permitted.
Allowed bool `json:"allowed,omitempty"`
// Result contains extra details into why an admission request was denied.
// This field IS NOT consulted in any way if "Allowed" is "true".
// +optional
Result *StatusResult `json:"result,omitempty"`
// Converted object bytes
ObjectBytes []byte `json:"object_bytes,omitempty"`
}

type StatusResult struct {
// Status of the operation.
// One of: "Success" or "Failure".
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +optional
Status string `json:"status,omitempty"`
// A human-readable description of the status of this operation.
// +optional
Message string `json:"message,omitempty"`
// A machine-readable description of why this operation is in the
// "Failure" status. If this value is empty there
// is no information available. A Reason clarifies an HTTP status
// code but does not override it.
// +optional
Reason string `json:"reason,omitempty"`
// Suggested HTTP return code for this status, 0 if not set.
// +optional
Code int32 `json:"code,omitempty"`
}
48 changes: 48 additions & 0 deletions backend/admission_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package backend

import (
"context"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

// admissionSDKAdapter adapter between low level plugin protocol and SDK interfaces.
type admissionSDKAdapter struct {
handler AdmissionHandler
}

func newAdmissionSDKAdapter(handler AdmissionHandler) *admissionSDKAdapter {
return &admissionSDKAdapter{
handler: handler,
}
}

func (a *admissionSDKAdapter) ValidateAdmission(ctx context.Context, req *pluginv2.AdmissionRequest) (*pluginv2.ValidationResponse, error) {
ctx = propagateTenantIDIfPresent(ctx)
parsedReq := FromProto().AdmissionRequest(req)
resp, err := a.handler.ValidateAdmission(ctx, parsedReq)
if err != nil {
return nil, err
}
return ToProto().ValidationResponse(resp), nil
}

func (a *admissionSDKAdapter) MutateAdmission(ctx context.Context, req *pluginv2.AdmissionRequest) (*pluginv2.MutationResponse, error) {
ctx = propagateTenantIDIfPresent(ctx)
parsedReq := FromProto().AdmissionRequest(req)
resp, err := a.handler.MutateAdmission(ctx, parsedReq)
if err != nil {
return nil, err
}
return ToProto().MutationResponse(resp), nil
}

func (a *admissionSDKAdapter) ConvertObject(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
ctx = propagateTenantIDIfPresent(ctx)
parsedReq := FromProto().ConversionRequest(req)
resp, err := a.handler.ConvertObject(ctx, parsedReq)
if err != nil {
return nil, err
}
return ToProto().ConversionResponse(resp), nil
}
6 changes: 5 additions & 1 deletion backend/app/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/internal/buildinfo"
)

// ManageOpts can modify Manage behaviour.
// ManageOpts can modify Manage behavior.
type ManageOpts struct {
// GRPCSettings settings for gPRC.
GRPCSettings backend.GRPCSettings

// TracingOpts contains settings for tracing setup.
TracingOpts tracing.Opts

// Stateless admission handler
AdmissionHandler backend.AdmissionHandler
}

// Manage starts serving the app over gPRC with automatic instance management.
Expand All @@ -43,6 +46,7 @@ func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpt
CallResourceHandler: handler,
QueryDataHandler: handler,
StreamHandler: handler,
AdmissionHandler: opts.AdmissionHandler,
GRPCSettings: opts.GRPCSettings,
})
}
16 changes: 16 additions & 0 deletions backend/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ func (s *AppInstanceSettings) HTTPClientOptions(_ context.Context) (httpclient.O
return opts, nil
}

func (s *AppInstanceSettings) GVK() GroupVersionKind {
return GroupVersionKind{
Group: "grafana-plugin-sdk-go", // raw protobuf
Version: s.APIVersion,
Kind: "AppInstanceSettings",
}
}

// DataSourceInstanceSettings represents settings for a data source instance.
//
// In Grafana a data source instance is a data source plugin of certain
Expand Down Expand Up @@ -145,6 +153,14 @@ func (s *DataSourceInstanceSettings) HTTPClientOptions(ctx context.Context) (htt
return opts, nil
}

func (s *DataSourceInstanceSettings) GVK() GroupVersionKind {
return GroupVersionKind{
Group: "grafana-plugin-sdk-go", // raw protobuf
Version: s.APIVersion,
Kind: "DataSourceInstanceSettings",
}
}

// PluginContext holds contextual information about a plugin request, such as
// Grafana organization, user and plugin instance settings.
type PluginContext struct {
Expand Down
71 changes: 71 additions & 0 deletions backend/convert_from_protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,77 @@ func (f ConvertFromProtobuf) StreamPacket(protoReq *pluginv2.StreamPacket) *Stre
}
}

// StatusResult ...
func (f ConvertFromProtobuf) StatusResult(s *pluginv2.StatusResult) *StatusResult {
if s == nil {
return nil
}
return &StatusResult{
Status: s.Status,
Message: s.Message,
Reason: s.Reason,
Code: s.Code,
}
}

// GroupVersionKind ...
func (f ConvertFromProtobuf) GroupVersionKind(req *pluginv2.GroupVersionKind) GroupVersionKind {
return GroupVersionKind{
Group: req.Group,
Version: req.Version,
Kind: req.Kind,
}
}

// AdmissionRequest ...
func (f ConvertFromProtobuf) AdmissionRequest(req *pluginv2.AdmissionRequest) *AdmissionRequest {
return &AdmissionRequest{
PluginContext: f.PluginContext(req.PluginContext),
Operation: AdmissionRequestOperation(req.Operation),
Kind: f.GroupVersionKind(req.Kind),
ObjectBytes: req.ObjectBytes,
OldObjectBytes: req.OldObjectBytes,
}
}

// ConversionRequest ...
func (f ConvertFromProtobuf) ConversionRequest(req *pluginv2.ConversionRequest) *ConversionRequest {
return &ConversionRequest{
PluginContext: f.PluginContext(req.PluginContext),
Kind: f.GroupVersionKind(req.Kind),
ObjectBytes: req.ObjectBytes,
TargetVersion: req.TargetVersion,
}
}

// MutationResponse ...
func (f ConvertFromProtobuf) MutationResponse(rsp *pluginv2.MutationResponse) *MutationResponse {
return &MutationResponse{
Allowed: rsp.Allowed,
Result: f.StatusResult(rsp.Result),
Warnings: rsp.Warnings,
ObjectBytes: rsp.ObjectBytes,
}
}

// ValidationResponse ...
func (f ConvertFromProtobuf) ValidationResponse(rsp *pluginv2.ValidationResponse) *ValidationResponse {
return &ValidationResponse{
Allowed: rsp.Allowed,
Result: f.StatusResult(rsp.Result),
Warnings: rsp.Warnings,
}
}

// ConversionResponse ...
func (f ConvertFromProtobuf) ConversionResponse(rsp *pluginv2.ConversionResponse) *ConversionResponse {
return &ConversionResponse{
Allowed: rsp.Allowed,
Result: f.StatusResult(rsp.Result),
ObjectBytes: rsp.ObjectBytes,
}
}

func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *GrafanaCfg {
return NewGrafanaCfg(cfg)
}
Loading
Loading