Add a KubernetesObject Delete admin action

This commit is contained in:
Angus Salkeld 2020-04-14 10:15:51 +10:00 коммит произвёл Jim Minter
Родитель 1fbcd2cc4c
Коммит b81135334e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0730CBDA10D1A2D3
5 изменённых файлов: 180 добавлений и 10 удалений

Просмотреть файл

@ -33,7 +33,7 @@ func (f *frontend) getAdminKubernetesObjects(w http.ResponseWriter, r *http.Requ
func (f *frontend) _getAdminKubernetesObjects(ctx context.Context, r *http.Request) ([]byte, error) {
vars := mux.Vars(r)
err := validateGetAdminKubernetesObjects(r.URL.Query())
err := validateAdminKubernetesObjects(r.URL.Query(), r.Method)
if err != nil {
return nil, err
}
@ -56,11 +56,44 @@ func (f *frontend) _getAdminKubernetesObjects(ctx context.Context, r *http.Reque
return f.kubeActions.List(ctx, doc.OpenShiftCluster, kind, namespace)
}
func (f *frontend) deleteAdminKubernetesObjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
r.URL.Path = filepath.Dir(r.URL.Path)
err := f._deleteAdminKubernetesObjects(ctx, r)
adminReply(log, w, nil, nil, err)
}
func (f *frontend) _deleteAdminKubernetesObjects(ctx context.Context, r *http.Request) error {
vars := mux.Vars(r)
err := validateAdminKubernetesObjects(r.URL.Query(), r.Method)
if err != nil {
return err
}
kind, namespace, name := r.URL.Query().Get("kind"), r.URL.Query().Get("namespace"), r.URL.Query().Get("name")
resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
doc, err := f.db.OpenShiftClusters.Get(ctx, resourceID)
switch {
case cosmosdb.IsErrorStatusCode(err, http.StatusNotFound):
return api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeResourceNotFound, "", "The Resource '%s/%s' under resource group '%s' was not found.", vars["resourceType"], vars["resourceName"], vars["resourceGroupName"])
case err != nil:
return err
}
return f.kubeActions.Delete(ctx, doc.OpenShiftCluster, kind, namespace, name)
}
// rxKubernetesString is weaker than Kubernetes validation, but strong enough to
// prevent mischief
var rxKubernetesString = regexp.MustCompile(`(?i)^[-a-z0-9]{0,255}$`)
func validateGetAdminKubernetesObjects(q url.Values) error {
func validateAdminKubernetesObjects(q url.Values, method string) error {
kind := q.Get("kind")
if kind == "" ||
!rxKubernetesString.MatchString(kind) {
@ -71,12 +104,14 @@ func validateGetAdminKubernetesObjects(q url.Values) error {
}
namespace := q.Get("namespace")
if !rxKubernetesString.MatchString(namespace) {
if (method == http.MethodDelete && namespace == "") ||
!rxKubernetesString.MatchString(namespace) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided namespace '%s' is invalid.", namespace)
}
name := q.Get("name")
if !rxKubernetesString.MatchString(name) {
if (method == http.MethodDelete && name == "") ||
!rxKubernetesString.MatchString(name) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided name '%s' is invalid.", name)
}

Просмотреть файл

@ -31,7 +31,7 @@ import (
"github.com/Azure/ARO-RP/test/util/listener"
)
func TestAdminGetKubernetesObjects(t *testing.T) {
func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
mockSubID := "00000000-0000-0000-0000-000000000000"
ctx := context.Background()
@ -69,6 +69,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
objNamespace string
objName string
mocks func(*test, *mock_database.MockOpenShiftClusters, *mock_kubeactions.MockInterface)
method string
wantStatusCode int
wantResponse func() []byte
wantError string
@ -76,6 +77,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
for _, tt := range []*test{
{
method: http.MethodGet,
name: "cluster exist in db - get",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "ConfigMap",
@ -106,6 +108,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
},
},
{
method: http.MethodGet,
name: "cluster exist in db - list",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "ConfigMap",
@ -135,6 +138,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
},
},
{
method: http.MethodGet,
name: "no kind provided",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "",
@ -146,6 +150,83 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
wantError: "400: InvalidParameter: : The provided kind '' is invalid.",
},
{
method: http.MethodGet,
name: "secret requested",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "Secret",
objNamespace: "projX",
objName: "config",
mocks: func(tt *test, openshiftClusters *mock_database.MockOpenShiftClusters, kactions *mock_kubeactions.MockInterface) {
},
wantStatusCode: http.StatusForbidden,
wantError: "403: Forbidden: : Access to secrets is forbidden.",
},
{
method: http.MethodDelete,
name: "cluster exist in db",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "ConfigMap",
objNamespace: "projX",
objName: "config",
mocks: func(tt *test, openshiftClusters *mock_database.MockOpenShiftClusters, kactions *mock_kubeactions.MockInterface) {
clusterDoc := &api.OpenShiftClusterDocument{
OpenShiftCluster: &api.OpenShiftCluster{
ID: "fakeClusterID",
Name: "resourceName",
Type: "Microsoft.RedHatOpenShift/openshiftClusters",
Properties: api.OpenShiftClusterProperties{
AROServiceKubeconfig: api.SecureBytes(""),
},
},
}
kactions.EXPECT().
Delete(gomock.Any(), clusterDoc.OpenShiftCluster, tt.objKind, tt.objNamespace, tt.objName).
Return(nil)
openshiftClusters.EXPECT().Get(gomock.Any(), strings.ToLower(tt.resourceID)).
Return(clusterDoc, nil)
},
wantStatusCode: http.StatusOK,
},
{
method: http.MethodDelete,
name: "no kind provided",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "",
objNamespace: "projX",
objName: "config",
mocks: func(tt *test, openshiftClusters *mock_database.MockOpenShiftClusters, kactions *mock_kubeactions.MockInterface) {
},
wantStatusCode: http.StatusBadRequest,
wantError: "400: InvalidParameter: : The provided kind '' is invalid.",
},
{
method: http.MethodDelete,
name: "no name provided",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "this",
objNamespace: "projX",
objName: "",
mocks: func(tt *test, openshiftClusters *mock_database.MockOpenShiftClusters, kactions *mock_kubeactions.MockInterface) {
},
wantStatusCode: http.StatusBadRequest,
wantError: "400: InvalidParameter: : The provided name '' is invalid.",
},
{
method: http.MethodDelete,
name: "no namespace provided",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "this",
objNamespace: "",
objName: "config",
mocks: func(tt *test, openshiftClusters *mock_database.MockOpenShiftClusters, kactions *mock_kubeactions.MockInterface) {
},
wantStatusCode: http.StatusBadRequest,
wantError: "400: InvalidParameter: : The provided namespace '' is invalid.",
},
{
method: http.MethodDelete,
name: "secret requested",
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
objKind: "Secret",
@ -157,7 +238,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
wantError: "403: Forbidden: : Access to secrets is forbidden.",
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Run(fmt.Sprintf("%s: %s", tt.method, tt.name), func(t *testing.T) {
defer cli.CloseIdleConnections()
l := listener.NewListener()
@ -186,8 +267,8 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
}
go f.Run(ctx, nil, nil)
url := fmt.Sprintf("https://server/admin/%s/kubernetesObjects?kind=%s&namespace=%s&name=%s", tt.resourceID, tt.objKind, tt.objNamespace, tt.objName)
req, err := http.NewRequest(http.MethodGet, url, nil)
url := fmt.Sprintf("https://server/admin%s/kubernetesObjects?kind=%s&namespace=%s&name=%s", tt.resourceID, tt.objKind, tt.objNamespace, tt.objName)
req, err := http.NewRequest(tt.method, url, nil)
if err != nil {
t.Fatal(err)
}
@ -227,7 +308,7 @@ func TestAdminGetKubernetesObjects(t *testing.T) {
}
}
func TestValidateGetAdminKubernetesObjects(t *testing.T) {
func TestValidateAdminKubernetesObjects(t *testing.T) {
valid := func() url.Values {
return url.Values{
"kind": []string{"Valid-kind"},
@ -241,6 +322,7 @@ func TestValidateGetAdminKubernetesObjects(t *testing.T) {
name string
modify func(url.Values)
wantErr string
method string
}{
{
name: "valid",
@ -270,14 +352,29 @@ func TestValidateGetAdminKubernetesObjects(t *testing.T) {
modify: func(q url.Values) { q.Set("name", longName) },
wantErr: "400: InvalidParameter: : The provided name '" + longName + "' is invalid.",
},
{
name: "delete: empty name",
modify: func(q url.Values) { delete(q, "name") },
method: http.MethodDelete,
wantErr: "400: InvalidParameter: : The provided name '' is invalid.",
},
{
name: "delete: empty namespace",
modify: func(q url.Values) { delete(q, "namespace") },
method: http.MethodDelete,
wantErr: "400: InvalidParameter: : The provided namespace '' is invalid.",
},
} {
t.Run(tt.name, func(t *testing.T) {
q := valid()
if tt.modify != nil {
tt.modify(q)
}
if tt.method == "" {
tt.method = http.MethodGet
}
err := validateGetAdminKubernetesObjects(q)
err := validateAdminKubernetesObjects(q, tt.method)
if err != nil && err.Error() != tt.wantErr ||
err == nil && tt.wantErr != "" {
t.Error(err)

Просмотреть файл

@ -185,6 +185,7 @@ func (f *frontend) authenticatedRoutes(r *mux.Router) {
s.Methods(http.MethodGet).HandlerFunc(f.getAdminKubernetesObjects).Name("getAdminKubernetesObjects")
s.Methods(http.MethodPost).HandlerFunc(f.postAdminKubernetesObjects).Name("postAdminKubernetesObjects")
s.Methods(http.MethodDelete).HandlerFunc(f.deleteAdminKubernetesObjects).Name("deleteAdminKubernetesObjects")
s = r.
Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/upgrade").

Просмотреть файл

@ -32,6 +32,7 @@ type Interface interface {
Get(ctx context.Context, oc *api.OpenShiftCluster, kind, namespace, name string) ([]byte, error)
List(ctx context.Context, oc *api.OpenShiftCluster, kind, namespace string) ([]byte, error)
CreateOrUpdate(ctx context.Context, oc *api.OpenShiftCluster, body []byte) error
Delete(ctx context.Context, oc *api.OpenShiftCluster, kind, namespace, name string) error
ClusterUpgrade(ctx context.Context, oc *api.OpenShiftCluster) error
MustGather(ctx context.Context, oc *api.OpenShiftCluster, w io.Writer) error
}
@ -218,6 +219,28 @@ func (ka *kubeactions) CreateOrUpdate(ctx context.Context, oc *api.OpenShiftClus
return ka.createOrUpdateOne(ctx, dyn, grs, obj)
}
func (ka *kubeactions) Delete(ctx context.Context, oc *api.OpenShiftCluster, kind, namespace, name string) error {
dyn, grs, err := ka.getClient(oc)
if err != nil {
return err
}
gvrs := ka.findGVR(grs, kind)
if len(gvrs) == 0 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The kind '%s' was not found.", kind)
}
if len(gvrs) > 1 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The kind '%s' matched multiple GroupKinds.", kind)
}
gvr := gvrs[0]
// TODO log the deletion
return dyn.Resource(*gvr).Namespace(namespace).Delete(name, &metav1.DeleteOptions{})
}
// ClusterUpgrade posts the new version and image to the cluster-version-operator
// which will effect the upgrade.
func (ka *kubeactions) ClusterUpgrade(ctx context.Context, oc *api.OpenShiftCluster) error {

Просмотреть файл

@ -65,6 +65,20 @@ func (mr *MockInterfaceMockRecorder) CreateOrUpdate(arg0, arg1, arg2 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).CreateOrUpdate), arg0, arg1, arg2)
}
// Delete mocks base method
func (m *MockInterface) Delete(arg0 context.Context, arg1 *api.OpenShiftCluster, arg2, arg3, arg4 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockInterfaceMockRecorder) Delete(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockInterface)(nil).Delete), arg0, arg1, arg2, arg3, arg4)
}
// Get mocks base method
func (m *MockInterface) Get(arg0 context.Context, arg1 *api.OpenShiftCluster, arg2, arg3, arg4 string) ([]byte, error) {
m.ctrl.T.Helper()