diff --git a/k8s/operator-console/base/console-cluster-role.yaml b/k8s/operator-console/base/console-cluster-role.yaml index 9be94697f..8f4744b03 100644 --- a/k8s/operator-console/base/console-cluster-role.yaml +++ b/k8s/operator-console/base/console-cluster-role.yaml @@ -6,8 +6,18 @@ rules: - apiGroups: - "" resources: - - namespaces - secrets + verbs: + - get + - watch + - create + - list + - patch + - update + - apiGroups: + - "" + resources: + - namespaces - pods - services - events diff --git a/models/create_tenant_request.go b/models/create_tenant_request.go index b22ad895e..a7a6bd6d8 100644 --- a/models/create_tenant_request.go +++ b/models/create_tenant_request.go @@ -57,8 +57,8 @@ type CreateTenantRequest struct { // image Image string `json:"image,omitempty"` - // image pull secrets name - ImagePullSecretsName string `json:"imagePullSecretsName,omitempty"` + // image registry + ImageRegistry *ImageRegistry `json:"image_registry,omitempty"` // mounth path MounthPath string `json:"mounth_path,omitempty"` @@ -98,6 +98,10 @@ func (m *CreateTenantRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateImageRegistry(formats); err != nil { + res = append(res, err) + } + if err := m.validateName(formats); err != nil { res = append(res, err) } @@ -156,6 +160,24 @@ func (m *CreateTenantRequest) validateIdp(formats strfmt.Registry) error { return nil } +func (m *CreateTenantRequest) validateImageRegistry(formats strfmt.Registry) error { + + if swag.IsZero(m.ImageRegistry) { // not required + return nil + } + + if m.ImageRegistry != nil { + if err := m.ImageRegistry.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("image_registry") + } + return err + } + } + + return nil +} + func (m *CreateTenantRequest) validateName(formats strfmt.Registry) error { if err := validate.Required("name", "body", m.Name); err != nil { diff --git a/models/image_registry.go b/models/image_registry.go new file mode 100644 index 000000000..b8df13325 --- /dev/null +++ b/models/image_registry.go @@ -0,0 +1,115 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 ( + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ImageRegistry image registry +// +// swagger:model imageRegistry +type ImageRegistry struct { + + // password + // Required: true + Password *string `json:"password"` + + // registry + // Required: true + Registry *string `json:"registry"` + + // username + // Required: true + Username *string `json:"username"` +} + +// Validate validates this image registry +func (m *ImageRegistry) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validatePassword(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRegistry(formats); err != nil { + res = append(res, err) + } + + if err := m.validateUsername(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ImageRegistry) validatePassword(formats strfmt.Registry) error { + + if err := validate.Required("password", "body", m.Password); err != nil { + return err + } + + return nil +} + +func (m *ImageRegistry) validateRegistry(formats strfmt.Registry) error { + + if err := validate.Required("registry", "body", m.Registry); err != nil { + return err + } + + return nil +} + +func (m *ImageRegistry) validateUsername(formats strfmt.Registry) error { + + if err := validate.Required("username", "body", m.Username); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ImageRegistry) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ImageRegistry) UnmarshalBinary(b []byte) error { + var res ImageRegistry + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/update_tenant_request.go b/models/update_tenant_request.go index d1abff404..e3f5c260d 100644 --- a/models/update_tenant_request.go +++ b/models/update_tenant_request.go @@ -37,6 +37,9 @@ type UpdateTenantRequest struct { // image // Pattern: ^((.*?)/(.*?):(.+))$ Image string `json:"image,omitempty"` + + // image registry + ImageRegistry *ImageRegistry `json:"image_registry,omitempty"` } // Validate validates this update tenant request @@ -47,6 +50,10 @@ func (m *UpdateTenantRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateImageRegistry(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -66,6 +73,24 @@ func (m *UpdateTenantRequest) validateImage(formats strfmt.Registry) error { return nil } +func (m *UpdateTenantRequest) validateImageRegistry(formats strfmt.Registry) error { + + if swag.IsZero(m.ImageRegistry) { // not required + return nil + } + + if m.ImageRegistry != nil { + if err := m.ImageRegistry.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("image_registry") + } + return err + } + } + + return nil +} + // MarshalBinary interface implementation func (m *UpdateTenantRequest) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/restapi/admin_tenants.go b/restapi/admin_tenants.go index d2c3c19e1..7a18bef9d 100644 --- a/restapi/admin_tenants.go +++ b/restapi/admin_tenants.go @@ -49,9 +49,25 @@ import ( "github.com/minio/console/restapi/operations" "github.com/minio/console/restapi/operations/admin_api" operator "github.com/minio/operator/pkg/apis/minio.min.io/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" ) +const ( + minioRegCred = "minio-regcred-secret" +) + +type imageRegistry struct { + Auths map[string]imageRegistryCredentials `json:"auths"` +} + +type imageRegistryCredentials struct { + Username string `json:"username"` + Password string `json:"password"` + Auth string `json:"auth"` +} + func registerTenantHandlers(api *operations.ConsoleAPI) { // Add Tenant api.AdminAPICreateTenantHandler = admin_api.CreateTenantHandlerFunc(func(params admin_api.CreateTenantParams, session *models.Principal) middleware.Responder { @@ -336,6 +352,7 @@ func getListTenantsResponse(session *models.Principal, params admin_api.ListTena func getTenantCreatedResponse(session *models.Principal, params admin_api.CreateTenantParams) (*models.CreateTenantResponse, error) { tenantReq := params.Body minioImage := tenantReq.Image + ctx := context.Background() if minioImage == "" { minImg, err := cluster.GetMinioImage() @@ -377,7 +394,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create }, } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &instanceSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -483,7 +500,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create "tls.key": tlsKey, }, } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &externalTLSCertificateSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &externalTLSCertificateSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -541,7 +558,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create "tls.key": serverTLSKey, }, } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &kesExternalCertificateSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &kesExternalCertificateSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -572,7 +589,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create "tls.key": clientTLSKey, }, } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &instanceExternalClientCertificateSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &instanceExternalClientCertificateSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -703,7 +720,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create "server-config.yaml": serverConfigYaml, }, } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &kesConfigurationSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &kesConfigurationSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -764,7 +781,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create } } - _, err = clientset.CoreV1().Secrets(ns).Create(context.Background(), &instanceSecret, metav1.CreateOptions{}) + _, err = clientset.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -807,11 +824,9 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create minInst.Spec.Metadata.Annotations = tenantReq.Annotations } - // Set Image Pull Secrets Name if defined - if tenantReq.ImagePullSecretsName != "" { - minInst.Spec.ImagePullSecret = corev1.LocalObjectReference{ - Name: tenantReq.ImagePullSecretsName, - } + if err := setImageRegistry(ctx, tenantReq.ImageRegistry, clientset.CoreV1(), ns); err != nil { + log.Println("error setting image registry secret:", err) + return nil, err } opClient, err := cluster.OperatorClient(session.SessionToken) @@ -845,17 +860,76 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create return response, nil } +func setImageRegistry(ctx context.Context, req *models.ImageRegistry, clientset v1.CoreV1Interface, namespace string) error { + if req == nil || req.Registry == nil || req.Username == nil || req.Password == nil { + return nil + } + + credentials := make(map[string]imageRegistryCredentials) + // username:password encoded + authData := []byte(fmt.Sprintf("%s:%s", *req.Username, *req.Password)) + authStr := base64.StdEncoding.EncodeToString(authData) + + credentials[*req.Registry] = imageRegistryCredentials{ + Username: *req.Username, + Password: *req.Password, + Auth: authStr, + } + imRegistry := imageRegistry{ + Auths: credentials, + } + imRegistryJSON, err := json.Marshal(imRegistry) + if err != nil { + return err + } + + instanceSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: minioRegCred, + }, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte(string(imRegistryJSON)), + }, + Type: corev1.SecretTypeDockerConfigJson, + } + + // Get or Create secret if it doesn't exist + _, err = clientset.Secrets(namespace).Get(ctx, minioRegCred, metav1.GetOptions{}) + if err != nil { + if k8sErrors.IsNotFound(err) { + _, err = clientset.Secrets(namespace).Create(ctx, &instanceSecret, metav1.CreateOptions{}) + if err != nil { + return err + } + return nil + } + return err + } + _, err = clientset.Secrets(namespace).Update(ctx, &instanceSecret, metav1.UpdateOptions{}) + if err != nil { + return err + } + return nil +} + // updateTenantAction does an update on the minioTenant by patching the desired changes -func updateTenantAction(ctx context.Context, operatorClient OperatorClient, httpCl cluster.HTTPClientI, nameSpace string, params admin_api.UpdateTenantParams) error { +func updateTenantAction(ctx context.Context, operatorClient OperatorClient, clientset v1.CoreV1Interface, httpCl cluster.HTTPClientI, namespace string, params admin_api.UpdateTenantParams) error { imageToUpdate := params.Body.Image - minInst, err := operatorClient.TenantGet(ctx, nameSpace, params.Tenant, metav1.GetOptions{}) + imageRegistryReq := params.Body.ImageRegistry + + if err := setImageRegistry(ctx, imageRegistryReq, clientset, namespace); err != nil { + log.Println("error setting image registry secret:", err) + return err + } + + minInst, err := operatorClient.TenantGet(ctx, namespace, params.Tenant, metav1.GetOptions{}) if err != nil { return err } // if image to update is empty we'll use the latest image by default if strings.TrimSpace(imageToUpdate) != "" { - minInst.Spec.Image = params.Body.Image + minInst.Spec.Image = imageToUpdate } else { im, err := cluster.GetLatestMinioImage(httpCl) if err != nil { @@ -868,7 +942,7 @@ func updateTenantAction(ctx context.Context, operatorClient OperatorClient, http if err != nil { return err } - _, err = operatorClient.TenantPatch(ctx, nameSpace, minInst.Name, types.MergePatchType, payloadBytes, metav1.PatchOptions{}) + _, err = operatorClient.TenantPatch(ctx, namespace, minInst.Name, types.MergePatchType, payloadBytes, metav1.PatchOptions{}) if err != nil { return err } @@ -882,6 +956,11 @@ func getUpdateTenantResponse(session *models.Principal, params admin_api.UpdateT log.Println("error getting operator client:", err) return err } + // get Kubernetes Client + clientset, err := cluster.K8sClient(session.SessionToken) + if err != nil { + return err + } opClient := &operatorClient{ client: opClientClientSet, @@ -891,7 +970,8 @@ func getUpdateTenantResponse(session *models.Principal, params admin_api.UpdateT Timeout: 4 * time.Second, }, } - if err := updateTenantAction(ctx, opClient, httpC, params.Namespace, params); err != nil { + + if err := updateTenantAction(ctx, opClient, clientset.CoreV1(), httpC, params.Namespace, params); err != nil { log.Println("error patching Tenant:", err) return err } diff --git a/restapi/admin_tenants_test.go b/restapi/admin_tenants_test.go index a00879757..8fcf1d328 100644 --- a/restapi/admin_tenants_test.go +++ b/restapi/admin_tenants_test.go @@ -35,7 +35,9 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" types "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" ) var opClientTenantDeleteMock func(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error @@ -573,6 +575,7 @@ func Test_UpdateTenantAction(t *testing.T) { tests := []struct { name string args args + objs []runtime.Object wantErr bool }{ { @@ -708,8 +711,9 @@ func Test_UpdateTenantAction(t *testing.T) { opClientTenantGetMock = tt.args.mockTenantGet opClientTenantPatchMock = tt.args.mockTenantPatch httpClientGetMock = tt.args.mockHTTPClientGet + cnsClient := fake.NewSimpleClientset(tt.objs...) t.Run(tt.name, func(t *testing.T) { - if err := updateTenantAction(tt.args.ctx, tt.args.operatorClient, tt.args.httpCl, tt.args.nameSpace, tt.args.params); (err != nil) != tt.wantErr { + if err := updateTenantAction(tt.args.ctx, tt.args.operatorClient, cnsClient.CoreV1(), tt.args.httpCl, tt.args.nameSpace, tt.args.params); (err != nil) != tt.wantErr { t.Errorf("deleteTenantAction() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 460865ba4..8aa1c22a7 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -2043,8 +2043,8 @@ func init() { "image": { "type": "string" }, - "imagePullSecretsName": { - "type": "string" + "image_registry": { + "$ref": "#/definitions/imageRegistry" }, "mounth_path": { "type": "string" @@ -2292,6 +2292,25 @@ func init() { } } }, + "imageRegistry": { + "type": "object", + "required": [ + "registry", + "username", + "password" + ], + "properties": { + "password": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "listBucketEventsResponse": { "type": "object", "properties": { @@ -3052,6 +3071,9 @@ func init() { "image": { "type": "string", "pattern": "^((.*?)/(.*?):(.+))$" + }, + "image_registry": { + "$ref": "#/definitions/imageRegistry" } } }, @@ -5937,8 +5959,8 @@ func init() { "image": { "type": "string" }, - "imagePullSecretsName": { - "type": "string" + "image_registry": { + "$ref": "#/definitions/imageRegistry" }, "mounth_path": { "type": "string" @@ -6186,6 +6208,25 @@ func init() { } } }, + "imageRegistry": { + "type": "object", + "required": [ + "registry", + "username", + "password" + ], + "properties": { + "password": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "listBucketEventsResponse": { "type": "object", "properties": { @@ -6880,6 +6921,9 @@ func init() { "image": { "type": "string", "pattern": "^((.*?)/(.*?):(.+))$" + }, + "image_registry": { + "$ref": "#/definitions/imageRegistry" } } }, diff --git a/swagger.yml b/swagger.yml index be76e72c6..29a51a7ab 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1778,7 +1778,23 @@ definitions: image: type: string pattern: "^((.*?)/(.*?):(.+))$" - + image_registry: + $ref: "#/definitions/imageRegistry" + + imageRegistry: + type: object + required: + - registry + - username + - password + properties: + registry: + type: string + username: + type: string + password: + type: string + createTenantRequest: type: object required: @@ -1815,8 +1831,8 @@ definitions: type: object additionalProperties: type: string - imagePullSecretsName: - type: string + image_registry: + $ref: "#/definitions/imageRegistry" idp: type: object $ref: "#/definitions/idpConfiguration"