Add support for edit/add/remove environment variables to MinIO tenant (#2331)

![image](https://user-images.githubusercontent.com/1795553/191574784-69d55ca6-0a8c-41f3-b7f5-8526854cc8d2.png)


Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2022-09-29 20:50:35 -07:00
committed by GitHub
parent 73a687376a
commit a3b88567cc
21 changed files with 2741 additions and 7 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>.
//
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"
)
// TenantConfigurationResponse tenant configuration response
//
// swagger:model tenantConfigurationResponse
type TenantConfigurationResponse struct {
// environment variables
EnvironmentVariables []*EnvironmentVariable `json:"environmentVariables"`
}
// Validate validates this tenant configuration response
func (m *TenantConfigurationResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEnvironmentVariables(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *TenantConfigurationResponse) validateEnvironmentVariables(formats strfmt.Registry) error {
if swag.IsZero(m.EnvironmentVariables) { // not required
return nil
}
for i := 0; i < len(m.EnvironmentVariables); i++ {
if swag.IsZero(m.EnvironmentVariables[i]) { // not required
continue
}
if m.EnvironmentVariables[i] != nil {
if err := m.EnvironmentVariables[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this tenant configuration response based on the context it is used
func (m *TenantConfigurationResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateEnvironmentVariables(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *TenantConfigurationResponse) contextValidateEnvironmentVariables(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.EnvironmentVariables); i++ {
if m.EnvironmentVariables[i] != nil {
if err := m.EnvironmentVariables[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *TenantConfigurationResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *TenantConfigurationResponse) UnmarshalBinary(b []byte) error {
var res TenantConfigurationResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,136 @@
// 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 <http://www.gnu.org/licenses/>.
//
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"
)
// UpdateTenantConfigurationRequest update tenant configuration request
//
// swagger:model updateTenantConfigurationRequest
type UpdateTenantConfigurationRequest struct {
// environment variables
EnvironmentVariables []*EnvironmentVariable `json:"environmentVariables"`
// keys to be deleted
KeysToBeDeleted []string `json:"keysToBeDeleted"`
}
// Validate validates this update tenant configuration request
func (m *UpdateTenantConfigurationRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateEnvironmentVariables(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *UpdateTenantConfigurationRequest) validateEnvironmentVariables(formats strfmt.Registry) error {
if swag.IsZero(m.EnvironmentVariables) { // not required
return nil
}
for i := 0; i < len(m.EnvironmentVariables); i++ {
if swag.IsZero(m.EnvironmentVariables[i]) { // not required
continue
}
if m.EnvironmentVariables[i] != nil {
if err := m.EnvironmentVariables[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this update tenant configuration request based on the context it is used
func (m *UpdateTenantConfigurationRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateEnvironmentVariables(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *UpdateTenantConfigurationRequest) contextValidateEnvironmentVariables(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.EnvironmentVariables); i++ {
if m.EnvironmentVariables[i] != nil {
if err := m.EnvironmentVariables[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("environmentVariables" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *UpdateTenantConfigurationRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *UpdateTenantConfigurationRequest) UnmarshalBinary(b []byte) error {
var res UpdateTenantConfigurationRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -742,6 +742,83 @@ func init() {
}
}
},
"/namespaces/{namespace}/tenants/{tenant}/configuration": {
"get": {
"tags": [
"OperatorAPI"
],
"summary": "Tenant Configuration",
"operationId": "TenantConfiguration",
"parameters": [
{
"type": "string",
"name": "namespace",
"in": "path",
"required": true
},
{
"type": "string",
"name": "tenant",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/tenantConfigurationResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
},
"patch": {
"tags": [
"OperatorAPI"
],
"summary": "Update Tenant Configuration",
"operationId": "UpdateTenantConfiguration",
"parameters": [
{
"type": "string",
"name": "namespace",
"in": "path",
"required": true
},
{
"type": "string",
"name": "tenant",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/updateTenantConfigurationRequest"
}
}
],
"responses": {
"204": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/tenants/{tenant}/csr": {
"get": {
"tags": [
@@ -4514,6 +4591,17 @@ func init() {
}
}
},
"tenantConfigurationResponse": {
"type": "object",
"properties": {
"environmentVariables": {
"type": "array",
"items": {
"$ref": "#/definitions/environmentVariable"
}
}
}
},
"tenantList": {
"type": "object",
"properties": {
@@ -4921,6 +5009,23 @@ func init() {
}
}
},
"updateTenantConfigurationRequest": {
"type": "object",
"properties": {
"environmentVariables": {
"type": "array",
"items": {
"$ref": "#/definitions/environmentVariable"
}
},
"keysToBeDeleted": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"updateTenantRequest": {
"type": "object",
"properties": {
@@ -5848,6 +5953,83 @@ func init() {
}
}
},
"/namespaces/{namespace}/tenants/{tenant}/configuration": {
"get": {
"tags": [
"OperatorAPI"
],
"summary": "Tenant Configuration",
"operationId": "TenantConfiguration",
"parameters": [
{
"type": "string",
"name": "namespace",
"in": "path",
"required": true
},
{
"type": "string",
"name": "tenant",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/tenantConfigurationResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
},
"patch": {
"tags": [
"OperatorAPI"
],
"summary": "Update Tenant Configuration",
"operationId": "UpdateTenantConfiguration",
"parameters": [
{
"type": "string",
"name": "namespace",
"in": "path",
"required": true
},
{
"type": "string",
"name": "tenant",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/updateTenantConfigurationRequest"
}
}
],
"responses": {
"204": {
"description": "A successful response."
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/namespaces/{namespace}/tenants/{tenant}/csr": {
"get": {
"tags": [
@@ -10328,6 +10510,17 @@ func init() {
}
}
},
"tenantConfigurationResponse": {
"type": "object",
"properties": {
"environmentVariables": {
"type": "array",
"items": {
"$ref": "#/definitions/environmentVariable"
}
}
}
},
"tenantList": {
"type": "object",
"properties": {
@@ -10735,6 +10928,23 @@ func init() {
}
}
},
"updateTenantConfigurationRequest": {
"type": "object",
"properties": {
"environmentVariables": {
"type": "array",
"items": {
"$ref": "#/definitions/environmentVariable"
}
},
"keysToBeDeleted": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"updateTenantRequest": {
"type": "object",
"properties": {

View File

@@ -223,6 +223,9 @@ func NewOperatorAPI(spec *loads.Document) *OperatorAPI {
OperatorAPITenantAddPoolHandler: operator_api.TenantAddPoolHandlerFunc(func(params operator_api.TenantAddPoolParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.TenantAddPool has not yet been implemented")
}),
OperatorAPITenantConfigurationHandler: operator_api.TenantConfigurationHandlerFunc(func(params operator_api.TenantConfigurationParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.TenantConfiguration has not yet been implemented")
}),
OperatorAPITenantDeleteEncryptionHandler: operator_api.TenantDeleteEncryptionHandlerFunc(func(params operator_api.TenantDeleteEncryptionParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.TenantDeleteEncryption has not yet been implemented")
}),
@@ -250,6 +253,9 @@ func NewOperatorAPI(spec *loads.Document) *OperatorAPI {
OperatorAPIUpdateTenantHandler: operator_api.UpdateTenantHandlerFunc(func(params operator_api.UpdateTenantParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.UpdateTenant has not yet been implemented")
}),
OperatorAPIUpdateTenantConfigurationHandler: operator_api.UpdateTenantConfigurationHandlerFunc(func(params operator_api.UpdateTenantConfigurationParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.UpdateTenantConfiguration has not yet been implemented")
}),
OperatorAPIUpdateTenantDomainsHandler: operator_api.UpdateTenantDomainsHandlerFunc(func(params operator_api.UpdateTenantDomainsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation operator_api.UpdateTenantDomains has not yet been implemented")
}),
@@ -414,6 +420,8 @@ type OperatorAPI struct {
OperatorAPISubscriptionValidateHandler operator_api.SubscriptionValidateHandler
// OperatorAPITenantAddPoolHandler sets the operation handler for the tenant add pool operation
OperatorAPITenantAddPoolHandler operator_api.TenantAddPoolHandler
// OperatorAPITenantConfigurationHandler sets the operation handler for the tenant configuration operation
OperatorAPITenantConfigurationHandler operator_api.TenantConfigurationHandler
// OperatorAPITenantDeleteEncryptionHandler sets the operation handler for the tenant delete encryption operation
OperatorAPITenantDeleteEncryptionHandler operator_api.TenantDeleteEncryptionHandler
// OperatorAPITenantDetailsHandler sets the operation handler for the tenant details operation
@@ -432,6 +440,8 @@ type OperatorAPI struct {
OperatorAPITenantUpdatePoolsHandler operator_api.TenantUpdatePoolsHandler
// OperatorAPIUpdateTenantHandler sets the operation handler for the update tenant operation
OperatorAPIUpdateTenantHandler operator_api.UpdateTenantHandler
// OperatorAPIUpdateTenantConfigurationHandler sets the operation handler for the update tenant configuration operation
OperatorAPIUpdateTenantConfigurationHandler operator_api.UpdateTenantConfigurationHandler
// OperatorAPIUpdateTenantDomainsHandler sets the operation handler for the update tenant domains operation
OperatorAPIUpdateTenantDomainsHandler operator_api.UpdateTenantDomainsHandler
// OperatorAPIUpdateTenantIdentityProviderHandler sets the operation handler for the update tenant identity provider operation
@@ -678,6 +688,9 @@ func (o *OperatorAPI) Validate() error {
if o.OperatorAPITenantAddPoolHandler == nil {
unregistered = append(unregistered, "operator_api.TenantAddPoolHandler")
}
if o.OperatorAPITenantConfigurationHandler == nil {
unregistered = append(unregistered, "operator_api.TenantConfigurationHandler")
}
if o.OperatorAPITenantDeleteEncryptionHandler == nil {
unregistered = append(unregistered, "operator_api.TenantDeleteEncryptionHandler")
}
@@ -705,6 +718,9 @@ func (o *OperatorAPI) Validate() error {
if o.OperatorAPIUpdateTenantHandler == nil {
unregistered = append(unregistered, "operator_api.UpdateTenantHandler")
}
if o.OperatorAPIUpdateTenantConfigurationHandler == nil {
unregistered = append(unregistered, "operator_api.UpdateTenantConfigurationHandler")
}
if o.OperatorAPIUpdateTenantDomainsHandler == nil {
unregistered = append(unregistered, "operator_api.UpdateTenantDomainsHandler")
}
@@ -1024,6 +1040,10 @@ func (o *OperatorAPI) initHandlerCache() {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/namespaces/{namespace}/tenants/{tenant}/pools"] = operator_api.NewTenantAddPool(o.context, o.OperatorAPITenantAddPoolHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/namespaces/{namespace}/tenants/{tenant}/configuration"] = operator_api.NewTenantConfiguration(o.context, o.OperatorAPITenantConfigurationHandler)
if o.handlers["DELETE"] == nil {
o.handlers["DELETE"] = make(map[string]http.Handler)
}
@@ -1060,6 +1080,10 @@ func (o *OperatorAPI) initHandlerCache() {
o.handlers["PUT"] = make(map[string]http.Handler)
}
o.handlers["PUT"]["/namespaces/{namespace}/tenants/{tenant}"] = operator_api.NewUpdateTenant(o.context, o.OperatorAPIUpdateTenantHandler)
if o.handlers["PATCH"] == nil {
o.handlers["PATCH"] = make(map[string]http.Handler)
}
o.handlers["PATCH"]["/namespaces/{namespace}/tenants/{tenant}/configuration"] = operator_api.NewUpdateTenantConfiguration(o.context, o.OperatorAPIUpdateTenantConfigurationHandler)
if o.handlers["PUT"] == nil {
o.handlers["PUT"] = make(map[string]http.Handler)
}

View File

@@ -0,0 +1,88 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// TenantConfigurationHandlerFunc turns a function with the right signature into a tenant configuration handler
type TenantConfigurationHandlerFunc func(TenantConfigurationParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn TenantConfigurationHandlerFunc) Handle(params TenantConfigurationParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// TenantConfigurationHandler interface for that can handle valid tenant configuration params
type TenantConfigurationHandler interface {
Handle(TenantConfigurationParams, *models.Principal) middleware.Responder
}
// NewTenantConfiguration creates a new http.Handler for the tenant configuration operation
func NewTenantConfiguration(ctx *middleware.Context, handler TenantConfigurationHandler) *TenantConfiguration {
return &TenantConfiguration{Context: ctx, Handler: handler}
}
/*
TenantConfiguration swagger:route GET /namespaces/{namespace}/tenants/{tenant}/configuration OperatorAPI tenantConfiguration
Tenant Configuration
*/
type TenantConfiguration struct {
Context *middleware.Context
Handler TenantConfigurationHandler
}
func (o *TenantConfiguration) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewTenantConfigurationParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,112 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewTenantConfigurationParams creates a new TenantConfigurationParams object
//
// There are no default values defined in the spec.
func NewTenantConfigurationParams() TenantConfigurationParams {
return TenantConfigurationParams{}
}
// TenantConfigurationParams contains all the bound params for the tenant configuration operation
// typically these are obtained from a http.Request
//
// swagger:parameters TenantConfiguration
type TenantConfigurationParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
Namespace string
/*
Required: true
In: path
*/
Tenant string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewTenantConfigurationParams() beforehand.
func (o *TenantConfigurationParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rNamespace, rhkNamespace, _ := route.Params.GetOK("namespace")
if err := o.bindNamespace(rNamespace, rhkNamespace, route.Formats); err != nil {
res = append(res, err)
}
rTenant, rhkTenant, _ := route.Params.GetOK("tenant")
if err := o.bindTenant(rTenant, rhkTenant, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindNamespace binds and validates parameter Namespace from path.
func (o *TenantConfigurationParams) bindNamespace(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Namespace = raw
return nil
}
// bindTenant binds and validates parameter Tenant from path.
func (o *TenantConfigurationParams) bindTenant(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Tenant = raw
return nil
}

View File

@@ -0,0 +1,135 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// TenantConfigurationOKCode is the HTTP code returned for type TenantConfigurationOK
const TenantConfigurationOKCode int = 200
/*
TenantConfigurationOK A successful response.
swagger:response tenantConfigurationOK
*/
type TenantConfigurationOK struct {
/*
In: Body
*/
Payload *models.TenantConfigurationResponse `json:"body,omitempty"`
}
// NewTenantConfigurationOK creates TenantConfigurationOK with default headers values
func NewTenantConfigurationOK() *TenantConfigurationOK {
return &TenantConfigurationOK{}
}
// WithPayload adds the payload to the tenant configuration o k response
func (o *TenantConfigurationOK) WithPayload(payload *models.TenantConfigurationResponse) *TenantConfigurationOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the tenant configuration o k response
func (o *TenantConfigurationOK) SetPayload(payload *models.TenantConfigurationResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *TenantConfigurationOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*
TenantConfigurationDefault Generic error response.
swagger:response tenantConfigurationDefault
*/
type TenantConfigurationDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewTenantConfigurationDefault creates TenantConfigurationDefault with default headers values
func NewTenantConfigurationDefault(code int) *TenantConfigurationDefault {
if code <= 0 {
code = 500
}
return &TenantConfigurationDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the tenant configuration default response
func (o *TenantConfigurationDefault) WithStatusCode(code int) *TenantConfigurationDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the tenant configuration default response
func (o *TenantConfigurationDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the tenant configuration default response
func (o *TenantConfigurationDefault) WithPayload(payload *models.Error) *TenantConfigurationDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the tenant configuration default response
func (o *TenantConfigurationDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *TenantConfigurationDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,124 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// TenantConfigurationURL generates an URL for the tenant configuration operation
type TenantConfigurationURL struct {
Namespace string
Tenant string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *TenantConfigurationURL) WithBasePath(bp string) *TenantConfigurationURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *TenantConfigurationURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *TenantConfigurationURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/namespaces/{namespace}/tenants/{tenant}/configuration"
namespace := o.Namespace
if namespace != "" {
_path = strings.Replace(_path, "{namespace}", namespace, -1)
} else {
return nil, errors.New("namespace is required on TenantConfigurationURL")
}
tenant := o.Tenant
if tenant != "" {
_path = strings.Replace(_path, "{tenant}", tenant, -1)
} else {
return nil, errors.New("tenant is required on TenantConfigurationURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *TenantConfigurationURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *TenantConfigurationURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *TenantConfigurationURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on TenantConfigurationURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on TenantConfigurationURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *TenantConfigurationURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -0,0 +1,88 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// UpdateTenantConfigurationHandlerFunc turns a function with the right signature into a update tenant configuration handler
type UpdateTenantConfigurationHandlerFunc func(UpdateTenantConfigurationParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn UpdateTenantConfigurationHandlerFunc) Handle(params UpdateTenantConfigurationParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// UpdateTenantConfigurationHandler interface for that can handle valid update tenant configuration params
type UpdateTenantConfigurationHandler interface {
Handle(UpdateTenantConfigurationParams, *models.Principal) middleware.Responder
}
// NewUpdateTenantConfiguration creates a new http.Handler for the update tenant configuration operation
func NewUpdateTenantConfiguration(ctx *middleware.Context, handler UpdateTenantConfigurationHandler) *UpdateTenantConfiguration {
return &UpdateTenantConfiguration{Context: ctx, Handler: handler}
}
/*
UpdateTenantConfiguration swagger:route PATCH /namespaces/{namespace}/tenants/{tenant}/configuration OperatorAPI updateTenantConfiguration
Update Tenant Configuration
*/
type UpdateTenantConfiguration struct {
Context *middleware.Context
Handler UpdateTenantConfigurationHandler
}
func (o *UpdateTenantConfiguration) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewUpdateTenantConfigurationParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,150 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/minio/console/models"
)
// NewUpdateTenantConfigurationParams creates a new UpdateTenantConfigurationParams object
//
// There are no default values defined in the spec.
func NewUpdateTenantConfigurationParams() UpdateTenantConfigurationParams {
return UpdateTenantConfigurationParams{}
}
// UpdateTenantConfigurationParams contains all the bound params for the update tenant configuration operation
// typically these are obtained from a http.Request
//
// swagger:parameters UpdateTenantConfiguration
type UpdateTenantConfigurationParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.UpdateTenantConfigurationRequest
/*
Required: true
In: path
*/
Namespace string
/*
Required: true
In: path
*/
Tenant string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewUpdateTenantConfigurationParams() beforehand.
func (o *UpdateTenantConfigurationParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.UpdateTenantConfigurationRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
ctx := validate.WithOperationRequest(r.Context())
if err := body.ContextValidate(ctx, route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
rNamespace, rhkNamespace, _ := route.Params.GetOK("namespace")
if err := o.bindNamespace(rNamespace, rhkNamespace, route.Formats); err != nil {
res = append(res, err)
}
rTenant, rhkTenant, _ := route.Params.GetOK("tenant")
if err := o.bindTenant(rTenant, rhkTenant, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindNamespace binds and validates parameter Namespace from path.
func (o *UpdateTenantConfigurationParams) bindNamespace(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Namespace = raw
return nil
}
// bindTenant binds and validates parameter Tenant from path.
func (o *UpdateTenantConfigurationParams) bindTenant(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Tenant = raw
return nil
}

View File

@@ -0,0 +1,115 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// UpdateTenantConfigurationNoContentCode is the HTTP code returned for type UpdateTenantConfigurationNoContent
const UpdateTenantConfigurationNoContentCode int = 204
/*
UpdateTenantConfigurationNoContent A successful response.
swagger:response updateTenantConfigurationNoContent
*/
type UpdateTenantConfigurationNoContent struct {
}
// NewUpdateTenantConfigurationNoContent creates UpdateTenantConfigurationNoContent with default headers values
func NewUpdateTenantConfigurationNoContent() *UpdateTenantConfigurationNoContent {
return &UpdateTenantConfigurationNoContent{}
}
// WriteResponse to the client
func (o *UpdateTenantConfigurationNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses
rw.WriteHeader(204)
}
/*
UpdateTenantConfigurationDefault Generic error response.
swagger:response updateTenantConfigurationDefault
*/
type UpdateTenantConfigurationDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewUpdateTenantConfigurationDefault creates UpdateTenantConfigurationDefault with default headers values
func NewUpdateTenantConfigurationDefault(code int) *UpdateTenantConfigurationDefault {
if code <= 0 {
code = 500
}
return &UpdateTenantConfigurationDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the update tenant configuration default response
func (o *UpdateTenantConfigurationDefault) WithStatusCode(code int) *UpdateTenantConfigurationDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the update tenant configuration default response
func (o *UpdateTenantConfigurationDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the update tenant configuration default response
func (o *UpdateTenantConfigurationDefault) WithPayload(payload *models.Error) *UpdateTenantConfigurationDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the update tenant configuration default response
func (o *UpdateTenantConfigurationDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *UpdateTenantConfigurationDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,124 @@
// 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 <http://www.gnu.org/licenses/>.
//
package operator_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// UpdateTenantConfigurationURL generates an URL for the update tenant configuration operation
type UpdateTenantConfigurationURL struct {
Namespace string
Tenant string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *UpdateTenantConfigurationURL) WithBasePath(bp string) *UpdateTenantConfigurationURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *UpdateTenantConfigurationURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *UpdateTenantConfigurationURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/namespaces/{namespace}/tenants/{tenant}/configuration"
namespace := o.Namespace
if namespace != "" {
_path = strings.Replace(_path, "{namespace}", namespace, -1)
} else {
return nil, errors.New("namespace is required on UpdateTenantConfigurationURL")
}
tenant := o.Tenant
if tenant != "" {
_path = strings.Replace(_path, "{tenant}", tenant, -1)
} else {
return nil, errors.New("tenant is required on UpdateTenantConfigurationURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *UpdateTenantConfigurationURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *UpdateTenantConfigurationURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *UpdateTenantConfigurationURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on UpdateTenantConfigurationURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on UpdateTenantConfigurationURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *UpdateTenantConfigurationURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -110,6 +110,24 @@ func registerTenantHandlers(api *operations.OperatorAPI) {
return operator_api.NewTenantDetailsOK().WithPayload(resp)
})
// Tenant Configuration details
// Tenant Security details
api.OperatorAPITenantConfigurationHandler = operator_api.TenantConfigurationHandlerFunc(func(params operator_api.TenantConfigurationParams, session *models.Principal) middleware.Responder {
resp, err := getTenantConfigurationResponse(session, params)
if err != nil {
return operator_api.NewTenantConfigurationDefault(int(err.Code)).WithPayload(err)
}
return operator_api.NewTenantConfigurationOK().WithPayload(resp)
})
// Update Tenant Configuration
api.OperatorAPIUpdateTenantConfigurationHandler = operator_api.UpdateTenantConfigurationHandlerFunc(func(params operator_api.UpdateTenantConfigurationParams, session *models.Principal) middleware.Responder {
err := getUpdateTenantConfigurationResponse(session, params)
if err != nil {
return operator_api.NewUpdateTenantConfigurationDefault(int(err.Code)).WithPayload(err)
}
return operator_api.NewUpdateTenantConfigurationNoContent()
})
// Tenant Security details
api.OperatorAPITenantSecurityHandler = operator_api.TenantSecurityHandlerFunc(func(params operator_api.TenantSecurityParams, session *models.Principal) middleware.Responder {
resp, err := getTenantSecurityResponse(session, params)
@@ -972,6 +990,123 @@ func getSetTenantAdministratorsResponse(session *models.Principal, params operat
return nil
}
func getTenantConfigurationResponse(session *models.Principal, params operator_api.TenantConfigurationParams) (*models.TenantConfigurationResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, restapi.ErrorWithContext(ctx, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
minTenant, err := getTenant(ctx, opClient, params.Namespace, params.Tenant)
if err != nil {
return nil, restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
k8sClient := k8sClient{
client: clientSet,
}
if err != nil {
return nil, restapi.ErrorWithContext(ctx, err)
}
tenantConfiguration, err := GetTenantConfiguration(ctx, &k8sClient, minTenant)
if err != nil {
return nil, restapi.ErrorWithContext(ctx, err)
}
delete(tenantConfiguration, "accesskey")
delete(tenantConfiguration, "secretkey")
var envVars []*models.EnvironmentVariable
for key, value := range tenantConfiguration {
envVars = append(envVars, &models.EnvironmentVariable{
Key: key,
Value: value,
})
}
sort.Slice(envVars, func(i, j int) bool {
return envVars[i].Key < envVars[j].Key
})
configurationInfo := &models.TenantConfigurationResponse{EnvironmentVariables: envVars}
return configurationInfo, nil
}
func getUpdateTenantConfigurationResponse(session *models.Principal, params operator_api.UpdateTenantConfigurationParams) *models.Error {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return restapi.ErrorWithContext(ctx, err)
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return restapi.ErrorWithContext(ctx, err)
}
k8sClient := k8sClient{
client: clientSet,
}
opClient := &operatorClient{
client: opClientClientSet,
}
if err := updateTenantConfigurationFile(ctx, opClient, &k8sClient, params.Namespace, params); err != nil {
return restapi.ErrorWithContext(ctx, err, errors.New("unable to update tenant configuration"))
}
return nil
}
func updateTenantConfigurationFile(ctx context.Context, operatorClient OperatorClientI, client K8sClientI, namespace string, params operator_api.UpdateTenantConfigurationParams) error {
tenant, err := operatorClient.TenantGet(ctx, namespace, params.Tenant, metav1.GetOptions{})
if err != nil {
return err
}
tenantConfiguration, err := GetTenantConfiguration(ctx, client, tenant)
if err != nil {
return err
}
delete(tenantConfiguration, "accesskey")
delete(tenantConfiguration, "secretkey")
requestBody := params.Body
if requestBody == nil {
return errors.New("missing request body")
}
// Patch tenant configuration file with the new values provided by the user
for _, envVar := range requestBody.EnvironmentVariables {
if envVar.Key == "" {
continue
}
tenantConfiguration[envVar.Key] = envVar.Value
}
// Remove existing values from configuration file
for _, keyToBeDeleted := range requestBody.KeysToBeDeleted {
delete(tenantConfiguration, keyToBeDeleted)
}
if !tenant.HasConfigurationSecret() {
return errors.New("tenant configuration file not found")
}
tenantConfigurationSecret, err := client.getSecret(ctx, tenant.Namespace, tenant.Spec.Configuration.Name, metav1.GetOptions{})
if err != nil {
return err
}
tenantConfigurationSecret.Data["config.env"] = []byte(GenerateTenantConfigurationFile(tenantConfiguration))
_, err = client.updateSecret(ctx, namespace, tenantConfigurationSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
// Restart all MinIO pods at the same time for they to take the new configuration
err = client.deletePodCollection(ctx, namespace, metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, tenant.Name),
})
if err != nil {
return err
}
return nil
}
func getTenantSecurityResponse(session *models.Principal, params operator_api.TenantSecurityParams) (*models.TenantSecurityResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()

View File

@@ -129,7 +129,7 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
mockGetService func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error)
}{
{
name: "Return Tenant Admin, no errors",
name: "Return Tenant Admin, no errors using legacy credentials",
args: args{
ctx: ctx,
client: kClient,
@@ -138,7 +138,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
Namespace: "default",
Name: "tenant-1",
},
Spec: miniov2.TenantSpec{CredsSecret: &corev1.LocalObjectReference{Name: "secret-name"}},
Spec: miniov2.TenantSpec{
CredsSecret: &corev1.LocalObjectReference{
Name: "secret-name",
},
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
@@ -161,6 +165,90 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
},
wantErr: false,
},
{
name: "Return Tenant Admin, no errors using credentials from configuration file",
args: args{
ctx: ctx,
client: kClient,
tenant: miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
Spec: miniov2.TenantSpec{
CredsSecret: &corev1.LocalObjectReference{
Name: "secret-name",
},
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration",
},
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["config.env"] = []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
`)
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: false,
},
{
name: "Return Tenant Admin, no errors using credentials from configuration file 2",
args: args{
ctx: ctx,
client: kClient,
tenant: miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
Spec: miniov2.TenantSpec{
CredsSecret: &corev1.LocalObjectReference{
Name: "secret-name",
},
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration",
},
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["config.env"] = []byte(`
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
`)
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: false,
},
{
name: "Access key not stored on secrets",
args: args{
@@ -1354,3 +1442,405 @@ ZrJuAw==
})
}
}
func Test_getTenant(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opClient := opClientMock{}
type args struct {
ctx context.Context
operatorClient OperatorClientI
namespace string
tenantName string
mockTenantGet func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error)
}
tests := []struct {
name string
args args
want *miniov2.Tenant
wantErr bool
}{
{
name: "error getting tenant information",
args: args{
ctx: ctx,
operatorClient: opClient,
namespace: "default",
tenantName: "test",
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return nil, errors.New("error getting tenant information")
},
},
want: nil,
wantErr: true,
},
{
name: "success getting tenant information",
args: args{
ctx: ctx,
operatorClient: opClient,
namespace: "default",
tenantName: "test",
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
}, nil
},
},
want: &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
},
wantErr: false,
},
}
for _, tt := range tests {
opClientTenantGetMock = tt.args.mockTenantGet
t.Run(tt.name, func(t *testing.T) {
got, err := getTenant(tt.args.ctx, tt.args.operatorClient, tt.args.namespace, tt.args.tenantName)
if (err != nil) != tt.wantErr {
t.Errorf("getTenant(%v, %v, %v, %v)", tt.args.ctx, tt.args.operatorClient, tt.args.namespace, tt.args.tenantName)
}
assert.Equalf(t, tt.want, got, "getTenant(%v, %v, %v, %v)", tt.args.ctx, tt.args.operatorClient, tt.args.namespace, tt.args.tenantName)
})
}
}
func Test_updateTenantConfigurationFile(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opClient := opClientMock{}
kClient := k8sClientMock{}
type args struct {
ctx context.Context
operatorClient OperatorClientI
client K8sClientI
namespace string
params operator_api.UpdateTenantConfigurationParams
mockTenantGet func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error)
mockGetSecret func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error)
mockUpdateSecret func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error)
mockDeletePodCollection func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "error getting tenant information",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return nil, errors.New("error getting tenant")
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
{
name: "error during getting tenant configuration",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("error getting tenant configuration")
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
{
name: "error updating tenant configuration because of missing configuration secret",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
Body: &models.UpdateTenantConfigurationRequest{},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
{
name: "error because tenant configuration secret is nil",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
Body: &models.UpdateTenantConfigurationRequest{},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
{
name: "error updating tenant configuration because of k8s issue",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
Body: &models.UpdateTenantConfigurationRequest{},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, errors.New("error updating configuration secret")
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
{
name: "error during deleting pod collection",
wantErr: true,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
Body: &models.UpdateTenantConfigurationRequest{},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return errors.New("error deleting minio pods")
},
},
},
{
name: "success updating tenant configuration secret",
wantErr: false,
args: args{
ctx: ctx,
operatorClient: opClient,
client: kClient,
namespace: "default",
params: operator_api.UpdateTenantConfigurationParams{
Namespace: "default",
Tenant: "test",
Body: &models.UpdateTenantConfigurationRequest{},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
}, nil
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
mockUpdateSecret: func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
},
}
for _, tt := range tests {
k8sclientGetSecretMock = tt.args.mockGetSecret
opClientTenantGetMock = tt.args.mockTenantGet
UpdateSecretMock = tt.args.mockUpdateSecret
DeletePodCollectionMock = tt.args.mockDeletePodCollection
t.Run(tt.name, func(t *testing.T) {
err := updateTenantConfigurationFile(tt.args.ctx, tt.args.operatorClient, tt.args.client, tt.args.namespace, tt.args.params)
if (err != nil) != tt.wantErr {
t.Errorf("updateTenantConfigurationFile(%v, %v, %v, %v, %v)", tt.args.ctx, tt.args.operatorClient, tt.args.client, tt.args.namespace, tt.args.params)
}
})
}
}

View File

@@ -46,11 +46,15 @@ func GetTenantConfiguration(ctx context.Context, clientSet K8sClientI, tenant *m
}
if tenant.HasConfigurationSecret() {
minioConfigurationSecret, err := clientSet.getSecret(ctx, tenant.Namespace, tenant.Spec.Configuration.Name, metav1.GetOptions{})
if err == nil {
configFromFile := miniov2.ParseRawConfiguration(minioConfigurationSecret.Data["config.env"])
for key, val := range configFromFile {
tenantConfiguration[key] = string(val)
}
if err != nil {
return tenantConfiguration, err
}
if minioConfigurationSecret == nil {
return tenantConfiguration, errors.New("tenant configuration secret is empty")
}
configFromFile := miniov2.ParseRawConfiguration(minioConfigurationSecret.Data["config.env"])
for key, val := range configFromFile {
tenantConfiguration[key] = string(val)
}
}
return tenantConfiguration, nil

230
operatorapi/utils_test.go Normal file
View File

@@ -0,0 +1,230 @@
// 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 <http://www.gnu.org/licenses/>.
package operatorapi
import (
"context"
"errors"
"testing"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestGetTenantConfiguration(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
kClient := k8sClientMock{}
type args struct {
ctx context.Context
clientSet K8sClientI
tenant *miniov2.Tenant
}
tests := []struct {
name string
args args
want map[string]string
mockGetSecret func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error)
wantErr bool
}{
{
name: "error because nil tenant",
args: args{
ctx: ctx,
clientSet: kClient,
tenant: nil,
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, nil
},
want: nil,
wantErr: true,
},
{
name: "empty configuration map because no configuration secret is present",
args: args{
ctx: ctx,
clientSet: kClient,
tenant: &miniov2.Tenant{},
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, nil
},
want: map[string]string{},
wantErr: false,
},
{
name: "empty configuration map because error while retrieving configuration secret",
args: args{
ctx: ctx,
clientSet: kClient,
tenant: &miniov2.Tenant{
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
},
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("an error has occurred")
},
want: map[string]string{},
wantErr: true,
},
{
name: "parsing tenant configuration from secret file",
args: args{
ctx: ctx,
clientSet: kClient,
tenant: &miniov2.Tenant{
Spec: miniov2.TenantSpec{
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
},
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
want: map[string]string{
"MINIO_ROOT_USER": "minio",
"MINIO_ROOT_PASSWORD": "minio123",
"MINIO_CONSOLE_ADDRESS": ":8080",
"accesskey": "minio",
"secretkey": "minio123",
"MINIO_IDENTITY_LDAP_SERVER_INSECURE": "on",
"MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER": "(&(objectclass=groupOfNames)(member=%d))",
"MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN": "ou=swengg,dc=min,dc=io",
"MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER": "(uid=%s)",
"MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN": "dc=min,dc=io",
"MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD": "admin",
"MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN": "cn=admin,dc=min,dc=io",
"MINIO_IDENTITY_LDAP_SERVER_ADDR": "localhost:389",
},
wantErr: false,
},
{
name: "parsing tenant configuration from secret file and environment variables",
args: args{
ctx: ctx,
clientSet: kClient,
tenant: &miniov2.Tenant{
Spec: miniov2.TenantSpec{
Env: []corev1.EnvVar{
{
Name: "MINIO_KMS_SECRET_KEY",
Value: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=",
},
},
Configuration: &corev1.LocalObjectReference{
Name: "tenant-configuration-secret",
},
},
},
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{Data: map[string][]byte{
"config.env": []byte(`
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_CONSOLE_ADDRESS=:8080
export MINIO_IDENTITY_LDAP_SERVER_ADDR=localhost:389
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN="cn=admin,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD="admin"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN="dc=min,dc=io"
export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER="(uid=%s)"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="ou=swengg,dc=min,dc=io"
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(member=%d))"
export MINIO_IDENTITY_LDAP_SERVER_INSECURE="on"
`),
}}, nil
},
want: map[string]string{
"MINIO_ROOT_USER": "minio",
"MINIO_ROOT_PASSWORD": "minio123",
"MINIO_CONSOLE_ADDRESS": ":8080",
"accesskey": "minio",
"secretkey": "minio123",
"MINIO_IDENTITY_LDAP_SERVER_INSECURE": "on",
"MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER": "(&(objectclass=groupOfNames)(member=%d))",
"MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN": "ou=swengg,dc=min,dc=io",
"MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER": "(uid=%s)",
"MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN": "dc=min,dc=io",
"MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD": "admin",
"MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN": "cn=admin,dc=min,dc=io",
"MINIO_IDENTITY_LDAP_SERVER_ADDR": "localhost:389",
"MINIO_KMS_SECRET_KEY": "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw=",
},
wantErr: false,
},
}
for _, tt := range tests {
k8sclientGetSecretMock = tt.mockGetSecret
t.Run(tt.name, func(t *testing.T) {
got, err := GetTenantConfiguration(tt.args.ctx, tt.args.clientSet, tt.args.tenant)
if (err != nil) != tt.wantErr {
t.Errorf("GetTenantConfiguration(%v, %v, %v)", tt.args.ctx, tt.args.clientSet, tt.args.tenant)
}
assert.Equalf(t, tt.want, got, "GetTenantConfiguration(%v, %v, %v)", tt.args.ctx, tt.args.clientSet, tt.args.tenant)
})
}
}
func TestGenerateTenantConfigurationFile(t *testing.T) {
type args struct {
configuration map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "convert configuration map into raw string",
args: args{
configuration: map[string]string{
"MINIO_ROOT_USER": "minio",
},
},
want: `export MINIO_ROOT_USER="minio"
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, GenerateTenantConfigurationFile(tt.args.configuration), "GenerateTenantConfigurationFile(%v)", tt.args.configuration)
})
}
}

View File

@@ -733,6 +733,26 @@ export const getClientOS = (): string => {
return getPlatform;
};
export const MinIOEnvVarsSettings: any = {
MINIO_ACCESS_KEY: { secret: true },
MINIO_ACCESS_KEY_OLD: { secret: true },
MINIO_AUDIT_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD: { secret: true },
MINIO_IDENTITY_OPENID_CLIENT_SECRET: { secret: true },
MINIO_KMS_SECRET_KEY: { secret: true },
MINIO_LOGGER_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_NOTIFY_ELASTICSEARCH_PASSWORD: { secret: true },
MINIO_NOTIFY_KAFKA_SASL_PASSWORD: { secret: true },
MINIO_NOTIFY_MQTT_PASSWORD: { secret: true },
MINIO_NOTIFY_NATS_PASSWORD: { secret: true },
MINIO_NOTIFY_NATS_TOKEN: { secret: true },
MINIO_NOTIFY_REDIS_PASSWORD: { secret: true },
MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_ROOT_PASSWORD: { secret: true },
MINIO_SECRET_KEY: { secret: true },
MINIO_SECRET_KEY_OLD: { secret: true },
};
export const MinIOEnvironmentVariables = [
"MINIO_ACCESS_KEY",
"MINIO_ACCESS_KEY_OLD",

View File

@@ -0,0 +1,321 @@
// 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 <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { connect, useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import { DialogContentText, IconButton } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "../../../../icons/RemoveIcon";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import {
ITenantConfigurationRequest,
ITenantConfigurationResponse,
LabelKeyPair,
} from "../types";
import {
containerForHeader,
createTenantCommon,
formFieldStyles,
modalBasic,
spacingUtils,
tenantDetailsStyles,
wizardCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { AppState, useAppDispatch } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import { ConfirmModalIcon } from "../../../../icons";
import { setErrorSnackMessage } from "../../../../systemSlice";
import api from "../../../../common/api";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import Loader from "../../Common/Loader/Loader";
import { Button } from "mds";
import { MinIOEnvVarsSettings } from "../../../../common/utils";
interface ITenantConfiguration {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...spacingUtils,
envVarRow: {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
"&:last-child": {
borderBottom: 0,
},
"@media (max-width: 900px)": {
flex: 1,
"& div label": {
minWidth: 50,
},
},
},
rowActions: {
display: "flex",
justifyContent: "flex-end",
"@media (max-width: 900px)": {
flex: 1,
},
},
overlayAction: {
marginLeft: 10,
"& svg": {
width: 15,
height: 15,
maxWidth: 15,
maxHeight: 15,
},
"& button": {
background: "#EAEAEA",
},
},
loaderAlign: {
textAlign: "center",
},
bold: { fontWeight: "bold" },
italic: { fontStyle: "italic" },
fileItem: {
marginRight: 10,
display: "flex",
"& div label": {
minWidth: 50,
},
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
...containerForHeader(theme.spacing(4)),
...createTenantCommon,
...formFieldStyles,
...modalBasic,
...wizardCommon,
});
const TenantConfiguration = ({ classes }: ITenantConfiguration) => {
const dispatch = useAppDispatch();
const tenant = useSelector((state: AppState) => state.tenants.tenantInfo);
const loadingTenant = useSelector(
(state: AppState) => state.tenants.loadingTenant
);
const [isSending, setIsSending] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [envVars, setEnvVars] = useState<LabelKeyPair[]>([]);
const [envVarsToBeDeleted, setEnvVarsToBeDeleted] = useState<string[]>([]);
const getTenantConfigurationInfo = useCallback(() => {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant?.namespace}/tenants/${tenant?.name}/configuration`
)
.then((res: ITenantConfigurationResponse) => {
if (res.environmentVariables) {
setEnvVars(res.environmentVariables);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
}, [tenant, dispatch]);
useEffect(() => {
if (tenant) {
getTenantConfigurationInfo();
}
}, [tenant, getTenantConfigurationInfo]);
const updateTenantConfiguration = () => {
setIsSending(true);
let payload: ITenantConfigurationRequest = {
environmentVariables: envVars.filter((env) => env.key !== ""),
keysToBeDeleted: envVarsToBeDeleted,
};
api
.invoke(
"PATCH",
`/api/v1/namespaces/${tenant?.namespace}/tenants/${tenant?.name}/configuration`,
payload
)
.then(() => {
setIsSending(false);
setDialogOpen(false);
getTenantConfigurationInfo();
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setIsSending(false);
});
};
return (
<React.Fragment>
<ConfirmDialog
title={"Save and Restart"}
confirmText={"Restart"}
cancelText="Cancel"
titleIcon={<ConfirmModalIcon />}
isLoading={isSending}
onClose={() => setDialogOpen(false)}
isOpen={dialogOpen}
onConfirm={updateTenantConfiguration}
confirmationContent={
<DialogContentText>
Are you sure you want to save the changes and restart the service?
</DialogContentText>
}
/>
{loadingTenant ? (
<div className={classes.loaderAlign}>
<Loader />
</div>
) : (
<Grid container spacing={1}>
<Grid item xs={12}>
<h1 className={classes.sectionTitle}>Configuration</h1>
<hr className={classes.hrClass} />
</Grid>
<Grid container spacing={1}>
{envVars.map((envVar, index) => (
<Grid
item
xs={12}
className={`${classes.formFieldRow} ${classes.envVarRow}`}
key={`tenant-envVar-${index.toString()}`}
>
<Grid item xs={5} className={classes.fileItem}>
<InputBoxWrapper
id="env_var_key"
name="env_var_key"
label="Key"
value={envVar.key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const existingEnvVars = [...envVars];
setEnvVars(
existingEnvVars.map((keyPair, i) =>
i === index
? { key: e.target.value, value: keyPair.value }
: keyPair
)
);
}}
index={index}
key={`env_var_key_${index.toString()}`}
/>
</Grid>
<Grid item xs={5} className={classes.fileItem}>
<InputBoxWrapper
id="env_var_value"
name="env_var_value"
label="Value"
value={envVar.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const existingEnvVars = [...envVars];
setEnvVars(
existingEnvVars.map((keyPair, i) =>
i === index
? { key: keyPair.key, value: e.target.value }
: keyPair
)
);
}}
index={index}
key={`env_var_value_${index.toString()}`}
type={
MinIOEnvVarsSettings[envVar.key] &&
MinIOEnvVarsSettings[envVar.key].secret
? "password"
: "text"
}
/>
</Grid>
<Grid item xs={2} className={classes.rowActions}>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const existingEnvVars = [...envVars];
existingEnvVars.push({ key: "", value: "" });
setEnvVars(existingEnvVars);
}}
disabled={index !== envVars.length - 1}
>
<AddIcon />
</IconButton>
</div>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const existingEnvVars = envVars.filter(
(item, fIndex) => fIndex !== index
);
setEnvVars(existingEnvVars);
setEnvVarsToBeDeleted([
...envVarsToBeDeleted,
envVar.key,
]);
}}
disabled={envVars.length <= 1}
>
<RemoveIcon />
</IconButton>
</div>
</Grid>
</Grid>
))}
</Grid>
<Grid
item
xs={12}
sx={{ display: "flex", justifyContent: "flex-end" }}
>
<Button
id={"save-environment-variables"}
type="submit"
variant="callAction"
disabled={dialogOpen || isSending}
onClick={() => setDialogOpen(true)}
label={"Save"}
/>
</Grid>
</Grid>
)}
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.loadingTenant,
selectedTenant: state.tenants.currentTenant,
tenant: state.tenants.tenantInfo,
});
const connector = connect(mapState, null);
export default withStyles(styles)(connector(TenantConfiguration));

View File

@@ -89,6 +89,9 @@ const PodDetails = withSuspense(React.lazy(() => import("./pods/PodDetails")));
const EditTenantMonitoringScreen = withSuspense(
React.lazy(() => import("./EditTenantMonitoringScreen"))
);
const TenantConfiguration = withSuspense(
React.lazy(() => import("./TenantConfiguration"))
);
interface ITenantDetailsProps {
classes: any;
@@ -366,6 +369,10 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
<div className={classes.contentSpacer}>
<Routes>
<Route path={"summary"} element={<TenantSummary />} />
<Route
path={"configuration"}
element={<TenantConfiguration />}
/>
<Route path={`summary/yaml`} element={<TenantYAML />} />
<Route path={"metrics"} element={<TenantMetrics />} />
<Route path={"trace"} element={<TenantTrace />} />
@@ -408,6 +415,14 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
to: getRoutePath("summary"),
},
}}
{{
tabConfig: {
label: "Configuration",
value: "configuration",
component: Link,
to: getRoutePath("configuration"),
},
}}
{{
tabConfig: {
label: "Metrics",

View File

@@ -40,6 +40,15 @@ export interface ICustomCertificates {
consoleCAs: ICertificateInfo[];
}
export interface ITenantConfigurationResponse {
environmentVariables: LabelKeyPair[];
}
export interface ITenantConfigurationRequest {
environmentVariables: LabelKeyPair[];
keysToBeDeleted: string[];
}
export interface ITenantSecurityResponse {
autoCert: boolean;
customCertificates: ICustomCertificates;

View File

@@ -414,6 +414,57 @@ paths:
tags:
- OperatorAPI
/namespaces/{namespace}/tenants/{tenant}/configuration:
get:
summary: Tenant Configuration
operationId: TenantConfiguration
parameters:
- name: namespace
in: path
required: true
type: string
- name: tenant
in: path
required: true
type: string
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/tenantConfigurationResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- OperatorAPI
patch:
summary: Update Tenant Configuration
operationId: UpdateTenantConfiguration
parameters:
- name: namespace
in: path
required: true
type: string
- name: tenant
in: path
required: true
type: string
- name: body
in: body
required: true
schema:
$ref: "#/definitions/updateTenantConfigurationRequest"
responses:
204:
description: A successful response.
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- OperatorAPI
/namespaces/{namespace}/tenants/{tenant}/security:
get:
summary: Tenant Security
@@ -1683,6 +1734,26 @@ definitions:
type: integer
format: int64
tenantConfigurationResponse:
type: object
properties:
environmentVariables:
type: array
items:
$ref: "#/definitions/environmentVariable"
updateTenantConfigurationRequest:
type: object
properties:
keysToBeDeleted:
type: array
items:
type: string
environmentVariables:
type: array
items:
$ref: "#/definitions/environmentVariable"
tenantSecurityResponse:
type: object
properties: