Skip to content

Commit

Permalink
API: Add ResourceConversion service (#1044)
Browse files Browse the repository at this point in the history
  • Loading branch information
toddtreece authored Aug 15, 2024
1 parent fbe8fc3 commit 6158ec5
Show file tree
Hide file tree
Showing 18 changed files with 1,222 additions and 476 deletions.
29 changes: 0 additions & 29 deletions backend/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ const (

// EndpointMutateAdmission friendly name for the mutate admission endpoint/handler.
EndpointMutateAdmission Endpoint = "mutateAdmission"

// EndpointConvertObject friendly name for the convert object endpoint/handler.
EndpointConvertObject Endpoint = "convertObject"
)

// AdmissionHandler is an EXPERIMENTAL service that allows checking objects before they are saved
Expand All @@ -26,13 +23,10 @@ type AdmissionHandler interface {
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
Expand Down Expand Up @@ -69,18 +63,6 @@ type AdmissionRequest struct {
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.
Expand Down Expand Up @@ -115,17 +97,6 @@ type MutationResponse struct {
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".
Expand Down
17 changes: 0 additions & 17 deletions backend/admission_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,3 @@ func (a *admissionSDKAdapter) MutateAdmission(ctx context.Context, req *pluginv2

return ToProto().MutationResponse(resp), nil
}

func (a *admissionSDKAdapter) ConvertObject(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
ctx = setupContext(ctx, EndpointConvertObject)
parsedReq := FromProto().ConversionRequest(req)

var resp *ConversionResponse
err := wrapHandler(ctx, parsedReq.PluginContext, func(ctx context.Context) (RequestStatus, error) {
var innerErr error
resp, innerErr = a.handler.ConvertObject(ctx, parsedReq)
return RequestStatusFromError(innerErr), innerErr
})
if err != nil {
return nil, err
}

return ToProto().ConversionResponse(resp), nil
}
4 changes: 4 additions & 0 deletions backend/app/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type ManageOpts struct {

// Stateless admission handler
AdmissionHandler backend.AdmissionHandler

// Stateless conversion handler
ConversionHandler backend.ConversionHandler
}

// Manage starts serving the app over gPRC with automatic instance management.
Expand All @@ -47,6 +50,7 @@ func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpt
QueryDataHandler: handler,
StreamHandler: handler,
AdmissionHandler: opts.AdmissionHandler,
ConversionHandler: opts.ConversionHandler,
GRPCSettings: opts.GRPCSettings,
})
}
59 changes: 59 additions & 0 deletions backend/conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package backend

import (
"context"
)

const (
// EndpointConvertObject friendly name for the convert object endpoint/handler.
EndpointConvertObject Endpoint = "convertObject"
)

// ConversionHandler is an EXPERIMENTAL service that allows converting objects between versions
// This is modeled after the kubernetes CRD conversion webhooks.
// 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 ConversionHandler interface {
// ConvertObject is called to covert objects between different versions
ConvertObjects(context.Context, *ConversionRequest) (*ConversionResponse, error)
}

type ConvertObjectsFunc func(context.Context, *ConversionRequest) (*ConversionResponse, error)

type GroupVersion struct {
Group string `json:"group,omitempty"`
Version string `json:"version,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"`
// UID is an identifier for the individual request/response. It allows distinguishing instances of requests which are
// otherwise identical (parallel requests, etc).
// The UID is meant to track the round trip (request/response) between the Kubernetes API server and the webhook, not the user request.
// It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.
UID string `json:"uid,omitempty"`
// TargetVersion is the version the object should be converted to.
TargetVersion GroupVersion `json:"target_version,omitempty"`
// Objects is the list of objects to convert. This contains the full metadata envelope.
Objects []RawObject `json:"objects,omitempty"`
}

type RawObject struct {
// Raw is the underlying serialization of this object.
Raw []byte `json:"-" `
// ContentType is the media type of the object.
ContentType string `json:"-"`
}

type ConversionResponse struct {
// UID is an identifier for the individual request/response.
// This should be copied over from the corresponding `request.uid`.
UID string `json:"uid,omitempty"`
// Result contains extra details into why an admission request was denied.
Result *StatusResult `json:"result,omitempty"`
// Objects is the list of converted version of `request.objects` if the `result` is successful, otherwise empty.
Objects []RawObject `json:"objects,omitempty"`
}
34 changes: 34 additions & 0 deletions backend/conversion_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package backend

import (
"context"

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

type conversionSDKAdapter struct {
handler ConversionHandler
}

func newConversionSDKAdapter(handler ConversionHandler) *conversionSDKAdapter {
return &conversionSDKAdapter{
handler: handler,
}
}

func (a *conversionSDKAdapter) ConvertObjects(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
ctx = setupContext(ctx, EndpointConvertObject)
parsedReq := FromProto().ConversionRequest(req)

var resp *ConversionResponse
err := wrapHandler(ctx, parsedReq.PluginContext, func(ctx context.Context) (RequestStatus, error) {
var innerErr error
resp, innerErr = a.handler.ConvertObjects(ctx, parsedReq)
return RequestStatusFromError(innerErr), innerErr
})
if err != nil {
return nil, err
}

return ToProto().ConversionResponse(resp), nil
}
37 changes: 31 additions & 6 deletions backend/convert_from_protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,31 @@ func (f ConvertFromProtobuf) GroupVersionKind(req *pluginv2.GroupVersionKind) Gr
}
}

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

// RawObject ...
func (f ConvertFromProtobuf) RawObject(req *pluginv2.RawObject) RawObject {
return RawObject{
Raw: req.Raw,
ContentType: req.ContentType,
}
}

// RawObjects ...
func (f ConvertFromProtobuf) RawObjects(req []*pluginv2.RawObject) []RawObject {
rawObjects := make([]RawObject, len(req))
for i, r := range req {
rawObjects[i] = f.RawObject(r)
}
return rawObjects
}

// AdmissionRequest ...
func (f ConvertFromProtobuf) AdmissionRequest(req *pluginv2.AdmissionRequest) *AdmissionRequest {
return &AdmissionRequest{
Expand All @@ -334,9 +359,9 @@ func (f ConvertFromProtobuf) AdmissionRequest(req *pluginv2.AdmissionRequest) *A
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,
UID: req.Uid,
Objects: f.RawObjects(req.Objects),
TargetVersion: f.GroupVersion(req.TargetVersion),
}
}

Expand All @@ -362,9 +387,9 @@ func (f ConvertFromProtobuf) ValidationResponse(rsp *pluginv2.ValidationResponse
// ConversionResponse ...
func (f ConvertFromProtobuf) ConversionResponse(rsp *pluginv2.ConversionResponse) *ConversionResponse {
return &ConversionResponse{
Allowed: rsp.Allowed,
Result: f.StatusResult(rsp.Result),
ObjectBytes: rsp.ObjectBytes,
UID: rsp.Uid,
Result: f.StatusResult(rsp.Result),
Objects: f.RawObjects(rsp.Objects),
}
}

Expand Down
Loading

0 comments on commit 6158ec5

Please sign in to comment.