Skip to content

Commit 94941f4

Browse files
ryantxuandresmgotmarefr
authored
API: Add new AdmissionControl service (experimental for now) (#983)
Co-authored-by: Andres Martinez Gotor <[email protected]> Co-authored-by: Marcus Efraimsson <[email protected]>
1 parent ee05993 commit 94941f4

17 files changed

+1888
-130
lines changed

backend/admission.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package backend
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
7+
)
8+
9+
// AdmissionHandler is an EXPERIMENTAL service that allows checking objects before they are saved
10+
// This is modeled after the kubernetes model for admission controllers
11+
// Since grafana 11.1, this feature is under active development and will continue to evolve in 2024
12+
// This may also be replaced with a more native kubernetes solution that does not work with existing tooling
13+
type AdmissionHandler interface {
14+
// ValidateAdmission is a simple yes|no check if an object can be saved
15+
ValidateAdmission(context.Context, *AdmissionRequest) (*ValidationResponse, error)
16+
// MutateAdmission converts the input into an object that can be saved, or rejects the request
17+
MutateAdmission(context.Context, *AdmissionRequest) (*MutationResponse, error)
18+
// ConvertObject is called to covert objects between different versions
19+
ConvertObject(context.Context, *ConversionRequest) (*ConversionResponse, error)
20+
}
21+
22+
type ValidateAdmissionFunc func(context.Context, *AdmissionRequest) (*ValidationResponse, error)
23+
type MutateAdmissionFunc func(context.Context, *AdmissionRequest) (*MutationResponse, error)
24+
type ConvertObjectFunc func(context.Context, *ConversionRequest) (*ConversionResponse, error)
25+
26+
// Operation is the type of resource operation being checked for admission control
27+
// https://github.com/kubernetes/kubernetes/blob/v1.30.0/pkg/apis/admission/types.go#L158
28+
type AdmissionRequestOperation int32
29+
30+
const (
31+
AdmissionRequestCreate AdmissionRequestOperation = 0
32+
AdmissionRequestUpdate AdmissionRequestOperation = 1
33+
AdmissionRequestDelete AdmissionRequestOperation = 2
34+
)
35+
36+
// String textual representation of the operation.
37+
func (o AdmissionRequestOperation) String() string {
38+
return pluginv2.AdmissionRequest_Operation(o).String()
39+
}
40+
41+
// Identify the Object properties
42+
type GroupVersionKind struct {
43+
Group string `json:"group,omitempty"`
44+
Version string `json:"version,omitempty"`
45+
Kind string `json:"kind,omitempty"`
46+
}
47+
48+
type AdmissionRequest struct {
49+
// NOTE: this may not include populated instance settings depending on the request
50+
PluginContext PluginContext `json:"pluginContext,omitempty"`
51+
// The requested operation
52+
Operation AdmissionRequestOperation `json:"operation,omitempty"`
53+
// The object kind
54+
Kind GroupVersionKind `json:"kind,omitempty"`
55+
// Object is the object in the request. This includes the full metadata envelope.
56+
ObjectBytes []byte `json:"object_bytes,omitempty"`
57+
// OldObject is the object as it currently exists in storage. This includes the full metadata envelope.
58+
OldObjectBytes []byte `json:"old_object_bytes,omitempty"`
59+
}
60+
61+
// ConversionRequest supports converting an object from on version to another
62+
type ConversionRequest struct {
63+
// NOTE: this may not include app or datasource instance settings depending on the request
64+
PluginContext PluginContext `json:"pluginContext,omitempty"`
65+
// The object kind
66+
Kind GroupVersionKind `json:"kind,omitempty"`
67+
// Object is the object in the request. This includes the full metadata envelope.
68+
ObjectBytes []byte `json:"object_bytes,omitempty"`
69+
// Target converted version
70+
TargetVersion string `json:"target_version,omitempty"`
71+
}
72+
73+
// Basic request to say if the validation was successful or not
74+
type ValidationResponse struct {
75+
// Allowed indicates whether or not the admission request was permitted.
76+
Allowed bool `json:"allowed,omitempty"`
77+
// Result contains extra details into why an admission request was denied.
78+
// This field IS NOT consulted in any way if "Allowed" is "true".
79+
// +optional
80+
Result *StatusResult `json:"result,omitempty"`
81+
// warnings is a list of warning messages to return to the requesting API client.
82+
// Warning messages describe a problem the client making the API request should correct or be aware of.
83+
// Limit warnings to 120 characters if possible.
84+
// Warnings over 256 characters and large numbers of warnings may be truncated.
85+
// +optional
86+
Warnings []string `json:"warnings,omitempty"`
87+
}
88+
89+
type MutationResponse struct {
90+
// Allowed indicates whether or not the admission request was permitted.
91+
Allowed bool `json:"allowed,omitempty"`
92+
// Result contains extra details into why an admission request was denied.
93+
// This field IS NOT consulted in any way if "Allowed" is "true".
94+
// +optional
95+
Result *StatusResult `json:"result,omitempty"`
96+
// warnings is a list of warning messages to return to the requesting API client.
97+
// Warning messages describe a problem the client making the API request should correct or be aware of.
98+
// Limit warnings to 120 characters if possible.
99+
// Warnings over 256 characters and large numbers of warnings may be truncated.
100+
// +optional
101+
Warnings []string `json:"warnings,omitempty"`
102+
// Mutated object bytes (when requested)
103+
// +optional
104+
ObjectBytes []byte `json:"object_bytes,omitempty"`
105+
}
106+
107+
type ConversionResponse struct {
108+
// Allowed indicates whether or not the admission request was permitted.
109+
Allowed bool `json:"allowed,omitempty"`
110+
// Result contains extra details into why an admission request was denied.
111+
// This field IS NOT consulted in any way if "Allowed" is "true".
112+
// +optional
113+
Result *StatusResult `json:"result,omitempty"`
114+
// Converted object bytes
115+
ObjectBytes []byte `json:"object_bytes,omitempty"`
116+
}
117+
118+
type StatusResult struct {
119+
// Status of the operation.
120+
// One of: "Success" or "Failure".
121+
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
122+
// +optional
123+
Status string `json:"status,omitempty"`
124+
// A human-readable description of the status of this operation.
125+
// +optional
126+
Message string `json:"message,omitempty"`
127+
// A machine-readable description of why this operation is in the
128+
// "Failure" status. If this value is empty there
129+
// is no information available. A Reason clarifies an HTTP status
130+
// code but does not override it.
131+
// +optional
132+
Reason string `json:"reason,omitempty"`
133+
// Suggested HTTP return code for this status, 0 if not set.
134+
// +optional
135+
Code int32 `json:"code,omitempty"`
136+
}

backend/admission_adapter.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package backend
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
7+
)
8+
9+
// admissionSDKAdapter adapter between low level plugin protocol and SDK interfaces.
10+
type admissionSDKAdapter struct {
11+
handler AdmissionHandler
12+
}
13+
14+
func newAdmissionSDKAdapter(handler AdmissionHandler) *admissionSDKAdapter {
15+
return &admissionSDKAdapter{
16+
handler: handler,
17+
}
18+
}
19+
20+
func (a *admissionSDKAdapter) ValidateAdmission(ctx context.Context, req *pluginv2.AdmissionRequest) (*pluginv2.ValidationResponse, error) {
21+
ctx = propagateTenantIDIfPresent(ctx)
22+
parsedReq := FromProto().AdmissionRequest(req)
23+
resp, err := a.handler.ValidateAdmission(ctx, parsedReq)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return ToProto().ValidationResponse(resp), nil
28+
}
29+
30+
func (a *admissionSDKAdapter) MutateAdmission(ctx context.Context, req *pluginv2.AdmissionRequest) (*pluginv2.MutationResponse, error) {
31+
ctx = propagateTenantIDIfPresent(ctx)
32+
parsedReq := FromProto().AdmissionRequest(req)
33+
resp, err := a.handler.MutateAdmission(ctx, parsedReq)
34+
if err != nil {
35+
return nil, err
36+
}
37+
return ToProto().MutationResponse(resp), nil
38+
}
39+
40+
func (a *admissionSDKAdapter) ConvertObject(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
41+
ctx = propagateTenantIDIfPresent(ctx)
42+
parsedReq := FromProto().ConversionRequest(req)
43+
resp, err := a.handler.ConvertObject(ctx, parsedReq)
44+
if err != nil {
45+
return nil, err
46+
}
47+
return ToProto().ConversionResponse(resp), nil
48+
}

backend/app/manage.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import (
1111
"github.com/grafana/grafana-plugin-sdk-go/internal/buildinfo"
1212
)
1313

14-
// ManageOpts can modify Manage behaviour.
14+
// ManageOpts can modify Manage behavior.
1515
type ManageOpts struct {
1616
// GRPCSettings settings for gPRC.
1717
GRPCSettings backend.GRPCSettings
1818

1919
// TracingOpts contains settings for tracing setup.
2020
TracingOpts tracing.Opts
21+
22+
// Stateless admission handler
23+
AdmissionHandler backend.AdmissionHandler
2124
}
2225

2326
// Manage starts serving the app over gPRC with automatic instance management.
@@ -43,6 +46,7 @@ func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpt
4346
CallResourceHandler: handler,
4447
QueryDataHandler: handler,
4548
StreamHandler: handler,
49+
AdmissionHandler: opts.AdmissionHandler,
4650
GRPCSettings: opts.GRPCSettings,
4751
})
4852
}

backend/common.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ func (s *AppInstanceSettings) HTTPClientOptions(_ context.Context) (httpclient.O
5656
return opts, nil
5757
}
5858

59+
func (s *AppInstanceSettings) GVK() GroupVersionKind {
60+
return GroupVersionKind{
61+
Group: "grafana-plugin-sdk-go", // raw protobuf
62+
Version: s.APIVersion,
63+
Kind: "AppInstanceSettings",
64+
}
65+
}
66+
5967
// DataSourceInstanceSettings represents settings for a data source instance.
6068
//
6169
// In Grafana a data source instance is a data source plugin of certain
@@ -145,6 +153,14 @@ func (s *DataSourceInstanceSettings) HTTPClientOptions(ctx context.Context) (htt
145153
return opts, nil
146154
}
147155

156+
func (s *DataSourceInstanceSettings) GVK() GroupVersionKind {
157+
return GroupVersionKind{
158+
Group: "grafana-plugin-sdk-go", // raw protobuf
159+
Version: s.APIVersion,
160+
Kind: "DataSourceInstanceSettings",
161+
}
162+
}
163+
148164
// PluginContext holds contextual information about a plugin request, such as
149165
// Grafana organization, user and plugin instance settings.
150166
type PluginContext struct {

backend/convert_from_protobuf.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,77 @@ func (f ConvertFromProtobuf) StreamPacket(protoReq *pluginv2.StreamPacket) *Stre
297297
}
298298
}
299299

300+
// StatusResult ...
301+
func (f ConvertFromProtobuf) StatusResult(s *pluginv2.StatusResult) *StatusResult {
302+
if s == nil {
303+
return nil
304+
}
305+
return &StatusResult{
306+
Status: s.Status,
307+
Message: s.Message,
308+
Reason: s.Reason,
309+
Code: s.Code,
310+
}
311+
}
312+
313+
// GroupVersionKind ...
314+
func (f ConvertFromProtobuf) GroupVersionKind(req *pluginv2.GroupVersionKind) GroupVersionKind {
315+
return GroupVersionKind{
316+
Group: req.Group,
317+
Version: req.Version,
318+
Kind: req.Kind,
319+
}
320+
}
321+
322+
// AdmissionRequest ...
323+
func (f ConvertFromProtobuf) AdmissionRequest(req *pluginv2.AdmissionRequest) *AdmissionRequest {
324+
return &AdmissionRequest{
325+
PluginContext: f.PluginContext(req.PluginContext),
326+
Operation: AdmissionRequestOperation(req.Operation),
327+
Kind: f.GroupVersionKind(req.Kind),
328+
ObjectBytes: req.ObjectBytes,
329+
OldObjectBytes: req.OldObjectBytes,
330+
}
331+
}
332+
333+
// ConversionRequest ...
334+
func (f ConvertFromProtobuf) ConversionRequest(req *pluginv2.ConversionRequest) *ConversionRequest {
335+
return &ConversionRequest{
336+
PluginContext: f.PluginContext(req.PluginContext),
337+
Kind: f.GroupVersionKind(req.Kind),
338+
ObjectBytes: req.ObjectBytes,
339+
TargetVersion: req.TargetVersion,
340+
}
341+
}
342+
343+
// MutationResponse ...
344+
func (f ConvertFromProtobuf) MutationResponse(rsp *pluginv2.MutationResponse) *MutationResponse {
345+
return &MutationResponse{
346+
Allowed: rsp.Allowed,
347+
Result: f.StatusResult(rsp.Result),
348+
Warnings: rsp.Warnings,
349+
ObjectBytes: rsp.ObjectBytes,
350+
}
351+
}
352+
353+
// ValidationResponse ...
354+
func (f ConvertFromProtobuf) ValidationResponse(rsp *pluginv2.ValidationResponse) *ValidationResponse {
355+
return &ValidationResponse{
356+
Allowed: rsp.Allowed,
357+
Result: f.StatusResult(rsp.Result),
358+
Warnings: rsp.Warnings,
359+
}
360+
}
361+
362+
// ConversionResponse ...
363+
func (f ConvertFromProtobuf) ConversionResponse(rsp *pluginv2.ConversionResponse) *ConversionResponse {
364+
return &ConversionResponse{
365+
Allowed: rsp.Allowed,
366+
Result: f.StatusResult(rsp.Result),
367+
ObjectBytes: rsp.ObjectBytes,
368+
}
369+
}
370+
300371
func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *GrafanaCfg {
301372
return NewGrafanaCfg(cfg)
302373
}

0 commit comments

Comments
 (0)