Skip to content

Commit

Permalink
API: Add new AdmissionControl service (experimental for now) (#983)
Browse files Browse the repository at this point in the history
Co-authored-by: Andres Martinez Gotor <andres.martinez@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
  • Loading branch information
3 people committed May 24, 2024
1 parent ee05993 commit 94941f4
Show file tree
Hide file tree
Showing 17 changed files with 1,888 additions and 130 deletions.
136 changes: 136 additions & 0 deletions backend/admission.go
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 {
// 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)
}

0 comments on commit 94941f4

Please sign in to comment.