mirror of https://github.com/grafana/grafana
K8s/Dashboards: Delegate large objects to blob store (#94943)
parent
b1c5aa0929
commit
c0de407fee
@ -0,0 +1,52 @@ |
||||
package dashboard |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
|
||||
commonV0 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" |
||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore" |
||||
) |
||||
|
||||
func newDashboardLargeObjectSupport() *apistore.BasicLargeObjectSupport { |
||||
return &apistore.BasicLargeObjectSupport{ |
||||
TheGroupResource: dashboard.DashboardResourceInfo.GroupResource(), |
||||
|
||||
// byte size, while testing lets do almost everything (10bytes)
|
||||
ThresholdSize: 10, |
||||
|
||||
// 10mb -- we should check what the largest ones are... might be bigger
|
||||
MaxByteSize: 10 * 1024 * 1024, |
||||
|
||||
ReduceSpec: func(obj runtime.Object) error { |
||||
dash, ok := obj.(*dashboard.Dashboard) |
||||
if !ok { |
||||
return fmt.Errorf("expected dashboard") |
||||
} |
||||
old := dash.Spec.Object |
||||
spec := commonV0.Unstructured{Object: make(map[string]any)} |
||||
dash.Spec = spec |
||||
dash.SetManagedFields(nil) // this could be bigger than the object!
|
||||
|
||||
keep := []string{"title", "description", "schemaVersion"} |
||||
for _, k := range keep { |
||||
v, ok := old[k] |
||||
if ok { |
||||
spec.Object[k] = v |
||||
} |
||||
} |
||||
return nil |
||||
}, |
||||
|
||||
RebuildSpec: func(obj runtime.Object, blob []byte) error { |
||||
dash, ok := obj.(*dashboard.Dashboard) |
||||
if !ok { |
||||
return fmt.Errorf("expected dashboard") |
||||
} |
||||
return json.Unmarshal(blob, &dash.Spec) |
||||
}, |
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@ |
||||
package dashboard |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
|
||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" |
||||
) |
||||
|
||||
func TestLargeDashboardSupport(t *testing.T) { |
||||
devdash := "../../../../devenv/dev-dashboards/all-panels.json" |
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
|
||||
f, err := os.ReadFile(devdash) |
||||
require.NoError(t, err) |
||||
|
||||
dash := &dashboard.Dashboard{ |
||||
ObjectMeta: v1.ObjectMeta{ |
||||
Name: "test", |
||||
Namespace: "test", |
||||
}, |
||||
} |
||||
err = json.Unmarshal(f, &dash.Spec) |
||||
require.NoError(t, err) |
||||
|
||||
expectedPanelCount := 19 |
||||
panels, found, err := unstructured.NestedSlice(dash.Spec.Object, "panels") |
||||
require.NoError(t, err) |
||||
require.True(t, found) |
||||
require.Len(t, panels, expectedPanelCount) |
||||
|
||||
largeObject := newDashboardLargeObjectSupport() |
||||
|
||||
// Convert the dashboard to a small value
|
||||
err = largeObject.ReduceSpec(dash) |
||||
require.NoError(t, err) |
||||
|
||||
small, err := json.MarshalIndent(&dash.Spec, "", " ") |
||||
require.NoError(t, err) |
||||
require.JSONEq(t, `{ |
||||
"schemaVersion": 33, |
||||
"title": "Panel tests - All panels" |
||||
}`, string(small)) |
||||
|
||||
// Now make it big again
|
||||
err = largeObject.RebuildSpec(dash, f) |
||||
require.NoError(t, err) |
||||
|
||||
// check that all panels exist again
|
||||
panels, found, err = unstructured.NestedSlice(dash.Spec.Object, "panels") |
||||
require.NoError(t, err) |
||||
require.True(t, found) |
||||
require.Len(t, panels, expectedPanelCount) |
||||
} |
||||
@ -0,0 +1,166 @@ |
||||
package apistore |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/apimachinery/utils" |
||||
"github.com/grafana/grafana/pkg/storage/unified/resource" |
||||
) |
||||
|
||||
type LargeObjectSupport interface { |
||||
// The resource this can process
|
||||
GroupResource() schema.GroupResource |
||||
|
||||
// The size that triggers delegating part of the object to blob storage
|
||||
Threshold() int |
||||
|
||||
// Each resource may have a maximum size that is different than the global maximum
|
||||
// for example, we know we will allow dashboards up to 10mb, however most
|
||||
// resources should have a smaller limit (1mb?)
|
||||
MaxSize() int |
||||
|
||||
// Deconstruct takes a large object, write most of it to blob storage and leave a few metadata bits around to help with list
|
||||
// NOTE: changes to the object must be handled by mutating the input obj
|
||||
Deconstruct(ctx context.Context, key *resource.ResourceKey, client resource.BlobStoreClient, obj utils.GrafanaMetaAccessor, raw []byte) error |
||||
|
||||
// Reconstruct will join the resource+blob back into a complete resource
|
||||
// NOTE: changes to the object must be handled by mutating the input obj
|
||||
Reconstruct(ctx context.Context, key *resource.ResourceKey, client resource.BlobStoreClient, obj utils.GrafanaMetaAccessor) error |
||||
} |
||||
|
||||
var _ LargeObjectSupport = (*BasicLargeObjectSupport)(nil) |
||||
|
||||
type BasicLargeObjectSupport struct { |
||||
TheGroupResource schema.GroupResource |
||||
ThresholdSize int |
||||
MaxByteSize int |
||||
|
||||
// Mutate the spec so it only has the small properties
|
||||
ReduceSpec func(obj runtime.Object) error |
||||
|
||||
// Update the spec so it has the full object
|
||||
// This is used to support server-side apply
|
||||
RebuildSpec func(obj runtime.Object, blob []byte) error |
||||
} |
||||
|
||||
func (s *BasicLargeObjectSupport) GroupResource() schema.GroupResource { |
||||
return s.TheGroupResource |
||||
} |
||||
|
||||
// Threshold implements LargeObjectSupport.
|
||||
func (s *BasicLargeObjectSupport) Threshold() int { |
||||
return s.ThresholdSize |
||||
} |
||||
|
||||
// MaxSize implements LargeObjectSupport.
|
||||
func (s *BasicLargeObjectSupport) MaxSize() int { |
||||
return s.MaxByteSize |
||||
} |
||||
|
||||
// Deconstruct implements LargeObjectSupport.
|
||||
func (s *BasicLargeObjectSupport) Deconstruct(ctx context.Context, key *resource.ResourceKey, client resource.BlobStoreClient, obj utils.GrafanaMetaAccessor, raw []byte) error { |
||||
if key.Group != s.TheGroupResource.Group { |
||||
return fmt.Errorf("requested group mismatch") |
||||
} |
||||
if key.Resource != s.TheGroupResource.Resource { |
||||
return fmt.Errorf("requested resource mismatch") |
||||
} |
||||
|
||||
spec, err := obj.GetSpec() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var val []byte |
||||
|
||||
// :( could not figure out custom JSON marshaling
|
||||
// with pointer receiver... this is a quick fix to support dashboards
|
||||
u, ok := spec.(common.Unstructured) |
||||
if ok { |
||||
val, err = json.Marshal(u.Object) |
||||
} else { |
||||
val, err = json.Marshal(spec) |
||||
} |
||||
|
||||
// Write only the spec
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
rt, ok := obj.GetRuntimeObject() |
||||
if !ok { |
||||
return fmt.Errorf("expected runtime object") |
||||
} |
||||
|
||||
err = s.ReduceSpec(rt) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Save the blob
|
||||
info, err := client.PutBlob(ctx, &resource.PutBlobRequest{ |
||||
ContentType: "application/json", |
||||
Value: val, |
||||
Resource: key, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Update the resource metadata with the blob info
|
||||
obj.SetBlob(&utils.BlobInfo{ |
||||
UID: info.Uid, |
||||
Size: info.Size, |
||||
Hash: info.Hash, |
||||
MimeType: info.MimeType, |
||||
Charset: info.Charset, |
||||
}) |
||||
return err |
||||
} |
||||
|
||||
// Reconstruct implements LargeObjectSupport.
|
||||
func (s *BasicLargeObjectSupport) Reconstruct(ctx context.Context, key *resource.ResourceKey, client resource.BlobStoreClient, obj utils.GrafanaMetaAccessor) error { |
||||
blobInfo := obj.GetBlob() |
||||
if blobInfo == nil { |
||||
return fmt.Errorf("the object does not have a blob") |
||||
} |
||||
|
||||
rv, err := obj.GetResourceVersionInt64() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
rsp, err := client.GetBlob(ctx, &resource.GetBlobRequest{ |
||||
Resource: &resource.ResourceKey{ |
||||
Group: s.TheGroupResource.Group, |
||||
Resource: s.TheGroupResource.Resource, |
||||
Namespace: obj.GetNamespace(), |
||||
Name: obj.GetName(), |
||||
}, |
||||
MustProxyBytes: true, |
||||
ResourceVersion: rv, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if rsp.Error != nil { |
||||
return fmt.Errorf("error loading value from object store %+v", rsp.Error) |
||||
} |
||||
|
||||
// Replace the spec with the value saved in the blob store
|
||||
if len(rsp.Value) == 0 { |
||||
return fmt.Errorf("empty blob value") |
||||
} |
||||
|
||||
rt, ok := obj.GetRuntimeObject() |
||||
if !ok { |
||||
return fmt.Errorf("unable to get raw object") |
||||
} |
||||
obj.SetBlob(nil) // remove the blob info
|
||||
return s.RebuildSpec(rt, rsp.Value) |
||||
} |
||||
Loading…
Reference in new issue