K8s: Dashboards: Add deletion validation for provisioned dashboards (#98504)
This commit is contained in:
committed by
GitHub
parent
ce512862f7
commit
5ed2a4c624
@@ -1,18 +1,26 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
|
||||
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
@@ -22,13 +30,18 @@ var (
|
||||
)
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type DashboardsAPIBuilder struct{}
|
||||
type DashboardsAPIBuilder struct {
|
||||
ProvisioningDashboardService dashboards.DashboardProvisioningService
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
provisioningDashboardService dashboards.DashboardProvisioningService,
|
||||
) *DashboardsAPIBuilder {
|
||||
builder := &DashboardsAPIBuilder{}
|
||||
builder := &DashboardsAPIBuilder{
|
||||
ProvisioningDashboardService: provisioningDashboardService,
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
@@ -63,3 +76,37 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti
|
||||
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
return oas, nil
|
||||
}
|
||||
|
||||
// Validate will prevent deletion of provisioned dashboards, unless the grace period is set to 0, indicating a force deletion
|
||||
func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
op := a.GetOperation()
|
||||
if op == admission.Delete {
|
||||
obj := a.GetOperationOptions()
|
||||
deleteOptions, ok := obj.(*metav1.DeleteOptions)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected v1.DeleteOptions")
|
||||
}
|
||||
|
||||
if deleteOptions.GracePeriodSeconds == nil || *deleteOptions.GracePeriodSeconds != 0 {
|
||||
nsInfo, err := claims.ParseNamespace(a.GetNamespace())
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "failed to parse namespace", err)
|
||||
}
|
||||
|
||||
provisioningData, err := b.ProvisioningDashboardService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
|
||||
if err != nil {
|
||||
if errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err)
|
||||
}
|
||||
|
||||
if provisioningData != nil {
|
||||
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
func TestDashboardAPIBuilder_Validate(t *testing.T) {
|
||||
oneInt64 := int64(1)
|
||||
zeroInt64 := int64(0)
|
||||
tests := []struct {
|
||||
name string
|
||||
inputObj *v0alpha1.Dashboard
|
||||
deletionOptions metav1.DeleteOptions
|
||||
dashboardResponse *dashboards.DashboardProvisioning
|
||||
dashboardErrorResponse error
|
||||
checkRan bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "should return an error if data is found",
|
||||
inputObj: &v0alpha1.Dashboard{
|
||||
Spec: common.Unstructured{},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Dashboard",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
deletionOptions: metav1.DeleteOptions{
|
||||
GracePeriodSeconds: nil,
|
||||
},
|
||||
dashboardResponse: &dashboards.DashboardProvisioning{ID: 1},
|
||||
dashboardErrorResponse: nil,
|
||||
checkRan: true,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "should return an error if unable to check",
|
||||
inputObj: &v0alpha1.Dashboard{
|
||||
Spec: common.Unstructured{},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Dashboard",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
deletionOptions: metav1.DeleteOptions{
|
||||
GracePeriodSeconds: nil,
|
||||
},
|
||||
dashboardResponse: nil,
|
||||
dashboardErrorResponse: fmt.Errorf("generic error"),
|
||||
checkRan: true,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "should be okay if error is provisioned dashboard not found",
|
||||
inputObj: &v0alpha1.Dashboard{
|
||||
Spec: common.Unstructured{},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Dashboard",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
deletionOptions: metav1.DeleteOptions{
|
||||
GracePeriodSeconds: nil,
|
||||
},
|
||||
dashboardResponse: nil,
|
||||
dashboardErrorResponse: dashboards.ErrProvisionedDashboardNotFound,
|
||||
checkRan: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Should still run the check for delete if grace period is not 0",
|
||||
inputObj: &v0alpha1.Dashboard{
|
||||
Spec: common.Unstructured{},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Dashboard",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
deletionOptions: metav1.DeleteOptions{
|
||||
GracePeriodSeconds: &oneInt64,
|
||||
},
|
||||
dashboardResponse: nil,
|
||||
dashboardErrorResponse: nil,
|
||||
checkRan: true,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "should not run the check for delete if grace period is set to 0",
|
||||
inputObj: &v0alpha1.Dashboard{
|
||||
Spec: common.Unstructured{},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Dashboard",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
deletionOptions: metav1.DeleteOptions{
|
||||
GracePeriodSeconds: &zeroInt64,
|
||||
},
|
||||
dashboardResponse: nil,
|
||||
dashboardErrorResponse: nil,
|
||||
checkRan: false,
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fakeService := &dashboards.FakeDashboardProvisioning{}
|
||||
fakeService.On("GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything).Return(tt.dashboardResponse, tt.dashboardErrorResponse).Once()
|
||||
b := &DashboardsAPIBuilder{
|
||||
ProvisioningDashboardService: fakeService,
|
||||
}
|
||||
err := b.Validate(context.Background(), admission.NewAttributesRecord(
|
||||
tt.inputObj,
|
||||
nil,
|
||||
v0alpha1.DashboardResourceInfo.GroupVersionKind(),
|
||||
"stacks-123",
|
||||
tt.inputObj.Name,
|
||||
v0alpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
"",
|
||||
admission.Operation("DELETE"),
|
||||
&tt.deletionOptions,
|
||||
true,
|
||||
&user.SignedInUser{},
|
||||
), nil)
|
||||
|
||||
if tt.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.checkRan {
|
||||
fakeService.AssertCalled(t, "GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything)
|
||||
} else {
|
||||
fakeService.AssertNotCalled(t, "GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ var (
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type DashboardsAPIBuilder struct {
|
||||
dashboard.DashboardsAPIBuilder
|
||||
dashboardService dashboards.DashboardService
|
||||
features featuremgmt.FeatureToggles
|
||||
|
||||
@@ -59,6 +60,7 @@ type DashboardsAPIBuilder struct {
|
||||
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
provisioningDashboardService dashboards.DashboardProvisioningService,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
dashStore dashboards.Store,
|
||||
@@ -72,7 +74,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
builder := &DashboardsAPIBuilder{
|
||||
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
|
||||
|
||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||
ProvisioningDashboardService: provisioningDashboardService,
|
||||
},
|
||||
dashboardService: dashboardService,
|
||||
features: features,
|
||||
accessControl: accessControl,
|
||||
|
||||
@@ -42,6 +42,7 @@ var (
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type DashboardsAPIBuilder struct {
|
||||
dashboard.DashboardsAPIBuilder
|
||||
dashboardService dashboards.DashboardService
|
||||
features featuremgmt.FeatureToggles
|
||||
|
||||
@@ -56,6 +57,7 @@ type DashboardsAPIBuilder struct {
|
||||
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
provisioningDashboardService dashboards.DashboardProvisioningService,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
dashStore dashboards.Store,
|
||||
@@ -69,7 +71,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
builder := &DashboardsAPIBuilder{
|
||||
log: log.New("grafana-apiserver.dashboards.v1alpha1"),
|
||||
|
||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||
ProvisioningDashboardService: provisioningDashboardService,
|
||||
},
|
||||
dashboardService: dashboardService,
|
||||
features: features,
|
||||
accessControl: accessControl,
|
||||
|
||||
@@ -42,6 +42,7 @@ var (
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type DashboardsAPIBuilder struct {
|
||||
dashboard.DashboardsAPIBuilder
|
||||
dashboardService dashboards.DashboardService
|
||||
features featuremgmt.FeatureToggles
|
||||
|
||||
@@ -56,6 +57,7 @@ type DashboardsAPIBuilder struct {
|
||||
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
provisioningDashboardService dashboards.DashboardProvisioningService,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
dashStore dashboards.Store,
|
||||
@@ -70,6 +72,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
builder := &DashboardsAPIBuilder{
|
||||
log: log.New("grafana-apiserver.dashboards.v2alpha1"),
|
||||
|
||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||
ProvisioningDashboardService: provisioningDashboardService,
|
||||
},
|
||||
dashboardService: dashboardService,
|
||||
features: features,
|
||||
accessControl: accessControl,
|
||||
|
||||
Reference in New Issue
Block a user