зеркало из https://github.com/Azure/ARO-RP.git
feat: added admin endpoint for approving csrs
This commit is contained in:
Родитель
7480d45142
Коммит
ff5fbc91eb
|
@ -0,0 +1,62 @@
|
|||
package frontend
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
"github.com/Azure/ARO-RP/pkg/frontend/middleware"
|
||||
)
|
||||
|
||||
func (f *frontend) postAdminOpenShiftClusterApproveCSR(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._postAdminOpenShiftClusterApproveCSR(ctx, r, log)
|
||||
|
||||
adminReply(log, w, nil, nil, err)
|
||||
}
|
||||
|
||||
func (f *frontend) _postAdminOpenShiftClusterApproveCSR(ctx context.Context, r *http.Request, log *logrus.Entry) error {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
approveAll := strings.EqualFold(r.URL.Query().Get("approveAll"), "true")
|
||||
csrName := ""
|
||||
if !approveAll {
|
||||
csrName = r.URL.Query().Get("csrName")
|
||||
err := validateAdminKubernetesObjects(r.Method, "CertificateSigningRequest", "", csrName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
|
||||
|
||||
doc, err := f.dbOpenShiftClusters.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
|
||||
}
|
||||
|
||||
k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if approveAll {
|
||||
return k.RunCertificateMassApprove(ctx)
|
||||
}
|
||||
return k.RunCertificateApprove(ctx, csrName)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package frontend
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/env"
|
||||
"github.com/Azure/ARO-RP/pkg/frontend/adminactions"
|
||||
"github.com/Azure/ARO-RP/pkg/metrics/noop"
|
||||
mock_adminactions "github.com/Azure/ARO-RP/pkg/util/mocks/adminactions"
|
||||
)
|
||||
|
||||
func TestAdminApproveCSR(t *testing.T) {
|
||||
mockSubID := "00000000-0000-0000-0000-000000000000"
|
||||
mockTenantID := "00000000-0000-0000-0000-000000000000"
|
||||
ctx := context.Background()
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
resourceID string
|
||||
csrName string
|
||||
approveAll string
|
||||
mocks func(*test, *mock_adminactions.MockKubeActions)
|
||||
method string
|
||||
wantStatusCode int
|
||||
wantResponse []byte
|
||||
wantError string
|
||||
}
|
||||
|
||||
for _, tt := range []*test{
|
||||
{
|
||||
method: http.MethodPost,
|
||||
name: "single csr",
|
||||
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
|
||||
csrName: "aro-csr",
|
||||
mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
|
||||
k.EXPECT().
|
||||
RunCertificateApprove(gomock.Any(), tt.csrName).
|
||||
Return(nil)
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
method: http.MethodPost,
|
||||
name: "all csrs",
|
||||
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
|
||||
csrName: "aro-csr",
|
||||
approveAll: "true",
|
||||
mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
|
||||
k.EXPECT().
|
||||
RunCertificateMassApprove(gomock.Any()).
|
||||
Return(nil)
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
method: http.MethodPost,
|
||||
name: "invalid csr name",
|
||||
resourceID: fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
|
||||
csrName: "",
|
||||
approveAll: "false",
|
||||
mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
|
||||
},
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "400: InvalidParameter: : The provided name '' is invalid.",
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s: %s", tt.method, tt.name), func(t *testing.T) {
|
||||
ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
|
||||
defer ti.done()
|
||||
|
||||
k := mock_adminactions.NewMockKubeActions(ti.controller)
|
||||
tt.mocks(tt, k)
|
||||
|
||||
ti.fixture.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
|
||||
Key: strings.ToLower(tt.resourceID),
|
||||
OpenShiftCluster: &api.OpenShiftCluster{
|
||||
ID: tt.resourceID,
|
||||
Name: "resourceName",
|
||||
Type: "Microsoft.RedHatOpenShift/openshiftClusters",
|
||||
},
|
||||
})
|
||||
ti.fixture.AddSubscriptionDocuments(&api.SubscriptionDocument{
|
||||
ID: mockSubID,
|
||||
Subscription: &api.Subscription{
|
||||
State: api.SubscriptionStateRegistered,
|
||||
Properties: &api.SubscriptionProperties{
|
||||
TenantID: mockTenantID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := ti.buildFixtures(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := NewFrontend(ctx, ti.audit, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster) (adminactions.KubeActions, error) {
|
||||
return k, nil
|
||||
}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go f.Run(ctx, nil, nil)
|
||||
|
||||
resp, b, err := ti.request(tt.method,
|
||||
fmt.Sprintf("https://server/admin%s/approvecsr?csrName=%s&approveAll=%s", tt.resourceID, tt.csrName, tt.approveAll),
|
||||
nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = validateResponse(resp, b, tt.wantStatusCode, tt.wantError, tt.wantResponse)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package adminactions
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
)
|
||||
|
||||
func (k *kubeActions) RunCertificateApprove(ctx context.Context, csrName string) error {
|
||||
csr, err := k.kubecli.CertificatesV1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
|
||||
if kerrors.IsNotFound(err) {
|
||||
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceNotFound, "", "certificate signing request '%s' was not found.", csrName)
|
||||
}
|
||||
|
||||
return k.RunCertificateApprovalUpdate(ctx, csr)
|
||||
}
|
||||
|
||||
func (k *kubeActions) RunCertificateMassApprove(ctx context.Context) error {
|
||||
csrs, err := k.kubecli.CertificatesV1().CertificateSigningRequests().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, csr := range csrs.Items {
|
||||
err = k.RunCertificateApprovalUpdate(ctx, &csr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *kubeActions) RunCertificateApprovalUpdate(ctx context.Context, csr *certificatesv1.CertificateSigningRequest) error {
|
||||
modifiedCSR, hasCondition, err := addConditionIfNeeded(csr, string(certificatesv1.CertificateDenied), string(certificatesv1.CertificateApproved), "AROSupportApprove", "This CSR was approved by ARO support personnel.")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasCondition {
|
||||
_, err = k.kubecli.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, modifiedCSR.Name, modifiedCSR, metav1.UpdateOptions{})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func addConditionIfNeeded(csr *certificatesv1.CertificateSigningRequest, mustNotHaveConditionType, conditionType, reason, message string) (*certificatesv1.CertificateSigningRequest, bool, error) {
|
||||
var alreadyHasCondition bool
|
||||
for _, c := range csr.Status.Conditions {
|
||||
if string(c.Type) == mustNotHaveConditionType {
|
||||
return nil, false, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodePropertyChangeNotAllowed, "", "certificate signing request %q is already %s", csr.Name, c.Type)
|
||||
}
|
||||
if string(c.Type) == conditionType {
|
||||
alreadyHasCondition = true
|
||||
}
|
||||
}
|
||||
if alreadyHasCondition {
|
||||
return csr, true, nil
|
||||
}
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1.RequestConditionType(conditionType),
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
LastUpdateTime: metav1.Now(),
|
||||
})
|
||||
return csr, false, nil
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/Azure/go-autorest/autorest/to"
|
||||
configclient "github.com/openshift/client-go/config/clientset/versioned"
|
||||
"github.com/sirupsen/logrus"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -31,6 +32,9 @@ type KubeActions interface {
|
|||
KubeDelete(ctx context.Context, groupKind, namespace, name string, force bool) error
|
||||
CordonNode(ctx context.Context, nodeName string, unschedulable bool) error
|
||||
DrainNode(ctx context.Context, nodeName string) error
|
||||
RunCertificateApprove(ctx context.Context, csrName string) error
|
||||
RunCertificateMassApprove(ctx context.Context) error
|
||||
RunCertificateApprovalUpdate(ctx context.Context, csr *certificatesv1.CertificateSigningRequest) error
|
||||
Upgrade(ctx context.Context, upgradeY bool) error
|
||||
KubeGetPodLogs(ctx context.Context, namespace, name, containerName string) ([]byte, error)
|
||||
}
|
||||
|
|
|
@ -256,6 +256,12 @@ func (f *frontend) authenticatedRoutes(r *mux.Router) {
|
|||
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}/approvecsr").
|
||||
Subrouter()
|
||||
|
||||
s.Methods(http.MethodPost).HandlerFunc(f.postAdminOpenShiftClusterApproveCSR).Name("postAdminOpenShiftClusterApproveCSR")
|
||||
|
||||
// Pod logs
|
||||
s = r.
|
||||
Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/kubernetespodlogs").
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
compute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
logrus "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/certificates/v1"
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
|
@ -139,6 +140,48 @@ func (mr *MockKubeActionsMockRecorder) KubeList(arg0, arg1, arg2 interface{}) *g
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeList", reflect.TypeOf((*MockKubeActions)(nil).KubeList), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// RunCertificateApprovalUpdate mocks base method.
|
||||
func (m *MockKubeActions) RunCertificateApprovalUpdate(arg0 context.Context, arg1 *v1.CertificateSigningRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RunCertificateApprovalUpdate", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RunCertificateApprovalUpdate indicates an expected call of RunCertificateApprovalUpdate.
|
||||
func (mr *MockKubeActionsMockRecorder) RunCertificateApprovalUpdate(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCertificateApprovalUpdate", reflect.TypeOf((*MockKubeActions)(nil).RunCertificateApprovalUpdate), arg0, arg1)
|
||||
}
|
||||
|
||||
// RunCertificateApprove mocks base method.
|
||||
func (m *MockKubeActions) RunCertificateApprove(arg0 context.Context, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RunCertificateApprove", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RunCertificateApprove indicates an expected call of RunCertificateApprove.
|
||||
func (mr *MockKubeActionsMockRecorder) RunCertificateApprove(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCertificateApprove", reflect.TypeOf((*MockKubeActions)(nil).RunCertificateApprove), arg0, arg1)
|
||||
}
|
||||
|
||||
// RunCertificateMassApprove mocks base method.
|
||||
func (m *MockKubeActions) RunCertificateMassApprove(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RunCertificateMassApprove", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RunCertificateMassApprove indicates an expected call of RunCertificateMassApprove.
|
||||
func (mr *MockKubeActionsMockRecorder) RunCertificateMassApprove(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCertificateMassApprove", reflect.TypeOf((*MockKubeActions)(nil).RunCertificateMassApprove), arg0)
|
||||
}
|
||||
|
||||
// Upgrade mocks base method.
|
||||
func (m *MockKubeActions) Upgrade(arg0 context.Context, arg1 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
Загрузка…
Ссылка в новой задаче