K8s: Remove restore functionality; can be done with list (#102560)
This commit is contained in:
@@ -41,11 +41,9 @@ func GetAuthorizer(dashboardService dashboards.DashboardService, l log.Logger) a
|
||||
}
|
||||
|
||||
// expensive path to lookup permissions for a single dashboard
|
||||
// must include deleted to allow for restores
|
||||
dto, err := dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{
|
||||
UID: attr.GetName(),
|
||||
OrgID: info.OrgID,
|
||||
IncludeDeleted: true,
|
||||
UID: attr.GetName(),
|
||||
OrgID: info.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "error loading dashboard", err
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
// LatestConnector will return the latest version of the resource - even if it is deleted
|
||||
type LatestConnector interface {
|
||||
rest.Storage
|
||||
rest.Connecter
|
||||
rest.StorageMetadata
|
||||
}
|
||||
|
||||
func NewLatestConnector(unified resource.ResourceClient, gr schema.GroupResource) LatestConnector {
|
||||
return &latestREST{
|
||||
unified: unified,
|
||||
gr: gr,
|
||||
}
|
||||
}
|
||||
|
||||
type latestREST struct {
|
||||
unified resource.ResourceClient
|
||||
gr schema.GroupResource
|
||||
}
|
||||
|
||||
func (l *latestREST) New() runtime.Object {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (l *latestREST) Destroy() {
|
||||
}
|
||||
|
||||
func (l *latestREST) ConnectMethods() []string {
|
||||
return []string{"GET"}
|
||||
}
|
||||
|
||||
func (l *latestREST) ProducesMIMETypes(verb string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *latestREST) ProducesObject(verb string) interface{} {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (l *latestREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
func (l *latestREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &resource.ResourceKey{
|
||||
Namespace: info.Value,
|
||||
Group: l.gr.Group,
|
||||
Resource: l.gr.Resource,
|
||||
Name: uid,
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
rsp, err := l.unified.Read(ctx, &resource.ReadRequest{
|
||||
Key: key,
|
||||
ResourceVersion: 0, // 0 will return the latest version that was not a delete event
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
} else if rsp == nil || (rsp.Error != nil && rsp.Error.Code == http.StatusNotFound) {
|
||||
responder.Error(storage.NewKeyNotFoundError(uid, 0))
|
||||
return
|
||||
} else if rsp.Error != nil {
|
||||
responder.Error(fmt.Errorf("could not retrieve object: %s", rsp.Error.Message))
|
||||
return
|
||||
}
|
||||
|
||||
uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, rsp.Value)
|
||||
if err != nil {
|
||||
responder.Error(fmt.Errorf("could not convert object: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
finalObj := uncastObj.(*unstructured.Unstructured)
|
||||
finalObj.SetResourceVersion(strconv.FormatInt(rsp.ResourceVersion, 10))
|
||||
|
||||
responder.Object(http.StatusOK, finalObj)
|
||||
}), nil
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
func TestLatest(t *testing.T) {
|
||||
gr := schema.GroupResource{
|
||||
Group: "group",
|
||||
Resource: "resource",
|
||||
}
|
||||
ctx := context.Background()
|
||||
mockResponder := &mockResponder{}
|
||||
mockClient := &mockResourceClient{}
|
||||
r := &latestREST{
|
||||
unified: mockClient,
|
||||
gr: gr,
|
||||
}
|
||||
|
||||
t.Run("no namespace in context", func(t *testing.T) {
|
||||
_, err := r.Connect(ctx, "test-uid", nil, mockResponder)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
ctx = request.WithNamespace(context.Background(), "default")
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/latest", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
readReq := &resource.ReadRequest{
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: "group",
|
||||
Resource: "resource",
|
||||
Name: "uid",
|
||||
},
|
||||
ResourceVersion: 0,
|
||||
IncludeDeleted: true,
|
||||
}
|
||||
|
||||
expectedObject := &metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "resource",
|
||||
APIVersion: "v0alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "uid",
|
||||
Namespace: "default",
|
||||
ResourceVersion: strconv.FormatInt(123, 10),
|
||||
},
|
||||
}
|
||||
|
||||
val, err := json.Marshal(expectedObject)
|
||||
require.NoError(t, err)
|
||||
mockClient.On("Read", ctx, readReq).Return(&resource.ReadResponse{
|
||||
ResourceVersion: 123,
|
||||
Value: val,
|
||||
}, nil).Once()
|
||||
|
||||
mockResponder.On("Object", http.StatusOK, mock.MatchedBy(func(obj interface{}) bool {
|
||||
unstructuredObj, ok := obj.(*unstructured.Unstructured)
|
||||
expectedMap := map[string]interface{}{
|
||||
"apiVersion": expectedObject.APIVersion,
|
||||
"kind": expectedObject.Kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"name": expectedObject.Name,
|
||||
"namespace": expectedObject.Namespace,
|
||||
"resourceVersion": expectedObject.ResourceVersion,
|
||||
"creationTimestamp": nil,
|
||||
},
|
||||
}
|
||||
return ok && reflect.DeepEqual(unstructuredObj.Object, expectedMap)
|
||||
}))
|
||||
|
||||
handler, err := r.Connect(ctx, "uid", nil, mockResponder)
|
||||
require.NoError(t, err)
|
||||
handler.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
mockResponder.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -70,11 +70,6 @@ func (d *directResourceClient) Read(ctx context.Context, in *resource.ReadReques
|
||||
return d.server.Read(ctx, in)
|
||||
}
|
||||
|
||||
// Restore implements ResourceClient.
|
||||
func (d *directResourceClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
|
||||
return d.server.Restore(ctx, in)
|
||||
}
|
||||
|
||||
// Search implements ResourceClient.
|
||||
func (d *directResourceClient) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
|
||||
return d.server.Search(ctx, in)
|
||||
|
||||
@@ -280,11 +280,6 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
return err
|
||||
}
|
||||
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
|
||||
storage[dashboards.StoragePath("restore")] = NewRestoreConnector(b.unified, gr)
|
||||
storage[dashboards.StoragePath("latest")] = NewLatestConnector(b.unified, gr)
|
||||
}
|
||||
|
||||
// Register the DTO endpoint that will consolidate all dashboard bits
|
||||
storage[dashboards.StoragePath("dto")], err = NewDTOConnector(
|
||||
storage[dashboards.StoragePath()].(rest.Getter),
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
type RestoreConnector interface {
|
||||
rest.Storage
|
||||
rest.Connecter
|
||||
rest.StorageMetadata
|
||||
}
|
||||
|
||||
func NewRestoreConnector(unified resource.ResourceClient, gr schema.GroupResource) RestoreConnector {
|
||||
return &restoreREST{
|
||||
unified: unified,
|
||||
gr: gr,
|
||||
}
|
||||
}
|
||||
|
||||
type restoreREST struct {
|
||||
unified resource.ResourceClient
|
||||
gr schema.GroupResource
|
||||
}
|
||||
|
||||
func (r *restoreREST) New() runtime.Object {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (r *restoreREST) Destroy() {
|
||||
}
|
||||
|
||||
func (r *restoreREST) ConnectMethods() []string {
|
||||
return []string{"POST"}
|
||||
}
|
||||
|
||||
func (r *restoreREST) ProducesMIMETypes(verb string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *restoreREST) ProducesObject(verb string) interface{} {
|
||||
return &metav1.PartialObjectMetadataList{}
|
||||
}
|
||||
|
||||
func (r *restoreREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, ""
|
||||
}
|
||||
|
||||
type RestoreOptions struct {
|
||||
ResourceVersion int64 `json:"resourceVersion"`
|
||||
}
|
||||
|
||||
func (r *restoreREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &resource.ResourceKey{
|
||||
Namespace: info.Value,
|
||||
Group: r.gr.Group,
|
||||
Resource: r.gr.Resource,
|
||||
Name: uid,
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
responder.Error(fmt.Errorf("unable to read request body: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
reqBody := &RestoreOptions{}
|
||||
err = json.Unmarshal(body, &reqBody)
|
||||
if err != nil {
|
||||
responder.Error(fmt.Errorf("unable to unmarshal request body: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.ResourceVersion == 0 {
|
||||
responder.Error(fmt.Errorf("resource version required"))
|
||||
return
|
||||
}
|
||||
|
||||
rsp, err := r.unified.Restore(ctx, &resource.RestoreRequest{
|
||||
ResourceVersion: reqBody.ResourceVersion,
|
||||
Key: key,
|
||||
})
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
} else if rsp == nil || (rsp.Error != nil && rsp.Error.Code == http.StatusNotFound) {
|
||||
responder.Error(storage.NewKeyNotFoundError(uid, reqBody.ResourceVersion))
|
||||
return
|
||||
} else if rsp.Error != nil {
|
||||
responder.Error(fmt.Errorf("could not re-create object: %s", rsp.Error.Message))
|
||||
return
|
||||
}
|
||||
|
||||
obj := metav1.PartialObjectMetadata{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
ResourceVersion: strconv.FormatInt(rsp.ResourceVersion, 10),
|
||||
},
|
||||
}
|
||||
|
||||
responder.Object(http.StatusOK, &obj)
|
||||
}), nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
type mockResourceClient struct {
|
||||
mock.Mock
|
||||
resource.ResourceClient
|
||||
}
|
||||
|
||||
func (m *mockResourceClient) Restore(ctx context.Context, req *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*resource.RestoreResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockResourceClient) Read(ctx context.Context, req *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*resource.ReadResponse), args.Error(1)
|
||||
}
|
||||
|
||||
type mockResponder struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||
m.Called(statusCode, obj)
|
||||
}
|
||||
|
||||
func (m *mockResponder) Error(err error) {
|
||||
m.Called(err)
|
||||
}
|
||||
|
||||
func TestRestore(t *testing.T) {
|
||||
gr := schema.GroupResource{
|
||||
Group: "group",
|
||||
Resource: "resource",
|
||||
}
|
||||
ctx := context.Background()
|
||||
mockResponder := &mockResponder{}
|
||||
mockClient := &mockResourceClient{}
|
||||
r := &restoreREST{
|
||||
unified: mockClient,
|
||||
gr: gr,
|
||||
}
|
||||
|
||||
t.Run("no namespace in context", func(t *testing.T) {
|
||||
_, err := r.Connect(ctx, "test-uid", nil, mockResponder)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
ctx = request.WithNamespace(context.Background(), "default")
|
||||
|
||||
t.Run("invalid resourceVersion", func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/restore", bytes.NewReader([]byte(`{"resourceVersion":0}`)))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
expectedError := fmt.Errorf("resource version required")
|
||||
mockResponder.On("Error", mock.MatchedBy(func(err error) bool {
|
||||
return err.Error() == expectedError.Error()
|
||||
}))
|
||||
|
||||
handler, err := r.Connect(ctx, "test-uid", nil, mockResponder)
|
||||
assert.NoError(t, err)
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
mockResponder.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
req := httptest.NewRequest("POST", "/restore", bytes.NewReader([]byte(`{"resourceVersion":123}`)))
|
||||
w := httptest.NewRecorder()
|
||||
restoreReq := &resource.RestoreRequest{
|
||||
ResourceVersion: 123,
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: "group",
|
||||
Resource: "resource",
|
||||
Name: "uid",
|
||||
},
|
||||
}
|
||||
|
||||
expectedObject := &metav1.PartialObjectMetadata{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "uid",
|
||||
Namespace: "default",
|
||||
ResourceVersion: strconv.FormatInt(123, 10),
|
||||
},
|
||||
}
|
||||
|
||||
mockClient.On("Restore", ctx, restoreReq).Return(&resource.RestoreResponse{
|
||||
ResourceVersion: 123,
|
||||
}, nil).Once()
|
||||
|
||||
mockResponder.On("Object", http.StatusOK, mock.MatchedBy(func(obj interface{}) bool {
|
||||
metadata, ok := obj.(*metav1.PartialObjectMetadata)
|
||||
return ok &&
|
||||
metadata.ObjectMeta.Name == "uid" &&
|
||||
metadata.ObjectMeta.Namespace == "default" &&
|
||||
metadata.ObjectMeta.ResourceVersion == "123"
|
||||
})).Return(expectedObject)
|
||||
|
||||
handler, err := r.Connect(ctx, "uid", nil, mockResponder)
|
||||
assert.NoError(t, err)
|
||||
handler.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
mockResponder.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -459,7 +459,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
}
|
||||
|
||||
if folderUidIdx == -1 {
|
||||
return sharedDashboards, fmt.Errorf("Error retrieving folder information")
|
||||
return sharedDashboards, fmt.Errorf("error retrieving folder information")
|
||||
}
|
||||
|
||||
// populate list of unique folder UIDs in the list of dashboards user has read permissions
|
||||
|
||||
@@ -692,9 +692,6 @@ func (m *MockClient) Update(ctx context.Context, in *resource.UpdateRequest, opt
|
||||
func (m *MockClient) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *MockClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *MockClient) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user