From 6866b84da8857caf7fb8f3bfd8dd33a10b2ed261 Mon Sep 17 00:00:00 2001 From: Cesar Celis Hernandez Date: Wed, 25 May 2022 16:07:56 -0400 Subject: [PATCH] To support multiple CSRs per tenant (#1997) --- .github/workflows/deploy-tenant.sh | 61 ++++---- models/csr_elements.go | 133 ++++++++++++++++++ operator-integration/tenant_test.go | 38 ++++- operatorapi/embedded_spec.go | 26 +++- ...t_certificate_signing_request_responses.go | 6 +- operatorapi/volumes.go | 77 ++++++---- portal-ui/tests/scripts/operator.sh | 2 +- .../tenant-kes-encryption/kustomization.yaml | 9 ++ .../kustomization.yaml | 0 swagger-operator.yml | 10 +- 10 files changed, 293 insertions(+), 69 deletions(-) create mode 100644 models/csr_elements.go create mode 100644 portal-ui/tests/scripts/tenant-kes-encryption/kustomization.yaml rename portal-ui/tests/scripts/{tenant => tenant-lite}/kustomization.yaml (100%) diff --git a/.github/workflows/deploy-tenant.sh b/.github/workflows/deploy-tenant.sh index 42e996ae5..dbd7c2cab 100755 --- a/.github/workflows/deploy-tenant.sh +++ b/.github/workflows/deploy-tenant.sh @@ -20,17 +20,17 @@ export SCRIPT_DIR source "${SCRIPT_DIR}/common.sh" +function install_tenants() { + echo "Installing tenants" + # Install lite & kes tenants + try kubectl apply -k "${SCRIPT_DIR}/../../portal-ui/tests/scripts/tenant-lite" + try kubectl apply -k "${SCRIPT_DIR}/../../portal-ui/tests/scripts/tenant-kes-encryption" -function install_tenant() { - echo "Installing lite tenant" - - try kubectl apply -k "${SCRIPT_DIR}/../../portal-ui/tests/scripts/tenant" - - echo "Waiting for the tenant statefulset, this indicates the tenant is being fulfilled" - waitdone=0 - totalwait=0 - while true; do + echo "Waiting for the tenant statefulset, this indicates the tenant is being fulfilled" + waitdone=0 + totalwait=0 + while true; do waitdone=$(kubectl -n tenant-lite get pods -l v1.min.io/tenant=storage-lite --no-headers | wc -l) if [ "$waitdone" -ne 0 ]; then echo "Found $waitdone pods" @@ -39,41 +39,34 @@ function install_tenant() { sleep 5 totalwait=$((totalwait + 5)) if [ "$totalwait" -gt 300 ]; then - echo "Tenant never created statefulset after 5 minutes" - try false + echo "Tenant never created statefulset after 5 minutes" + try false fi - done + done - echo "Waiting for tenant pods to come online (5m timeout)" - try kubectl wait --namespace tenant-lite \ + echo "Waiting for tenant pods to come online (5m timeout)" + try kubectl wait --namespace tenant-lite \ --for=condition=ready pod \ --selector="v1.min.io/tenant=storage-lite" \ --timeout=300s - echo "Build passes basic tenant creation" + echo "Build passes basic tenant creation" } function main() { - destroy_kind - - setup_kind - - install_operator - - install_tenant - - check_tenant_status tenant-lite storage-lite - - kubectl proxy & - - # Beginning Kubernetes 1.24 ----> Service Account Token Secrets are not - # automatically generated, to generate them manually, users must manually - # create the secret, for our examples where we lead people to get the JWT - # from the console-sa service account, they additionally need to manually - # generate the secret via - kubectl apply -f "${SCRIPT_DIR}/console-sa-secret.yaml" - + destroy_kind + setup_kind + install_operator + install_tenants + check_tenant_status tenant-lite storage-lite + kubectl proxy & + # Beginning Kubernetes 1.24 ----> Service Account Token Secrets are not + # automatically generated, to generate them manually, users must manually + # create the secret, for our examples where we lead people to get the JWT + # from the console-sa service account, they additionally need to manually + # generate the secret via + kubectl apply -f "${SCRIPT_DIR}/console-sa-secret.yaml" } main "$@" diff --git a/models/csr_elements.go b/models/csr_elements.go new file mode 100644 index 000000000..23cb951de --- /dev/null +++ b/models/csr_elements.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// CsrElements csr elements +// +// swagger:model csrElements +type CsrElements struct { + + // csr element + CsrElement []*CsrElement `json:"csrElement"` +} + +// Validate validates this csr elements +func (m *CsrElements) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCsrElement(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CsrElements) validateCsrElement(formats strfmt.Registry) error { + if swag.IsZero(m.CsrElement) { // not required + return nil + } + + for i := 0; i < len(m.CsrElement); i++ { + if swag.IsZero(m.CsrElement[i]) { // not required + continue + } + + if m.CsrElement[i] != nil { + if err := m.CsrElement[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("csrElement" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("csrElement" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this csr elements based on the context it is used +func (m *CsrElements) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCsrElement(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CsrElements) contextValidateCsrElement(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.CsrElement); i++ { + + if m.CsrElement[i] != nil { + if err := m.CsrElement[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("csrElement" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("csrElement" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *CsrElements) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CsrElements) UnmarshalBinary(b []byte) error { + var res CsrElements + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/operator-integration/tenant_test.go b/operator-integration/tenant_test.go index c95828c7f..668ddfa1e 100644 --- a/operator-integration/tenant_test.go +++ b/operator-integration/tenant_test.go @@ -260,7 +260,7 @@ func TestListTenants(t *testing.T) { } TenantName := &result.Tenants[0].Name // The array has to be empty, no index accessible fmt.Println(*TenantName) - assert.Equal("storage-lite", *TenantName, *TenantName) + assert.Equal("storage-kms-encrypted", *TenantName, *TenantName) printEndFunc("TestListTenants") } @@ -637,5 +637,39 @@ func TestGetCSR(t *testing.T) { assert.Equal( 200, resp.StatusCode, finalResponse) } - assert.Equal(strings.Contains(finalResponse, "Automatically approved by MinIO Operator"), true) + assert.Equal(strings.Contains(finalResponse, "Automatically approved by MinIO Operator"), true, finalResponse) +} + +func TestGetMultipleCSRs(t *testing.T) { + /* + We can have multiple CSRs per tenant, the idea is to support them in our API and test them here, making sure we + can retrieve them all, as an example I found this tenant: + storage-kms-encrypted -client -tenant-kms-encrypted-csr + storage-kms-encrypted -kes -tenant-kms-encrypted-csr + storage-kms-encrypted -tenant-kms-encrypted-csr + Notice the nomenclature of it: + -<*>--csr + where * is anything either nothing or something, anything. + */ + assert := assert.New(t) + namespace := "tenant-kms-encrypted" + tenant := "storage-kms-encrypted" + resp, err := GetCSR(namespace, tenant) + assert.Nil(err) + if err != nil { + log.Println(err) + return + } + finalResponse := inspectHTTPResponse(resp) + if resp != nil { + assert.Equal( + 200, resp.StatusCode, finalResponse) + } + var expectedMessages [3]string + expectedMessages[0] = "storage-kms-encrypted-tenant-kms-encrypted-csr" + expectedMessages[1] = "storage-kms-encrypted-kes-tenant-kms-encrypted-csr" + expectedMessages[2] = "Automatically approved by MinIO Operator" + for _, element := range expectedMessages { + assert.Equal(strings.Contains(finalResponse, element), true) + } } diff --git a/operatorapi/embedded_spec.go b/operatorapi/embedded_spec.go index 69109e86d..df07fa4f7 100644 --- a/operatorapi/embedded_spec.go +++ b/operatorapi/embedded_spec.go @@ -608,7 +608,7 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/csrElement" + "$ref": "#/definitions/csrElements" } }, "default": { @@ -2387,6 +2387,17 @@ func init() { } } }, + "csrElements": { + "type": "object", + "properties": { + "csrElement": { + "type": "array", + "items": { + "$ref": "#/definitions/csrElement" + } + } + } + }, "deleteTenantRequest": { "type": "object", "properties": { @@ -5201,7 +5212,7 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/csrElement" + "$ref": "#/definitions/csrElements" } }, "default": { @@ -7823,6 +7834,17 @@ func init() { } } }, + "csrElements": { + "type": "object", + "properties": { + "csrElement": { + "type": "array", + "items": { + "$ref": "#/definitions/csrElement" + } + } + } + }, "deleteTenantRequest": { "type": "object", "properties": { diff --git a/operatorapi/operations/operator_api/list_tenant_certificate_signing_request_responses.go b/operatorapi/operations/operator_api/list_tenant_certificate_signing_request_responses.go index 50a24ce69..c9fe104e7 100644 --- a/operatorapi/operations/operator_api/list_tenant_certificate_signing_request_responses.go +++ b/operatorapi/operations/operator_api/list_tenant_certificate_signing_request_responses.go @@ -42,7 +42,7 @@ type ListTenantCertificateSigningRequestOK struct { /* In: Body */ - Payload *models.CsrElement `json:"body,omitempty"` + Payload *models.CsrElements `json:"body,omitempty"` } // NewListTenantCertificateSigningRequestOK creates ListTenantCertificateSigningRequestOK with default headers values @@ -52,13 +52,13 @@ func NewListTenantCertificateSigningRequestOK() *ListTenantCertificateSigningReq } // WithPayload adds the payload to the list tenant certificate signing request o k response -func (o *ListTenantCertificateSigningRequestOK) WithPayload(payload *models.CsrElement) *ListTenantCertificateSigningRequestOK { +func (o *ListTenantCertificateSigningRequestOK) WithPayload(payload *models.CsrElements) *ListTenantCertificateSigningRequestOK { o.Payload = payload return o } // SetPayload sets the payload to the list tenant certificate signing request o k response -func (o *ListTenantCertificateSigningRequestOK) SetPayload(payload *models.CsrElement) { +func (o *ListTenantCertificateSigningRequestOK) SetPayload(payload *models.CsrElements) { o.Payload = payload } diff --git a/operatorapi/volumes.go b/operatorapi/volumes.go index 408738e92..05aed469e 100644 --- a/operatorapi/volumes.go +++ b/operatorapi/volumes.go @@ -30,6 +30,7 @@ import ( "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations" "github.com/minio/console/operatorapi/operations/operator_api" + v1 "k8s.io/api/certificates/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -230,43 +231,67 @@ func getPVCEventsResponse(session *models.Principal, params operator_api.GetPVCE return retval, nil } -func getTenantCSResponse(session *models.Principal, params operator_api.ListTenantCertificateSigningRequestParams) (*models.CsrElement, *models.Error) { +func getTenantCSResponse(session *models.Principal, params operator_api.ListTenantCertificateSigningRequestParams) (*models.CsrElements, *models.Error) { ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() clientset, err := cluster.K8sClient(session.STSSessionToken) if err != nil { return nil, errors.ErrorWithContext(ctx, err) } - csrName := params.Tenant + "-" + params.Namespace + "-csr" - csrResult, csrError := clientset.CertificatesV1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{}) - if csrError != nil { - return nil, errors.ErrorWithContext(ctx, err) + + // Get CSRs by Label "v1.min.io/tenant=" + params.Tenant + listByTenantLabel := metav1.ListOptions{LabelSelector: "v1.min.io/tenant=" + params.Tenant} + listResult, listError := clientset.CertificatesV1().CertificateSigningRequests().List(ctx, listByTenantLabel) + if listError != nil { + return nil, errors.ErrorWithContext(ctx, listError) } - annotations := []*models.Annotation{} - for k, v := range csrResult.ObjectMeta.Annotations { - annotations = append(annotations, &models.Annotation{Key: k, Value: v}) + + // Get CSR by label "v1.min.io/kes=" + params.Tenant + "-kes" + listByKESLabel := metav1.ListOptions{LabelSelector: "v1.min.io/kes=" + params.Tenant + "-kes"} + listKESResult, listKESError := clientset.CertificatesV1().CertificateSigningRequests().List(ctx, listByKESLabel) + if listKESError != nil { + return nil, errors.ErrorWithContext(ctx, listKESError) } - var DeletionGracePeriodSeconds int64 - DeletionGracePeriodSeconds = 0 - if csrResult.ObjectMeta.DeletionGracePeriodSeconds != nil { - DeletionGracePeriodSeconds = *csrResult.ObjectMeta.DeletionGracePeriodSeconds + + var listOfCSRs []v1.CertificateSigningRequest + for index := 0; index < len(listResult.Items); index++ { + listOfCSRs = append(listOfCSRs, listResult.Items[index]) } - messages := "" - // A CSR.Status can contain multiple Conditions - for i := 0; i < len(csrResult.Status.Conditions); i++ { - messages = messages + " " + csrResult.Status.Conditions[i].Message + for index := 0; index < len(listKESResult.Items); index++ { + listOfCSRs = append(listOfCSRs, listKESResult.Items[index]) } - retval := &models.CsrElement{ - Name: csrResult.ObjectMeta.Name, - Annotations: annotations, - DeletionGracePeriodSeconds: DeletionGracePeriodSeconds, - GenerateName: csrResult.ObjectMeta.GenerateName, - Generation: csrResult.ObjectMeta.Generation, - Namespace: csrResult.ObjectMeta.Namespace, - ResourceVersion: csrResult.ObjectMeta.ResourceVersion, - Status: messages, + + var arrayElements []*models.CsrElement + for index := 0; index < len(listOfCSRs); index++ { + csrResult := listOfCSRs[index] + annotations := []*models.Annotation{} + for k, v := range csrResult.ObjectMeta.Annotations { + annotations = append(annotations, &models.Annotation{Key: k, Value: v}) + } + var DeletionGracePeriodSeconds int64 + DeletionGracePeriodSeconds = 0 + if csrResult.ObjectMeta.DeletionGracePeriodSeconds != nil { + DeletionGracePeriodSeconds = *csrResult.ObjectMeta.DeletionGracePeriodSeconds + } + messages := "" + // A CSR.Status can contain multiple Conditions + for i := 0; i < len(csrResult.Status.Conditions); i++ { + messages = messages + " " + csrResult.Status.Conditions[i].Message + } + retval := &models.CsrElement{ + Name: csrResult.ObjectMeta.Name, + Annotations: annotations, + DeletionGracePeriodSeconds: DeletionGracePeriodSeconds, + GenerateName: csrResult.ObjectMeta.GenerateName, + Generation: csrResult.ObjectMeta.Generation, + Namespace: csrResult.ObjectMeta.Namespace, + ResourceVersion: csrResult.ObjectMeta.ResourceVersion, + Status: messages, + } + arrayElements = append(arrayElements, retval) } - return retval, nil + result := &models.CsrElements{CsrElement: arrayElements} + return result, nil } func getPVCDescribeResponse(session *models.Principal, params operator_api.GetPVCDescribeParams) (*models.DescribePVCWrapper, *models.Error) { diff --git a/portal-ui/tests/scripts/operator.sh b/portal-ui/tests/scripts/operator.sh index 6459bf8de..586bc5a31 100755 --- a/portal-ui/tests/scripts/operator.sh +++ b/portal-ui/tests/scripts/operator.sh @@ -151,7 +151,7 @@ function install_tenant() { value=storage-lite echo "Installing lite tenant" - try kubectl apply -k "${SCRIPT_DIR}/tenant" + try kubectl apply -k "${SCRIPT_DIR}/tenant-lite" echo "Waiting for the tenant statefulset, this indicates the tenant is being fulfilled" echo $namespace diff --git a/portal-ui/tests/scripts/tenant-kes-encryption/kustomization.yaml b/portal-ui/tests/scripts/tenant-kes-encryption/kustomization.yaml new file mode 100644 index 000000000..9130cbb4d --- /dev/null +++ b/portal-ui/tests/scripts/tenant-kes-encryption/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: tenant-kms-encrypted + +images: + - name: minio/operator + +resources: + - github.com/minio/operator/examples/kustomization/tenant-kes-encryption diff --git a/portal-ui/tests/scripts/tenant/kustomization.yaml b/portal-ui/tests/scripts/tenant-lite/kustomization.yaml similarity index 100% rename from portal-ui/tests/scripts/tenant/kustomization.yaml rename to portal-ui/tests/scripts/tenant-lite/kustomization.yaml diff --git a/swagger-operator.yml b/swagger-operator.yml index e856181b7..c7eaf5cb3 100644 --- a/swagger-operator.yml +++ b/swagger-operator.yml @@ -327,7 +327,7 @@ paths: 200: description: A successful response. schema: - $ref: "#/definitions/csrElement" + $ref: "#/definitions/csrElements" default: description: Generic error response. schema: @@ -1634,6 +1634,14 @@ definitions: password: type: string + csrElements: + type: object + properties: + csrElement: + type: array + items: + $ref: "#/definitions/csrElement" + csrElement: type: object properties: