-
Notifications
You must be signed in to change notification settings - Fork 57
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
Changes from all commits
366c6cd
5b1a3ad
479ee40
0a0e017
8debcd4
4f7ef3c
53b94e1
20b1338
80f737d
e42def7
bb04333
c0a3f78
f850b5f
35a409b
fc6110a
823af31
43b23be
1e20bd7
d793710
186c771
88f4b29
9aee73f
61197ea
66df1d5
9686768
0a5fbac
218525d
68e4b33
416574f
a437721
be57ff5
fcce0f6
3662d46
9f22d16
a5f8757
62dae98
fd2f15b
9b8dfd5
48f12a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
} |
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 | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 vsbackend.AdmissionHandler
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.