Merge pull request #2109 from CloudFitSoftware/approve_csr_admin_endpoint

Add Approve CSR Admin Endpoint for New Geneva Action
This commit is contained in:
Ben Vesel 2022-09-16 11:34:18 -04:00 коммит произвёл GitHub
Родитель 7480d45142 71d6a37c45
Коммит 910607350a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 414 добавлений и 0 удалений

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

@ -0,0 +1,61 @@
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)
csrName := r.URL.Query().Get("csrName")
if 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 csrName != "" {
return k.ApproveCsr(ctx, csrName)
}
return k.ApproveAllCsrs(ctx)
}

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

@ -0,0 +1,117 @@
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
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().
ApproveCsr(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),
mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
k.EXPECT().
ApproveAllCsrs(gomock.Any()).
Return(nil)
},
wantStatusCode: http.StatusOK,
},
} {
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.clusterManagerDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, nil, 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", tt.resourceID, tt.csrName),
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,77 @@
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) ApproveCsr(ctx context.Context, csrName string) error {
csr, err := k.kubecli.CertificatesV1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceNotFound, "", "certificate signing request '%s' was not found.", csrName)
}
return err
}
return k.updateCsr(ctx, csr)
}
func (k *kubeActions) ApproveAllCsrs(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.updateCsr(ctx, &csr)
if err != nil {
return err
}
}
return nil
}
func (k *kubeActions) updateCsr(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
}

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

@ -31,6 +31,8 @@ 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
ApproveCsr(ctx context.Context, csrName string) error
ApproveAllCsrs(ctx context.Context) 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").

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

@ -38,6 +38,34 @@ func (m *MockKubeActions) EXPECT() *MockKubeActionsMockRecorder {
return m.recorder
}
// ApproveAllCsrs mocks base method.
func (m *MockKubeActions) ApproveAllCsrs(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApproveAllCsrs", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ApproveAllCsrs indicates an expected call of ApproveAllCsrs.
func (mr *MockKubeActionsMockRecorder) ApproveAllCsrs(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApproveAllCsrs", reflect.TypeOf((*MockKubeActions)(nil).ApproveAllCsrs), arg0)
}
// ApproveCsr mocks base method.
func (m *MockKubeActions) ApproveCsr(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApproveCsr", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ApproveCsr indicates an expected call of ApproveCsr.
func (mr *MockKubeActionsMockRecorder) ApproveCsr(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApproveCsr", reflect.TypeOf((*MockKubeActions)(nil).ApproveCsr), arg0, arg1)
}
// CordonNode mocks base method.
func (m *MockKubeActions) CordonNode(arg0 context.Context, arg1 string, arg2 bool) error {
m.ctrl.T.Helper()

123
test/e2e/adminapi_csr.go Normal file
Просмотреть файл

@ -0,0 +1,123 @@
package e2e
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"encoding/base64"
"net/http"
"net/url"
"strconv"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ = Describe("[Admin API] CertificateSigningRequest action", func() {
BeforeEach(skipIfNotInDevelopmentEnv)
const prefix = "e2e-test-csr"
const namespace = "openshift"
const csrCount = 4
const csrdataStr = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ3BEQ0NBWXdDQVFBd1h6RUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdNQWtOUE1ROHdEUVlEVlFRSApEQVpFWlc1MlpYSXhFakFRQmdOVkJBb01DVTFwWTNKdmMyOW1kREVNTUFvR0ExVUVDd3dEUVZKUE1SQXdEZ1lEClZRUUREQWRsTW1VdVlYSnZNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQWxqWUUKcnFkU0hvV1p2MVdHSnN1RFZsaGExVU1BVnJiUk0xWjJHaWZJMzNETlBUWGFmQnN1QVI2ZGVCQVgyWmpybUozNQpuekNBZ0k5d1ltdlYwN3JtTEFYQlloRnJiTWtNN1pSU1ZFT01hL2ZXdlN5ZjJVQWxSdm5Jd0JmRkgwS1pRSGg5Cm5aV3RIZHQxSzRuZ3ZnM1NuQ3JEU0NBRUhsS2hoN3Jua1pyRkdrMldabFFoVklWUXFReFFzdmx3VStvWlhnNjQKdmpleDRuc3BZaXFXMERzakl6RzFsSEszWHczN3RGeWhNNzJ4SjByblBYVTRGWkJsWXUzWkVqOFVhSFBoTlcrdgpqZmg2c0hCbWFkcHpEMWRuNDJ4eXgrUGhOaCtKWTVVT3ZWWnR2MWx5UU44eEswL0VjK0Mvcm1mOWZPYmdFSkNVCm00Z3pFSXhhVGhCVURsN1JHd0lEQVFBQm9BQXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQnYvVHdUR0JvL20KcVJVK0djZ3Bsa3I1aDlKQVdSZjNNazV3Z1o0ZmlSZm85UEVaYUxJWkZYQ0V0elNHV3JZenFjbFpZQ3JuRmUySQpzdHdNUU8yb1pQUzNvcUVIcWs5Uk0rbzRUVmtkSldjY3hKV3RMY3JoTWRwVjVMc3VMam1qRS9jeDcrbEtUZkh1Cno0eDllYzJTajhnZmV3SFowZTkzZjFTT3ZhVGFMaTQrT3JkM3FTT0NyNE5ZSGhvVDJiM0pBUFpMSmkvVEFpb1gKOUxJNFJpVXNSSWlMUm45VDZidzczM0FLMkpNMXREWU9Tc0hXdmJrZ3FDOFlHMmpYUW9LNUpZOWdTN0V5TkF6NwpjT1plbkkwK2dVeE1leUlNN2I0S05YWFQ3NmxVdHZ5M2N3LzhwVmxQU01pTDFVZ2RpMXFZMDl0MW9FMmU4YnljCm5GdWhZOW5ERU53PQotLS0tLUVORCBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K"
It("should be able to approve one or multiple CSRs", func() {
csrDataEncoded := []byte(csrdataStr)
csrDataDecoded := make([]byte, base64.StdEncoding.DecodedLen(len(csrDataEncoded)))
csrDataLength, err := base64.StdEncoding.Decode(csrDataDecoded, csrDataEncoded)
Expect(err).NotTo(HaveOccurred())
csrData := csrDataDecoded[:csrDataLength]
By("creating mock CSRs via Kubernetes API")
for i := 0; i < csrCount; i++ {
csr := mockCSR(prefix+strconv.Itoa(i), namespace, csrData)
_, err := clients.Kubernetes.CertificatesV1().CertificateSigningRequests().Create(context.Background(), csr, metav1.CreateOptions{})
Expect(err).NotTo(HaveOccurred())
}
defer func() {
By("deleting the mock CSRs via Kubernetes API")
for i := 0; i < csrCount; i++ {
err := clients.Kubernetes.CertificatesV1().CertificateSigningRequests().Delete(context.Background(), prefix+strconv.Itoa(i), metav1.DeleteOptions{})
Expect(err).NotTo(HaveOccurred())
}
}()
testCSRApproveOK(prefix+"0", namespace)
testCSRMassApproveOK(prefix, namespace, csrCount)
})
})
func testCSRApproveOK(objName, namespace string) {
By("approving the CSR via RP admin API")
params := url.Values{
"csrName": []string{objName},
}
resp, err := adminRequest(context.Background(), http.MethodPost, "/admin"+resourceIDFromEnv()+"/approvecsr", params, nil, nil)
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
By("checking that the CSR was approved via Kubernetes API")
testcsr, err := clients.Kubernetes.CertificatesV1().CertificateSigningRequests().Get(context.Background(), objName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
approved := false
for _, condition := range testcsr.Status.Conditions {
if condition.Type == certificatesv1.CertificateApproved {
Expect(condition.Status).To(Equal(corev1.ConditionTrue))
Expect(condition.Reason).To(Equal("AROSupportApprove"))
Expect(condition.Message).To(Equal("This CSR was approved by ARO support personnel."))
approved = true
}
}
Expect(approved).Should(BeTrue())
}
func testCSRMassApproveOK(namePrefix, namespace string, csrCount int) {
By("approving all CSRs via RP admin API")
resp, err := adminRequest(context.Background(), http.MethodPost, "/admin"+resourceIDFromEnv()+"/approvecsr", nil, nil, nil)
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusOK))
By("checking that all CSRs were approved via Kubernetes API")
for i := 1; i < csrCount; i++ {
testcsr, err := clients.Kubernetes.CertificatesV1().CertificateSigningRequests().Get(context.Background(), namePrefix+strconv.Itoa(i), metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
approved := false
for _, condition := range testcsr.Status.Conditions {
if condition.Type == certificatesv1.CertificateApproved {
Expect(condition.Status).To(Equal(corev1.ConditionTrue))
Expect(condition.Reason).To(Equal("AROSupportApprove"))
Expect(condition.Message).To(Equal("This CSR was approved by ARO support personnel."))
approved = true
}
}
Expect(approved).Should(BeTrue())
}
}
func mockCSR(objName, namespace string, csrData []byte) *certificatesv1.CertificateSigningRequest {
csr := &certificatesv1.CertificateSigningRequest{
// Username, UID, Groups will be injected by API server.
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
ObjectMeta: metav1.ObjectMeta{
Name: objName,
Namespace: namespace,
},
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: csrData,
Usages: []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
SignerName: "kubernetes.io/kube-apiserver-client",
},
}
return csr
}