Multiple fixes for operator-ui (#948)

- fix: create tenant from operator-ui was broken due to migration from
  standalone console to embedded console
- fix: refresh, activate and attach license in subscription page was
  broken
- fix: tenant usage report in operator-ui
- fix: show tenant encryption enabled if MINIO_KMS_SECRET_KEY is present

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2021-08-18 13:23:03 -07:00
committed by GitHub
parent a09be99ae6
commit ec47df3cc1
46 changed files with 672 additions and 1948 deletions

View File

@@ -56,12 +56,6 @@ var (
GlobalTLSCertsManager *xcerts.Manager
)
var consoleImage string
func init() {
consoleImage = env.Get(ConsoleOperatorConsoleImage, ConsoleImageDefaultVersion)
}
// getK8sSAToken assumes the plugin is running inside a k8s pod and extract the current service account from the
// /var/run/secrets/kubernetes.io/serviceaccount/token file
func getK8sSAToken() string {
@@ -71,6 +65,3 @@ func getK8sSAToken() string {
}
return string(dat)
}
func getConsoleImage() string {
return consoleImage
}

View File

@@ -93,7 +93,7 @@ func configureAPI(api *operations.OperatorAPI) http.Handler {
// Namespaces handlers
registerNamespaceHandlers(api)
// Subscription handlers
registerOperatorSubscriptionHandlers(api)
registerSubscriptionHandlers(api)
api.PreServerShutdown = func() {}

View File

@@ -18,37 +18,10 @@ package operatorapi
// list of all console environment constants
const (
// Constants for common configuration
ConsoleMinIOServer = "CONSOLE_MINIO_SERVER"
ConsoleMinIORegion = "CONSOLE_MINIO_REGION"
ConsoleHostname = "CONSOLE_HOSTNAME"
ConsolePort = "CONSOLE_PORT"
ConsoleTLSHostname = "CONSOLE_TLS_HOSTNAME"
ConsoleTLSPort = "CONSOLE_TLS_PORT"
ConsoleSubnetLicense = "CONSOLE_SUBNET_LICENSE"
// Constants for Secure middleware
ConsoleSecureAllowedHosts = "CONSOLE_SECURE_ALLOWED_HOSTS"
ConsoleSecureAllowedHostsAreRegex = "CONSOLE_SECURE_ALLOWED_HOSTS_ARE_REGEX"
ConsoleSecureFrameDeny = "CONSOLE_SECURE_FRAME_DENY"
ConsoleSecureContentTypeNoSniff = "CONSOLE_SECURE_CONTENT_TYPE_NO_SNIFF"
ConsoleSecureBrowserXSSFilter = "CONSOLE_SECURE_BROWSER_XSS_FILTER"
ConsoleSecureContentSecurityPolicy = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY"
ConsoleSecureContentSecurityPolicyReportOnly = "CONSOLE_SECURE_CONTENT_SECURITY_POLICY_REPORT_ONLY"
ConsoleSecureHostsProxyHeaders = "CONSOLE_SECURE_HOSTS_PROXY_HEADERS"
ConsoleSecureSTSSeconds = "CONSOLE_SECURE_STS_SECONDS"
ConsoleSecureSTSIncludeSubdomains = "CONSOLE_SECURE_STS_INCLUDE_SUB_DOMAINS"
ConsoleSecureSTSPreload = "CONSOLE_SECURE_STS_PRELOAD"
ConsoleSecureTLSRedirect = "CONSOLE_SECURE_TLS_REDIRECT"
ConsoleSecureTLSHost = "CONSOLE_SECURE_TLS_HOST"
ConsoleSecureTLSTemporaryRedirect = "CONSOLE_SECURE_TLS_TEMPORARY_REDIRECT"
ConsoleSecureForceSTSHeader = "CONSOLE_SECURE_FORCE_STS_HEADER"
ConsoleSecurePublicKey = "CONSOLE_SECURE_PUBLIC_KEY"
ConsoleSecureReferrerPolicy = "CONSOLE_SECURE_REFERRER_POLICY"
ConsoleSecureFeaturePolicy = "CONSOLE_SECURE_FEATURE_POLICY"
ConsoleSecureExpectCTHeader = "CONSOLE_SECURE_EXPECT_CT_HEADER"
ConsoleOperatorSAToken = "CONSOLE_OPERATOR_SA_TOKEN"
ConsoleOperatorConsoleImage = "CONSOLE_OPERATOR_CONSOLE_IMAGE"
ConsoleSubnetLicense = "CONSOLE_SUBNET_LICENSE"
ConsoleOperatorSAToken = "CONSOLE_OPERATOR_SA_TOKEN"
ConsoleOperatorConsoleImage = "CONSOLE_OPERATOR_CONSOLE_IMAGE"
MinIOSubnetLicense = "MINIO_SUBNET_LICENSE"
// Constants for prometheus annotations
prometheusPath = "prometheus.io/path"

View File

@@ -1448,21 +1448,6 @@ func init() {
}
}
},
"consoleConfiguration": {
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
{
"type": "object",
"properties": {
"image": {
"type": "string"
}
}
}
]
},
"createTenantRequest": {
"type": "object",
"required": [
@@ -1480,13 +1465,6 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/consoleConfiguration"
},
"console_image": {
"type": "string"
},
"enable_console": {
"type": "boolean",
"default": true
@@ -1570,6 +1548,9 @@ func init() {
"items": {
"$ref": "#/definitions/tenantResponseItem"
}
},
"externalIDP": {
"type": "boolean"
}
}
},
@@ -2716,15 +2697,6 @@ func init() {
"tenant": {
"type": "object",
"properties": {
"consoleEnabled": {
"type": "boolean"
},
"consoleTLS": {
"type": "boolean"
},
"console_image": {
"type": "string"
},
"creation_date": {
"type": "string"
},
@@ -2874,18 +2846,6 @@ func init() {
"customCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"consoleCAs": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"minio": {
"type": "array",
"items": {
@@ -2956,16 +2916,6 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"console_ca_certificates": {
"type": "array",
"items": {
"type": "string"
}
},
"minio": {
"type": "array",
"items": {
@@ -2977,10 +2927,6 @@ func init() {
"updateTenantRequest": {
"type": "object",
"properties": {
"console_image": {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
},
"enable_prometheus": {
"type": "boolean"
},
@@ -3005,18 +2951,6 @@ func init() {
"customCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/keyPairConfiguration"
}
},
"consoleCAs": {
"type": "array",
"items": {
"type": "string"
}
},
"minio": {
"type": "array",
"items": {
@@ -5046,18 +4980,6 @@ func init() {
"TenantSecurityResponseCustomCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"consoleCAs": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"minio": {
"type": "array",
"items": {
@@ -5075,18 +4997,6 @@ func init() {
"UpdateTenantSecurityRequestCustomCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/keyPairConfiguration"
}
},
"consoleCAs": {
"type": "array",
"items": {
"type": "string"
}
},
"minio": {
"type": "array",
"items": {
@@ -5217,21 +5127,6 @@ func init() {
}
}
},
"consoleConfiguration": {
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
{
"type": "object",
"properties": {
"image": {
"type": "string"
}
}
}
]
},
"createTenantRequest": {
"type": "object",
"required": [
@@ -5249,13 +5144,6 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/consoleConfiguration"
},
"console_image": {
"type": "string"
},
"enable_console": {
"type": "boolean",
"default": true
@@ -5339,6 +5227,9 @@ func init() {
"items": {
"$ref": "#/definitions/tenantResponseItem"
}
},
"externalIDP": {
"type": "boolean"
}
}
},
@@ -6338,15 +6229,6 @@ func init() {
"tenant": {
"type": "object",
"properties": {
"consoleEnabled": {
"type": "boolean"
},
"consoleTLS": {
"type": "boolean"
},
"console_image": {
"type": "string"
},
"creation_date": {
"type": "string"
},
@@ -6496,18 +6378,6 @@ func init() {
"customCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"consoleCAs": {
"type": "array",
"items": {
"$ref": "#/definitions/certificateInfo"
}
},
"minio": {
"type": "array",
"items": {
@@ -6578,16 +6448,6 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"console_ca_certificates": {
"type": "array",
"items": {
"type": "string"
}
},
"minio": {
"type": "array",
"items": {
@@ -6599,10 +6459,6 @@ func init() {
"updateTenantRequest": {
"type": "object",
"properties": {
"console_image": {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
},
"enable_prometheus": {
"type": "boolean"
},
@@ -6627,18 +6483,6 @@ func init() {
"customCertificates": {
"type": "object",
"properties": {
"console": {
"type": "array",
"items": {
"$ref": "#/definitions/keyPairConfiguration"
}
},
"consoleCAs": {
"type": "array",
"items": {
"type": "string"
}
},
"minio": {
"type": "array",
"items": {

View File

@@ -37,6 +37,7 @@ type K8sClientI interface {
deletePodCollection(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
deleteSecret(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error
createSecret(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)
updateSecret(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)
}
// Interface implementation
@@ -70,6 +71,10 @@ func (c *k8sClient) createSecret(ctx context.Context, namespace string, secret *
return c.client.CoreV1().Secrets(namespace).Create(ctx, secret, opts)
}
func (c *k8sClient) updateSecret(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error) {
return c.client.CoreV1().Secrets(namespace).Update(ctx, secret, opts)
}
func (c *k8sClient) getNamespace(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Namespace, error) {
return c.client.CoreV1().Namespaces().Get(ctx, name, opts)
}

View File

@@ -20,13 +20,12 @@ package operatorapi
import (
"context"
"errors"
"fmt"
"time"
"github.com/minio/console/pkg/subnet"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/minio/console/restapi"
@@ -38,10 +37,10 @@ import (
"github.com/minio/console/operatorapi/operations/operator_api"
)
func registerOperatorSubscriptionHandlers(api *operations.OperatorAPI) {
func registerSubscriptionHandlers(api *operations.OperatorAPI) {
// Activate license subscription for a particular tenant
api.OperatorAPISubscriptionActivateHandler = operator_api.SubscriptionActivateHandlerFunc(func(params operator_api.SubscriptionActivateParams, session *models.Principal) middleware.Responder {
err := getOperatorSubscriptionActivateResponse(session, params.Namespace, params.Tenant)
err := getSubscriptionActivateResponse(session, params.Namespace, params.Tenant)
if err != nil {
return operator_api.NewSubscriptionActivateDefault(int(err.Code)).WithPayload(err)
}
@@ -55,10 +54,33 @@ func registerOperatorSubscriptionHandlers(api *operations.OperatorAPI) {
}
return operator_api.NewSubscriptionRefreshOK().WithPayload(license)
})
// Validate subscription handler
api.OperatorAPISubscriptionValidateHandler = operator_api.SubscriptionValidateHandlerFunc(func(params operator_api.SubscriptionValidateParams, session *models.Principal) middleware.Responder {
license, err := getSubscriptionValidateResponse(session, params.Body)
if err != nil {
return operator_api.NewSubscriptionValidateDefault(int(err.Code)).WithPayload(err)
}
return operator_api.NewSubscriptionValidateOK().WithPayload(license)
})
// Get subscription information handler
api.OperatorAPISubscriptionInfoHandler = operator_api.SubscriptionInfoHandlerFunc(func(params operator_api.SubscriptionInfoParams, session *models.Principal) middleware.Responder {
license, err := getSubscriptionInfoResponse(session)
if err != nil {
return operator_api.NewSubscriptionInfoDefault(int(err.Code)).WithPayload(err)
}
return operator_api.NewSubscriptionInfoOK().WithPayload(license)
})
// Refresh license for k8s cluster
api.OperatorAPISubscriptionRefreshHandler = operator_api.SubscriptionRefreshHandlerFunc(func(params operator_api.SubscriptionRefreshParams, session *models.Principal) middleware.Responder {
license, err := getSubscriptionRefreshResponse(session)
if err != nil {
return operator_api.NewSubscriptionRefreshDefault(int(err.Code)).WithPayload(err)
}
return operator_api.NewSubscriptionRefreshOK().WithPayload(license)
})
}
// retrieveLicense returns license from K8S secrets (If console is deployed in operator mode) or from
// the configured CONSOLE_SUBNET_LICENSE environment variable
// retrieveLicense returns license from K8S secrets
func retrieveLicense(ctx context.Context, sessionToken string) (string, error) {
var license string
@@ -79,46 +101,6 @@ func retrieveLicense(ctx context.Context, sessionToken string) (string, error) {
return license, nil
}
func getOperatorSubscriptionActivateResponse(session *models.Principal, namespace, tenant string) *models.Error {
// 20 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(restapi.ErrorGeneric, nil, err)
}
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(restapi.ErrorGeneric, nil, err)
}
opClient := &operatorClient{
client: opClientClientSet,
}
minTenant, err := getTenant(ctx, opClient, namespace, tenant)
if err != nil {
return prepareError(err, restapi.ErrorGeneric)
}
// If console is not deployed for this tenant return an error
if minTenant.Spec.Console == nil {
return prepareError(restapi.ErrorGenericNotFound)
}
// configure kubernetes client
k8sClient := k8sClient{
client: clientSet,
}
// Get cluster subscription license
license, err := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName)
if err != nil {
return prepareError(errInvalidCredentials, nil, err)
}
// add subscription license to existing console Tenant
if err = addSubscriptionLicenseToTenant(ctx, &k8sClient, license, namespace, tenant, minTenant.Spec.Console.ConsoleSecret.Name); err != nil {
return prepareError(err, restapi.ErrorGeneric)
}
return nil
}
// getSubscriptionLicense will retrieve stored license jwt from k8s secret
func getSubscriptionLicense(ctx context.Context, clientSet K8sClientI, namespace, secretName string) (string, error) {
// retrieve license stored in k8s
@@ -135,86 +117,66 @@ func getSubscriptionLicense(ctx context.Context, clientSet K8sClientI, namespace
}
// addSubscriptionLicenseToTenant replace existing console tenant secret and adds the subnet license key
func addSubscriptionLicenseToTenant(ctx context.Context, clientSet K8sClientI, license, namespace, tenantName, secretName string) error {
// Retrieve console secret for Tenant
consoleSecret, err := clientSet.getSecret(ctx, namespace, secretName, metav1.GetOptions{})
if err != nil {
return err
}
// Copy current console secret
dataNewSecret := consoleSecret.Data
// Add subnet license to the new console secret
dataNewSecret[ConsoleSubnetLicense] = []byte(license)
// Delete existing console secret
err = clientSet.deleteSecret(ctx, namespace, secretName, metav1.DeleteOptions{})
if err != nil {
return err
}
// Prepare the new Console Secret
imm := true
newConsoleSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: dataNewSecret,
}
// Create new Console secret with the subnet License
_, err = clientSet.createSecret(ctx, namespace, newConsoleSecret, metav1.CreateOptions{})
if err != nil {
return err
}
// restart Console pods based on label:
// v1.min.io/console: TENANT-console
err = clientSet.deletePodCollection(ctx, namespace, metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s%s", miniov2.ConsoleTenantLabel, tenantName, miniov2.ConsoleName),
})
if err != nil {
return err
}
return nil
}
// updateTenantLicenseAndRestartConsole
func updateTenantLicenseAndRestartConsole(ctx context.Context, clientSet K8sClientI, license, namespace, tenantName string) error {
consoleSelector := fmt.Sprintf("%s-console", tenantName)
consoleSecretName := fmt.Sprintf("%s-secret", consoleSelector)
// read current console configuration from k8s secrets
currentConsoleSecret, err := clientSet.getSecret(ctx, namespace, consoleSecretName, metav1.GetOptions{})
if err != nil || currentConsoleSecret == nil {
return err
}
secretData := currentConsoleSecret.Data
secretData[ConsoleSubnetLicense] = []byte(license)
// delete existing console configuration from k8s secrets
err = clientSet.deleteSecret(ctx, namespace, consoleSecretName, metav1.DeleteOptions{})
if err != nil {
// log the error if any and continue
LogError("unable to delete secret %s: %v", consoleSecretName, err)
}
// Save subnet license in k8s secrets
imm := true
consoleConfigSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: consoleSecretName,
},
Immutable: &imm,
Data: secretData,
}
_, err = clientSet.createSecret(ctx, namespace, consoleConfigSecret, metav1.CreateOptions{})
if err != nil {
return err
}
// restart Console pods based on label:
// v1.min.io/console: TENANT-console
err = clientSet.deletePodCollection(ctx, namespace, metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s%s", miniov2.ConsoleTenantLabel, tenantName, miniov2.ConsoleName),
})
if err != nil {
return err
func addSubscriptionLicenseToTenant(ctx context.Context, clientSet K8sClientI, opClient OperatorClientI, license string, tenant *miniov2.Tenant) error {
// If Tenant has a configuration secret update the license there and MinIO pods doesn't need to get restarted
if tenant.HasConfigurationSecret() {
// Update the Tenant Configuration
tenantConfigurationSecret, err := clientSet.getSecret(ctx, tenant.Namespace, tenant.Spec.Configuration.Name, metav1.GetOptions{})
if err != nil {
return err
}
if _, ok := tenantConfigurationSecret.Data["config.env"]; ok {
updatedTenantConfiguration := map[string]string{}
tenantConfigurationMap := miniov2.ParseRawConfiguration(tenantConfigurationSecret.Data["config.env"])
for key, val := range tenantConfigurationMap {
updatedTenantConfiguration[key] = string(val)
}
updatedTenantConfiguration[MinIOSubnetLicense] = license
// removing accesskey & secretkey that are added automatically by parsing function
// and are not need it for the tenant itself
delete(updatedTenantConfiguration, "accesskey")
delete(updatedTenantConfiguration, "secretkey")
tenantConfigurationSecret.Data = map[string][]byte{
"config.env": []byte(GenerateTenantConfigurationFile(updatedTenantConfiguration)),
}
_, err = clientSet.updateSecret(ctx, tenant.Namespace, tenantConfigurationSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
} else {
return errors.New("tenant configuration secret has wrong format")
}
} else {
// If configuration file is not present set the license to the container env
updatedTenant := tenant.DeepCopy()
// reset container env vars
updatedTenant.Spec.Env = []corev1.EnvVar{}
var licenseIsSet bool
for _, env := range tenant.GetEnvVars() {
// check if license already exists and override
if env.Name == MinIOSubnetLicense {
updatedTenant.Spec.Env = append(updatedTenant.Spec.Env, corev1.EnvVar{
Name: MinIOSubnetLicense,
Value: license,
})
licenseIsSet = true
} else {
// copy existing container env variables
updatedTenant.Spec.Env = append(updatedTenant.Spec.Env, env)
}
}
// if license didnt exists append it
if !licenseIsSet {
updatedTenant.Spec.Env = append(updatedTenant.Spec.Env, corev1.EnvVar{
Name: MinIOSubnetLicense,
Value: license,
})
}
// this will start MinIO pods rolling restart
_, err := opClient.TenantUpdate(ctx, updatedTenant, metav1.UpdateOptions{})
if err != nil {
return err
}
}
return nil
}
@@ -226,7 +188,7 @@ func getSubscriptionRefreshResponse(session *models.Principal) (*models.License,
client := &cluster.HTTPClient{
Client: restapi.GetConsoleHTTPClient(),
}
licenseKey, err := retrieveLicense(context.Background(), session.STSSessionToken)
licenseKey, err := retrieveLicense(ctx, session.STSSessionToken)
if err != nil {
return nil, prepareError(errLicenseNotFound, nil, err)
}
@@ -251,17 +213,17 @@ func getSubscriptionRefreshResponse(session *models.Principal) (*models.License,
if err != nil {
return nil, prepareError(err)
}
opClient := &operatorClient{
opClient := operatorClient{
client: opClientClientSet,
}
tenants, err := listTenants(ctx, opClient, "", nil)
// iterate over all tenants and update licenses
tenants, err := opClient.TenantList(ctx, "", metav1.ListOptions{})
if err != nil {
return nil, prepareError(err)
}
// iterate over all tenants, update console configuration and restart console pods
for _, tenant := range tenants.Tenants {
if err := updateTenantLicenseAndRestartConsole(ctx, &k8sClient, licenseRaw, tenant.Namespace, tenant.Name); err != nil {
LogError("unable to updateTenantLicenseAndRestartConsole: %v", err)
for _, tenant := range tenants.Items {
if err = addSubscriptionLicenseToTenant(ctx, &k8sClient, &opClient, licenseRaw, &tenant); err != nil {
return nil, prepareError(err)
}
}
@@ -321,24 +283,31 @@ func subscriptionRefresh(httpClient *cluster.HTTPClient, license string) (*model
// saveSubscriptionLicense will create or replace an existing subnet license secret in the k8s cluster
func saveSubscriptionLicense(ctx context.Context, clientSet K8sClientI, license string) error {
// Delete subnet license secret if exists
err := clientSet.deleteSecret(ctx, cluster.Namespace, OperatorSubnetLicenseSecretName, metav1.DeleteOptions{})
licenseSecret, err := clientSet.getSecret(ctx, cluster.Namespace, OperatorSubnetLicenseSecretName, metav1.GetOptions{})
if err != nil {
// log the error if any and continue
LogError("unable to delete secret %s: %v", OperatorSubnetLicenseSecretName, err)
if k8serrors.IsNotFound(err) {
// Save subnet license in k8s secrets
licenseSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
},
Data: map[string][]byte{
ConsoleSubnetLicense: []byte(license),
},
}
_, err = clientSet.createSecret(ctx, cluster.Namespace, licenseSecret, metav1.CreateOptions{})
if err != nil {
return err
}
return nil
}
return err
}
// Save subnet license in k8s secrets
imm := true
licenseSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
},
Immutable: &imm,
Data: map[string][]byte{
ConsoleSubnetLicense: []byte(license),
},
// update existing license
licenseSecret.Data = map[string][]byte{
ConsoleSubnetLicense: []byte(license),
}
_, err = clientSet.createSecret(ctx, cluster.Namespace, licenseSecret, metav1.CreateOptions{})
_, err = clientSet.updateSecret(ctx, cluster.Namespace, licenseSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
@@ -360,3 +329,88 @@ func subscriptionValidate(client cluster.HTTPClientI, license, email, password s
Organization: licenseInfo.Organization,
}, rawLicense, nil
}
// getSubscriptionValidateResponse
func getSubscriptionValidateResponse(session *models.Principal, params *models.SubscriptionValidateRequest) (*models.License, *models.Error) {
// 20 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
client := &cluster.HTTPClient{
Client: restapi.GetConsoleHTTPClient(),
}
// validate license key
licenseInfo, license, err := subscriptionValidate(client, params.License, params.Email, params.Password)
if err != nil {
return nil, prepareError(errInvalidLicense, nil, err)
}
// configure kubernetes client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
k8sClient := k8sClient{
client: clientSet,
}
if err != nil {
return nil, prepareError(errorGeneric, nil, err)
}
// save license key to k8s
if err = saveSubscriptionLicense(ctx, &k8sClient, license); err != nil {
return nil, prepareError(errorGeneric, nil, err)
}
return licenseInfo, nil
}
func getSubscriptionActivateResponse(session *models.Principal, namespace, tenantName string) *models.Error {
// 20 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return prepareError(errorGeneric, nil, err)
}
clientSet, err := cluster.K8sClient(session.STSSessionToken)
if err != nil {
return prepareError(errorGeneric, nil, err)
}
opClient := operatorClient{
client: opClientClientSet,
}
tenant, err := getTenant(ctx, &opClient, namespace, tenantName)
if err != nil {
return prepareError(err, errorGeneric)
}
// configure kubernetes client
k8sClient := k8sClient{
client: clientSet,
}
// Get cluster subscription license
license, err := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName)
if err != nil {
return prepareError(errInvalidCredentials, nil, err)
}
// add subscription license to existing console Tenant
if err = addSubscriptionLicenseToTenant(ctx, &k8sClient, &opClient, license, tenant); err != nil {
return prepareError(err, errorGeneric)
}
return nil
}
// getSubscriptionInfoResponse returns information about the current configured subnet license for Console
func getSubscriptionInfoResponse(session *models.Principal) (*models.License, *models.Error) {
// 20 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
var licenseInfo *models.License
client := &cluster.HTTPClient{
Client: restapi.GetConsoleHTTPClient(),
}
licenseKey, err := retrieveLicense(ctx, session.STSSessionToken)
if err != nil {
return nil, prepareError(errLicenseNotFound, nil, err)
}
// validate license key and obtain license info
licenseInfo, _, err = subscriptionValidate(client, licenseKey, "", "")
if err != nil {
return nil, prepareError(errLicenseNotFound, nil, err)
}
return licenseInfo, nil
}

View File

@@ -20,6 +20,8 @@ import (
"context"
"testing"
v2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
"errors"
corev1 "k8s.io/api/core/v1"
@@ -28,13 +30,17 @@ import (
func Test_addSubscriptionLicenseToTenant(t *testing.T) {
k8sClient := k8sClientMock{}
opClient := opClientMock{}
tenant := &v2.Tenant{
ObjectMeta: metav1.ObjectMeta{},
Spec: v2.TenantSpec{},
}
type args struct {
ctx context.Context
clientSet K8sClientI
license string
namespace string
tenantName string
secretName string
ctx context.Context
clientSet K8sClientI
opClient OperatorClientI
license string
tenant *v2.Tenant
}
tests := []struct {
name string
@@ -43,150 +49,158 @@ func Test_addSubscriptionLicenseToTenant(t *testing.T) {
mockFunc func()
}{
{
name: "error because subnet license doesnt exists",
name: "success updating subscription for tenant with configuration file",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
license: "",
namespace: "",
tenantName: "",
secretName: "subnet-license",
},
wantErr: true,
mockFunc: func() {
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("something went wrong")
}
},
},
{
name: "error because existing license could not be deleted",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
license: "",
namespace: "",
tenantName: "",
secretName: OperatorSubnetLicenseSecretName,
},
wantErr: true,
mockFunc: func() {
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
imm := true
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
},
Immutable: &imm,
Data: map[string][]byte{
ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"),
},
}, nil
}
DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error {
return errors.New("something went wrong")
}
},
},
{
name: "error because unable to create new subnet license",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
license: "",
namespace: "",
tenantName: "",
secretName: OperatorSubnetLicenseSecretName,
},
wantErr: true,
mockFunc: func() {
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
imm := true
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
},
Immutable: &imm,
Data: map[string][]byte{
ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"),
},
}, nil
}
DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error {
return nil
}
CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
return nil, errors.New("something went wrong")
}
},
},
{
name: "error because unable to delete pod collection",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
license: "",
namespace: "",
tenantName: "",
secretName: OperatorSubnetLicenseSecretName,
},
wantErr: true,
mockFunc: func() {
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
imm := true
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
},
Immutable: &imm,
Data: map[string][]byte{
ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"),
},
}, nil
}
DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error {
return nil
}
CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
return nil, nil
}
DeletePodCollectionMock = func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return errors.New("something went wrong")
}
},
},
{
name: "subscription updated successfully",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
license: "",
namespace: "",
tenantName: "",
secretName: OperatorSubnetLicenseSecretName,
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: false,
mockFunc: func() {
tenant.Spec.Configuration = &corev1.LocalObjectReference{
Name: "minio-configuration",
}
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
imm := true
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: OperatorSubnetLicenseSecretName,
Name: "minio-configuration",
},
Immutable: &imm,
Data: map[string][]byte{
ConsoleSubnetLicense: []byte("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I"),
"config.env": []byte("export MINIO_SUBNET_LICENSE=\"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I\""),
},
}, nil
}
DeleteSecretMock = func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error {
return nil
}
CreateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
UpdateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
}
DeletePodCollectionMock = func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return nil
},
},
{
name: "error updating subscription for tenant because cannot get configuration file",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: true,
mockFunc: func() {
tenant.Spec.Configuration = &corev1.LocalObjectReference{
Name: "minio-configuration",
}
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("something wrong happened")
}
UpdateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
}
},
},
{
name: "error updating subscription for tenant because configuration file has wrong format",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: true,
mockFunc: func() {
tenant.Spec.Configuration = &corev1.LocalObjectReference{
Name: "minio-configuration",
}
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "minio-configuration",
},
Data: map[string][]byte{
"aaaaa": []byte("export MINIO_SUBNET_LICENSE=\"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I\""),
},
}, nil
}
UpdateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, nil
}
},
},
{
name: "error updating subscription for tenant because cannot update configuration file",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: true,
mockFunc: func() {
tenant.Spec.Configuration = &corev1.LocalObjectReference{
Name: "minio-configuration",
}
k8sclientGetSecretMock = func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "minio-configuration",
},
Data: map[string][]byte{
"config.env": []byte("export MINIO_SUBNET_LICENSE=\"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsZW5pbitjMUBtaW5pby5pbyIsInRlYW1OYW1lIjoiY29uc29sZS1jdXN0b21lciIsImV4cCI6MS42Mzk5NTI2MTE2MDkxNDQ3MzJlOSwiaXNzIjoic3VibmV0QG1pbmlvLmlvIiwiY2FwYWNpdHkiOjI1LCJpYXQiOjEuNjA4NDE2NjExNjA5MTQ0NzMyZTksImFjY291bnRJZCI6MTc2LCJzZXJ2aWNlVHlwZSI6IlNUQU5EQVJEIn0.ndtf8V_FJTvhXeemVLlORyDev6RJaSPhZ2djkMVK9SvXD0srR_qlYJATPjC4NljkS71nXMGVDov5uCTuUL97x6FGQEKDruA-z24x_2Zr8kof4LfBb3HUHudCR8QvE--I\""),
},
}, nil
}
UpdateSecretMock = func(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
return nil, errors.New("something wrong happened")
}
},
},
{
name: "success updating subscription for tenant with env variable",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: false,
mockFunc: func() {
tenant.Spec.Env = []corev1.EnvVar{
{
Name: "MINIO_SUBNET_LICENSE",
Value: "",
ValueFrom: nil,
},
}
opClientTenantUpdateMock = func(ctx context.Context, tenant *v2.Tenant, opts metav1.UpdateOptions) (*v2.Tenant, error) {
return nil, nil
}
},
},
{
name: "error updating subscription for tenant with env variable because of update tenant error",
args: args{
ctx: context.Background(),
clientSet: k8sClient,
opClient: opClient,
license: "",
tenant: tenant,
},
wantErr: true,
mockFunc: func() {
tenant.Spec.Env = []corev1.EnvVar{
{
Name: "MINIO_SUBNET_LICENSE",
Value: "",
ValueFrom: nil,
},
}
opClientTenantUpdateMock = func(ctx context.Context, tenant *v2.Tenant, opts metav1.UpdateOptions) (*v2.Tenant, error) {
return nil, errors.New("something wrong happened")
}
},
},
@@ -196,7 +210,7 @@ func Test_addSubscriptionLicenseToTenant(t *testing.T) {
if tt.mockFunc != nil {
tt.mockFunc()
}
if err := addSubscriptionLicenseToTenant(tt.args.ctx, tt.args.clientSet, tt.args.license, tt.args.namespace, tt.args.tenantName, tt.args.secretName); (err != nil) != tt.wantErr {
if err := addSubscriptionLicenseToTenant(tt.args.ctx, tt.args.clientSet, tt.args.opClient, tt.args.license, tt.args.tenant); (err != nil) != tt.wantErr {
t.Errorf("addSubscriptionLicenseToTenant() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@@ -33,10 +33,6 @@ import (
"strings"
"time"
"github.com/minio/console/pkg/auth/ldap"
"github.com/minio/console/pkg/auth/idp/oauth2"
"github.com/dustin/go-humanize"
"github.com/minio/console/restapi"
@@ -342,7 +338,7 @@ func getTenantAdminClient(ctx context.Context, client K8sClientI, tenant *miniov
return nil, err
}
sessionToken := ""
mAdmin, pErr := restapi.NewAdminClientWithInsecure(svcURL, tenantCreds.accessKey, tenantCreds.secretKey, sessionToken, false)
mAdmin, pErr := restapi.NewAdminClientWithInsecure(svcURL, tenantCreds.accessKey, tenantCreds.secretKey, sessionToken, true)
if pErr != nil {
return nil, pErr.Cause
}
@@ -355,28 +351,21 @@ type tenantKeys struct {
}
func getTenantCreds(ctx context.Context, client K8sClientI, tenant *miniov2.Tenant) (*tenantKeys, error) {
if tenant == nil || tenant.Spec.CredsSecret == nil {
return nil, errors.New("invalid arguments")
}
// get admin credentials from secret
creds, err := client.getSecret(ctx, tenant.Namespace, tenant.Spec.CredsSecret.Name, metav1.GetOptions{})
tenantConfiguration, err := GetTenantConfiguration(ctx, client, tenant)
if err != nil {
return nil, err
}
tenantAccessKey, ok := creds.Data["accesskey"]
tenantAccessKey, ok := tenantConfiguration["accesskey"]
if !ok {
restapi.LogError("tenant's secret doesn't contain accesskey")
return nil, restapi.ErrorGeneric
}
tenantSecretKey, ok := creds.Data["secretkey"]
tenantSecretKey, ok := tenantConfiguration["secretkey"]
if !ok {
restapi.LogError("tenant's secret doesn't contain secretkey")
return nil, restapi.ErrorGeneric
}
// TODO:
// We need to avoid using minio root credentials to talk to tenants, and instead use a different user credentials
// when that its implemented we also need to check here if the tenant has LDAP enabled so we authenticate first against AD
return &tenantKeys{accessKey: string(tenantAccessKey), secretKey: string(tenantSecretKey)}, nil
return &tenantKeys{accessKey: tenantAccessKey, secretKey: tenantSecretKey}, nil
}
func getTenant(ctx context.Context, operatorClient OperatorClientI, namespace, tenantName string) (*miniov2.Tenant, error) {
@@ -407,7 +396,6 @@ func isPrometheusEnabled(annotations map[string]string) bool {
func getTenantInfo(tenant *miniov2.Tenant) *models.Tenant {
var pools []*models.Pool
consoleImage := ""
var totalSize int64
for _, p := range tenant.Spec.Pools {
pools = append(pools, parseTenantPool(&p))
@@ -419,10 +407,6 @@ func getTenantInfo(tenant *miniov2.Tenant) *models.Tenant {
deletion = tenant.ObjectMeta.DeletionTimestamp.Format(time.RFC3339)
}
if tenant.HasConsoleEnabled() {
consoleImage = tenant.Spec.Console.Image
}
return &models.Tenant{
CreationDate: tenant.ObjectMeta.CreationTimestamp.Format(time.RFC3339),
DeletionDate: deletion,
@@ -432,16 +416,14 @@ func getTenantInfo(tenant *miniov2.Tenant) *models.Tenant {
Pools: pools,
Namespace: tenant.ObjectMeta.Namespace,
Image: tenant.Spec.Image,
ConsoleImage: consoleImage,
EnablePrometheus: isPrometheusEnabled(tenant.Annotations),
}
}
func getTenantDetailsResponse(session *models.Principal, params operator_api.TenantDetailsParams) (*models.Tenant, *models.Error) {
// 5 seconds timeout
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
ctx := context.Background()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
@@ -468,32 +450,9 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
client: clientSet,
}
tenantConfiguration := map[string][]byte{}
for _, config := range minTenant.GetEnvVars() {
tenantConfiguration[config.Name] = []byte(config.Value)
}
if minTenant.HasCredsSecret() {
minioSecret, err := clientSet.CoreV1().Secrets(minTenant.Namespace).Get(ctx, minTenant.Spec.CredsSecret.Name, metav1.GetOptions{})
// we can tolerate not getting this secret
if err != nil {
restapi.LogError("unable to fetch existing secrets for %s: %v", minTenant.Name, err)
}
configFromCredsSecret := minioSecret.Data
for key, val := range configFromCredsSecret {
tenantConfiguration[key] = val
}
}
if minTenant.HasConfigurationSecret() {
minioConfigurationSecret, err := clientSet.CoreV1().Secrets(minTenant.Namespace).Get(ctx, minTenant.Spec.Configuration.Name, metav1.GetOptions{})
if err == nil {
configFromFile := miniov2.ParseRawConfiguration(minioConfigurationSecret.Data["config.env"])
for key, val := range configFromFile {
tenantConfiguration[key] = val
}
}
tenantConfiguration, err := GetTenantConfiguration(ctx, &k8sClient, minTenant)
if err != nil {
restapi.LogError("unable to fetch configuration for tenant %s: %v", minTenant.Name, err)
}
// detect if AD/LDAP is enabled
@@ -508,27 +467,26 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
oidcEnabled = true
}
// detect if encryption is enabled
if minTenant.HasKESEnabled() || string(tenantConfiguration["MINIO_KMS_SECRET_KEY"]) != "" {
info.EncryptionEnabled = true
}
info.LogEnabled = minTenant.HasLogEnabled()
info.MonitoringEnabled = minTenant.HasPrometheusEnabled()
info.EncryptionEnabled = minTenant.HasKESEnabled()
info.IdpAdEnabled = ldapEnabled
info.IdpOidcEnabled = oidcEnabled
info.MinioTLS = minTenant.TLS()
info.ConsoleTLS = minTenant.AutoCert() || minTenant.ConsoleExternalCert()
info.ConsoleEnabled = minTenant.HasConsoleEnabled()
if minTenant.Spec.Console != nil {
// obtain current subnet license for tenant (if exists)
license, _ := getSubscriptionLicense(context.Background(), &k8sClient, params.Namespace, minTenant.Spec.Console.ConsoleSecret.Name)
if license != "" {
client := &cluster.HTTPClient{
Client: restapi.GetConsoleHTTPClient(),
}
licenseInfo, _, _ := subscriptionValidate(client, license, "", "")
// if licenseInfo is present attach it to the tenantInfo response
if licenseInfo != nil {
info.SubnetLicense = licenseInfo
}
// obtain current subnet license for tenant (if exists)
if license, ok := tenantConfiguration[MinIOSubnetLicense]; ok {
client := &cluster.HTTPClient{
Client: restapi.GetConsoleHTTPClient(),
}
licenseInfo, _, _ := subscriptionValidate(client, string(license), "", "")
// if licenseInfo is present attach it to the tenantInfo response
if licenseInfo != nil {
info.SubnetLicense = licenseInfo
}
}
@@ -558,13 +516,11 @@ func getTenantDetailsResponse(session *models.Principal, params operator_api.Ten
schema := "http"
consoleSchema := "http"
consolePort := ":9090"
consolePort := fmt.Sprintf(":%d", miniov2.ConsolePort)
if minTenant.TLS() {
schema = "https"
}
if minTenant.AutoCert() || minTenant.ConsoleExternalCert() {
consoleSchema = "https"
consolePort = ":9443"
consolePort = fmt.Sprintf(":%d", miniov2.ConsoleTLSPort)
}
var minioEndpoint string
var consoleEndpoint string
@@ -646,8 +602,6 @@ func parseTenantCertificates(ctx context.Context, clientSet K8sClientI, namespac
func getTenantSecurity(ctx context.Context, clientSet K8sClientI, tenant *miniov2.Tenant) (response *models.TenantSecurityResponse, err error) {
var minioExternalCertificates []*models.CertificateInfo
var minioExternalCaCertificates []*models.CertificateInfo
var consoleExternalCertificates []*models.CertificateInfo
var consoleExternalCaCertificates []*models.CertificateInfo
// Certificates used by MinIO server
if minioExternalCertificates, err = parseTenantCertificates(ctx, clientSet, tenant.Namespace, tenant.Spec.ExternalCertSecret); err != nil {
return nil, err
@@ -656,25 +610,11 @@ func getTenantSecurity(ctx context.Context, clientSet K8sClientI, tenant *miniov
if minioExternalCaCertificates, err = parseTenantCertificates(ctx, clientSet, tenant.Namespace, tenant.Spec.ExternalCaCertSecret); err != nil {
return nil, err
}
if tenant.HasConsoleEnabled() {
// Certificate used by Console server
if tenant.Spec.Console.ExternalCertSecret != nil {
if consoleExternalCertificates, err = parseTenantCertificates(ctx, clientSet, tenant.Namespace, []*miniov2.LocalCertificateReference{tenant.Spec.Console.ExternalCertSecret}); err != nil {
return nil, err
}
}
// CA Certificates used by Console server
if consoleExternalCaCertificates, err = parseTenantCertificates(ctx, clientSet, tenant.Namespace, tenant.Spec.Console.ExternalCaCertSecret); err != nil {
return nil, err
}
}
return &models.TenantSecurityResponse{
AutoCert: tenant.AutoCert(),
CustomCertificates: &models.TenantSecurityResponseCustomCertificates{
Minio: minioExternalCertificates,
MinioCAs: minioExternalCaCertificates,
Console: consoleExternalCertificates,
ConsoleCAs: consoleExternalCaCertificates,
Minio: minioExternalCertificates,
MinioCAs: minioExternalCaCertificates,
},
}, nil
}
@@ -746,8 +686,6 @@ func updateTenantSecurity(ctx context.Context, operatorClient OperatorClientI, c
minInst.Spec.RequestAutoCert = &params.Body.AutoCert
var newMinIOExternalCertSecret []*miniov2.LocalCertificateReference
var newMinIOExternalCaCertSecret []*miniov2.LocalCertificateReference
var newConsoleExternalCertSecret *miniov2.LocalCertificateReference
var newConsoleExternalCaCertSecret []*miniov2.LocalCertificateReference
// Remove Certificate Secrets from MinIO (Tenant.Spec.ExternalCertSecret)
for _, certificate := range minInst.Spec.ExternalCertSecret {
skip := false
@@ -776,33 +714,6 @@ func updateTenantSecurity(ctx context.Context, operatorClient OperatorClientI, c
}
newMinIOExternalCaCertSecret = append(newMinIOExternalCaCertSecret, certificate)
}
if minInst.HasConsoleEnabled() {
// Remove Certificate Secrets from Console (Tenant.Spec.Console.ExternalCertSecret)
if minInst.ConsoleExternalCert() {
newConsoleExternalCertSecret = minInst.Spec.Console.ExternalCertSecret
for _, certificateToBeDeleted := range params.Body.CustomCertificates.SecretsToBeDeleted {
if newConsoleExternalCertSecret.Name == certificateToBeDeleted {
newConsoleExternalCertSecret = nil
break
}
}
}
// Remove Certificate Secrets from Console CAs (Tenant.Spec.Console.ExternalCaCertSecret)
for _, certificate := range minInst.Spec.Console.ExternalCaCertSecret {
skip := false
for _, certificateToBeDeleted := range params.Body.CustomCertificates.SecretsToBeDeleted {
if certificate.Name == certificateToBeDeleted {
skip = true
break
}
}
if skip {
continue
}
newConsoleExternalCaCertSecret = append(newConsoleExternalCaCertSecret, certificate)
}
}
//Create new Certificate Secrets for MinIO
secretName := fmt.Sprintf("%s-%s", minInst.Name, strings.ToLower(utils.RandomCharString(5)))
externalCertSecretName := fmt.Sprintf("%s-external-certificates", secretName)
@@ -811,7 +722,6 @@ func updateTenantSecurity(ctx context.Context, operatorClient OperatorClientI, c
return err
}
newMinIOExternalCertSecret = append(newMinIOExternalCertSecret, externalCertSecrets...)
// Create new CAs Certificate Secrets for MinIO
var caCertificates []tenantSecret
for i, caCertificate := range params.Body.CustomCertificates.MinioCAs {
@@ -833,46 +743,9 @@ func updateTenantSecurity(ctx context.Context, operatorClient OperatorClientI, c
}
newMinIOExternalCaCertSecret = append(newMinIOExternalCaCertSecret, certificateSecrets...)
}
// Create new Certificate Secrets for Console
consoleExternalCertSecretName := fmt.Sprintf("%s-console-external-certificates", secretName)
consoleExternalCertSecrets, err := createOrReplaceExternalCertSecrets(ctx, client, minInst.Namespace, params.Body.CustomCertificates.Console, consoleExternalCertSecretName, minInst.Name)
if err != nil {
return err
}
if len(consoleExternalCertSecrets) > 0 {
newConsoleExternalCertSecret = consoleExternalCertSecrets[0]
}
// Create new CAs Certificate Secrets for Console
var consoleCaCertificates []tenantSecret
for i, caCertificate := range params.Body.CustomCertificates.ConsoleCAs {
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return err
}
consoleCaCertificates = append(consoleCaCertificates, tenantSecret{
Name: fmt.Sprintf("%s-console-ca-certificate-%d", secretName, i),
Content: map[string][]byte{
"public.crt": certificateContent,
},
})
}
if len(consoleCaCertificates) > 0 {
certificateSecrets, err := createOrReplaceSecrets(ctx, client, minInst.Namespace, consoleCaCertificates, minInst.Name)
if err != nil {
return err
}
newConsoleExternalCaCertSecret = append(newConsoleExternalCaCertSecret, certificateSecrets...)
}
// Update External Certificates
minInst.Spec.ExternalCertSecret = newMinIOExternalCertSecret
minInst.Spec.ExternalCaCertSecret = newMinIOExternalCaCertSecret
if minInst.HasConsoleEnabled() {
minInst.Spec.Console.ExternalCertSecret = newConsoleExternalCertSecret
minInst.Spec.Console.ExternalCaCertSecret = newConsoleExternalCaCertSecret
}
_, err = operatorClient.TenantUpdate(ctx, minInst, metav1.UpdateOptions{})
if err != nil {
return err
@@ -978,8 +851,6 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
tenantReq := params.Body
minioImage := tenantReq.Image
ctx := context.Background()
consoleHasTLS := false
if minioImage == "" {
minImg, err := cluster.GetMinioImage()
// we can live without figuring out the latest version of MinIO, Operator will use a hardcoded value
@@ -1015,9 +886,9 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
var instanceSecret corev1.Secret
var users []*corev1.LocalObjectReference
var tenantConfigurationENV string
tenantConfigurationENV := map[string]string{}
// Create the secret for the root credentials
// Create the secret for the root credentials (deprecated)
secretName := fmt.Sprintf("%s-secret", tenantName)
instanceSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -1033,8 +904,14 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
},
}
tenantConfigurationENV += fmt.Sprintf("export MINIO_ROOT_USER=\"%s\"\n", accessKey)
tenantConfigurationENV += fmt.Sprintf("export MINIO_ROOT_PASSWORD=\"%s\"\n", secretKey)
// Enable/Disable console object browser for MinIO tenant (default is on)
enabledConsole := "on"
if tenantReq.EnableConsole != nil && !*tenantReq.EnableConsole {
enabledConsole = "off"
}
tenantConfigurationENV["MINIO_BROWSER"] = enabledConsole
tenantConfigurationENV["MINIO_ROOT_USER"] = accessKey
tenantConfigurationENV["MINIO_ROOT_PASSWORD"] = secretKey
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{})
if err != nil {
@@ -1060,7 +937,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
return nil, prepareError(errorInvalidErasureCodingValue)
}
tenantConfigurationENV += fmt.Sprintf("export MINIO_STORAGE_CLASS_STANDARD=\"%s\"\n", fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity))
tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity)
}
//Construct a MinIO Instance with everything we are getting from parameters
@@ -1077,10 +954,11 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
},
},
}
var tenantExternalIDPConfigured bool
if tenantReq.Idp != nil {
// Enable IDP (Active Directory) for MinIO
if tenantReq.Idp.ActiveDirectory != nil {
tenantExternalIDPConfigured = true
serverAddress := *tenantReq.Idp.ActiveDirectory.URL
userNameFormat := tenantReq.Idp.ActiveDirectory.UsernameFormat
userNameSearchFilter := tenantReq.Idp.ActiveDirectory.UsernameSearchFilter
@@ -1096,33 +974,33 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
serverStartTLS := tenantReq.Idp.ActiveDirectory.ServerStartTLS
// LDAP Server
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_SERVER_ADDR=\"%s\"\n", serverAddress)
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_ADDR"] = serverAddress
if tlsSkipVerify {
tenantConfigurationENV += "export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on\n"
tenantConfigurationENV["MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"] = "on"
}
if serverInsecure {
tenantConfigurationENV += "export MINIO_IDENTITY_LDAP_SERVER_INSECURE=on\n"
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_INSECURE"] = "on"
}
if serverStartTLS {
tenantConfigurationENV += "export MINIO_IDENTITY_LDAP_SERVER_STARTTLS=on\n"
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_STARTTLS"] = "on"
}
// LDAP Username
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_USERNAME_FORMAT=\"%s\"\n", userNameFormat)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER=\"%s\"\n", userNameSearchFilter)
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_FORMAT"] = userNameFormat
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"] = userNameSearchFilter
// LDAP Lookup
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN=\"%s\"\n", lookupBindDN)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD=\"%s\"\n", lookupBindPassword)
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"] = lookupBindDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD"] = lookupBindPassword
// LDAP User DN
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN=\"%s\"\n", userDNSearchBaseDN)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER=\"%s\"\n", userDNSearchFilter)
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"] = userDNSearchBaseDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"] = userDNSearchFilter
// LDAP Group
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE=\"%s\"\n", groupNameAttribute)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN=\"%s\"\n", groupSearchBaseDN)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER=\"%s\"\n", groupSearchFilter)
tenantConfigurationENV["MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"] = groupNameAttribute
tenantConfigurationENV["MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"] = groupSearchBaseDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"] = groupSearchFilter
// Attach the list of LDAP user DNs that will be administrator for the Tenant
for i, userDN := range tenantReq.Idp.ActiveDirectory.UserDNS {
@@ -1149,20 +1027,23 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
minInst.Spec.Users = users
} else if tenantReq.Idp.Oidc != nil {
tenantExternalIDPConfigured = true
// Enable IDP (OIDC) for MinIO
url := *tenantReq.Idp.Oidc.ConfigurationURL
clientID := *tenantReq.Idp.Oidc.ClientID
secretID := *tenantReq.Idp.Oidc.SecretID
claimName := *tenantReq.Idp.Oidc.ClaimName
scopes := tenantReq.Idp.Oidc.Scopes
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_OPENID_CONFIG_URL=\"%s\"\n", url)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_OPENID_CLIENT_ID=\"%s\"\n", clientID)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_OPENID_CLIENT_SECRET=\"%s\"\n", secretID)
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_OPENID_CLAIM_NAME=\"%s\"\n", claimName)
callbackURL := tenantReq.Idp.Oidc.CallbackURL
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CONFIG_URL"] = url
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CLIENT_ID"] = clientID
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CLIENT_SECRET"] = secretID
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CLAIM_NAME"] = claimName
tenantConfigurationENV["MINIO_IDENTITY_OPENID_REDIRECT_URI"] = callbackURL
if scopes == "" {
scopes = "openid,profile,email"
}
tenantConfigurationENV += fmt.Sprintf("export MINIO_IDENTITY_OPENID_SCOPES=\"%s\"\n", scopes)
tenantConfigurationENV["MINIO_IDENTITY_OPENID_SCOPES"] = scopes
} else if len(tenantReq.Idp.Keys) > 0 {
// Create the secret any built-in user passed if no external IDP was configured
for i := 0; i < len(tenantReq.Idp.Keys); i++ {
@@ -1200,7 +1081,6 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if *tenantReq.EnableTLS {
// requestAutoCert is enabled, MinIO will be deployed with TLS enabled and encryption can be enabled
isEncryptionEnabled = true
consoleHasTLS = true
}
}
// External TLS certificates for MinIO
@@ -1262,130 +1142,12 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
minInst.Spec.ExternalCaCertSecret = certificateSecrets
}
}
// optionals are set below
var tenantUserAccessKey string
var tenantUserSecretKey string
keyElementEmpty := len(tenantReq.Idp.Keys) == 1 && (*tenantReq.Idp.Keys[0].AccessKey == "" && *tenantReq.Idp.Keys[0].SecretKey == "")
enableConsole := true
if tenantReq.EnableConsole != nil && *tenantReq.EnableConsole {
enableConsole = *tenantReq.EnableConsole
}
if enableConsole {
consoleSelector := fmt.Sprintf("%s-console", tenantName)
consoleSecretName := fmt.Sprintf("%s-secret", consoleSelector)
consoleSecretData := map[string][]byte{
"CONSOLE_PBKDF_PASSPHRASE": []byte(restapi.RandomCharString(16)),
"CONSOLE_PBKDF_SALT": []byte(restapi.RandomCharString(8)),
}
// If Subnet License is present in k8s secrets, copy that to the CONSOLE_SUBNET_LICENSE env variable
// of the console tenant
license, _ := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName)
if license != "" {
consoleSecretData[restapi.ConsoleSubnetLicense] = []byte(license)
}
imm := true
consoleSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: consoleSecretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: consoleSecretData,
}
minInst.Spec.Console = &miniov2.ConsoleConfiguration{
Replicas: 1,
Image: getConsoleImage(),
ConsoleSecret: &corev1.LocalObjectReference{Name: consoleSecretName},
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
"memory": resource.MustParse("64Mi"),
},
},
}
if tenantReq.TLS != nil && tenantReq.TLS.Console != nil {
consoleHasTLS = true
// Certificates used by the console instance
externalCertSecretName := fmt.Sprintf("%s-console-external-certificates", secretName)
certificates := []*models.KeyPairConfiguration{tenantReq.TLS.Console}
externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, externalCertSecretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
if len(externalCertSecret) > 0 {
minInst.Spec.Console.ExternalCertSecret = externalCertSecret[0]
}
}
// If IDP is not already enabled via LDAP (Active Directory) and OIDC configuration is present then
// enable oidc for console
if tenantReq.Idp != nil {
if tenantReq.Idp.ActiveDirectory != nil {
consoleSecret.Data[ldap.ConsoleLDAPEnabled] = []byte("on")
} else if tenantReq.Idp.Oidc != nil {
url := *tenantReq.Idp.Oidc.URL
clientID := *tenantReq.Idp.Oidc.ClientID
secretID := *tenantReq.Idp.Oidc.SecretID
callbackURL := tenantReq.Idp.Oidc.CallbackURL
if url != "" && clientID != "" && secretID != "" {
consoleSecret.Data[oauth2.ConsoleIdpURL] = []byte(url)
consoleSecret.Data[oauth2.ConsoleIdpClientID] = []byte(clientID)
consoleSecret.Data[oauth2.ConsoleIdpSecret] = []byte(secretID)
consoleScheme := "http"
consolePort := miniov2.ConsolePort
// If Console will be deployed with TLS enabled (using AutoCert or External certificates)
if consoleHasTLS {
consoleScheme = "https"
consolePort = miniov2.ConsoleTLSPort
}
// default callback url is https://localhost:9443/oauth_callback
consoleSecret.Data[oauth2.ConsoleIdpCallbackURL] = []byte(fmt.Sprintf("%s://localhost:%d/oauth_callback", consoleScheme, consolePort))
if callbackURL != "" {
consoleSecret.Data[oauth2.ConsoleIdpCallbackURL] = []byte(callbackURL)
}
}
}
}
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &consoleSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
// Set Labels, Annotations and Node Selector for Console
if tenantReq.Console != nil {
minInst.Spec.Console.Annotations = tenantReq.Console.Annotations
minInst.Spec.Console.Labels = tenantReq.Console.Labels
minInst.Spec.Console.NodeSelector = tenantReq.Console.NodeSelector
}
// External TLS CA certificates for Console
if tenantReq.TLS != nil && len(tenantReq.TLS.ConsoleCaCertificates) > 0 {
var caCertificates []tenantSecret
for i, caCertificate := range tenantReq.TLS.ConsoleCaCertificates {
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
caCertificates = append(caCertificates, tenantSecret{
Name: fmt.Sprintf("console-ca-certificate-%d", i),
Content: map[string][]byte{
"public.crt": certificateContent,
},
})
}
if len(caCertificates) > 0 {
certificateSecrets, err := createOrReplaceSecrets(ctx, &k8sClient, ns, caCertificates, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
minInst.Spec.Console.ExternalCaCertSecret = certificateSecrets
}
}
// If Subnet License is present in k8s secrets, copy that to the MINIO_SUBNET_LICENSE env variable
// of the console tenant
license, _ := getSubscriptionLicense(ctx, &k8sClient, cluster.Namespace, OperatorSubnetLicenseSecretName)
if license != "" {
tenantConfigurationENV[MinIOSubnetLicense] = license
}
// add annotations
@@ -1432,11 +1194,6 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
minInst.Annotations[prometheusScrape] = "true"
}
// set console image if provided
if tenantReq.ConsoleImage != "" {
minInst.Spec.Console.Image = tenantReq.ConsoleImage
}
//Default class name for Log search
diskSpaceFromAPI := int64(5) * humanize.GiByte // Default is 5Gi
logSearchStorageClass := "" // Default is ""
@@ -1450,18 +1207,15 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
if tenantReq.LogSearchConfiguration.StorageClass != "" {
logSearchStorageClass = tenantReq.LogSearchConfiguration.StorageClass
}
if tenantReq.LogSearchConfiguration.StorageClass == "" && len(tenantReq.Pools) > 0 {
logSearchStorageClass = tenantReq.Pools[0].VolumeConfiguration.StorageClassName
}
if tenantReq.LogSearchConfiguration.Image != "" {
logSearchImage = tenantReq.LogSearchConfiguration.Image
}
if tenantReq.LogSearchConfiguration.PostgresImage != "" {
logSearchPgImage = tenantReq.LogSearchConfiguration.PostgresImage
}
}
logSearchDiskSpace := resource.NewQuantity(diskSpaceFromAPI, resource.DecimalExponent)
@@ -1542,7 +1296,7 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
{
Name: tenantConfigurationName,
Content: map[string][]byte{
"config.env": []byte(tenantConfigurationENV),
"config.env": []byte(GenerateTenantConfigurationFile(tenantConfigurationENV)),
},
},
}, tenantName)
@@ -1569,20 +1323,16 @@ func getTenantCreatedResponse(session *models.Principal, params operator_api.Cre
return nil, prepareError(err)
}
}
response = &models.CreateTenantResponse{}
// Attach Console Credentials
if enableConsole {
var itemsToReturn []*models.TenantResponseItem
if len(tenantReq.Idp.Keys) == 0 || keyElementEmpty {
itemsToReturn = append(itemsToReturn, &models.TenantResponseItem{AccessKey: tenantUserAccessKey, SecretKey: tenantUserSecretKey})
} else { // IDP Keys
for _, item := range tenantReq.Idp.Keys {
itemsToReturn = append(itemsToReturn, &models.TenantResponseItem{AccessKey: *item.AccessKey, SecretKey: *item.SecretKey})
}
response = &models.CreateTenantResponse{
ExternalIDP: tenantExternalIDPConfigured,
}
if tenantReq.Idp != nil && !tenantExternalIDPConfigured {
for _, credential := range tenantReq.Idp.Keys {
response.Console = append(response.Console, &models.TenantResponseItem{
AccessKey: *credential.AccessKey,
SecretKey: *credential.SecretKey,
})
}
response.Console = itemsToReturn
}
return response, nil
}
@@ -1666,11 +1416,6 @@ func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, cli
}
}
// update the console image
if strings.TrimSpace(params.Body.ConsoleImage) != "" && minInst.Spec.Console != nil {
minInst.Spec.Console.Image = params.Body.ConsoleImage
}
// if image to update is empty we'll use the latest image by default
if strings.TrimSpace(imageToUpdate) != "" {
minInst.Spec.Image = imageToUpdate
@@ -1811,8 +1556,8 @@ func getTenantAddPoolResponse(session *models.Principal, params operator_api.Ten
// getTenantUsageResponse returns the usage of a tenant
func getTenantUsageResponse(session *models.Principal, params operator_api.GetTenantUsageParams) (*models.TenantUsage, *models.Error) {
// 5 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 30 seconds timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
opClientClientSet, err := cluster.OperatorClient(session.STSSessionToken)

View File

@@ -63,22 +63,6 @@ func tenantUpdateCertificates(ctx context.Context, operatorClient OperatorClient
return err
}
}
// check if Console is deployed with external certs and user provided new Console keypair
if tenant.ConsoleExternalCert() && tenant.HasConsoleEnabled() && body.Console != nil {
consoleCertSecretName := fmt.Sprintf("%s-console-external-certificates", secretName)
// update certificates
certificates := []*models.KeyPairConfiguration{body.Console}
if _, err := createOrReplaceExternalCertSecrets(ctx, clientSet, namespace, certificates, consoleCertSecretName, tenantName); err != nil {
return err
}
// restart Console pods
err := clientSet.deletePodCollection(ctx, namespace, metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", miniov2.ConsoleTenantLabel, fmt.Sprintf("%s-console", tenantName)),
})
if err != nil {
return err
}
}
return nil
}
@@ -251,7 +235,6 @@ func createOrReplaceSecrets(ctx context.Context, clientSet K8sClientI, ns string
// log the error if any and continue
LogError("deleting secret name %s failed: %v, continuing..", secret.Name, err)
}
imm := true
k8sSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secret.Name,
@@ -259,9 +242,8 @@ func createOrReplaceSecrets(ctx context.Context, clientSet K8sClientI, ns string
miniov2.TenantLabel: tenantName,
},
},
Type: corev1.SecretTypeOpaque,
Immutable: &imm,
Data: secret.Content,
Type: corev1.SecretTypeOpaque,
Data: secret.Content,
}
_, err = clientSet.createSecret(ctx, ns, k8sSecret, metav1.CreateOptions{})
if err != nil {

View File

@@ -33,6 +33,7 @@ import (
var DeletePodCollectionMock func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
var DeleteSecretMock func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error
var CreateSecretMock func(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)
var UpdateSecretMock func(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)
func (c k8sClientMock) deletePodCollection(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return DeletePodCollectionMock(ctx, namespace, opts, listOpts)
@@ -46,6 +47,10 @@ func (c k8sClientMock) createSecret(ctx context.Context, namespace string, secre
return CreateSecretMock(ctx, namespace, secret, opts)
}
func (c k8sClientMock) updateSecret(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error) {
return UpdateSecretMock(ctx, namespace, secret, opts)
}
func Test_tenantUpdateCertificates(t *testing.T) {
k8sClient := k8sClientMock{}
opClient := opClientMock{}
@@ -228,70 +233,6 @@ func Test_tenantUpdateCertificates(t *testing.T) {
},
wantErr: true,
},
{
name: "error replacing external certs for console because of missing keypair",
args: args{
ctx: context.Background(),
opClient: opClient,
clientSet: k8sClient,
namespace: "",
params: operator_api.TenantUpdateCertificateParams{
Body: &models.TLSConfiguration{
Console: &models.KeyPairConfiguration{},
},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
Spec: miniov2.TenantSpec{
Console: &miniov2.ConsoleConfiguration{
ExternalCertSecret: &miniov2.LocalCertificateReference{
Name: "secret",
},
},
},
}, nil
},
},
wantErr: true,
},
{
name: "certificates replaced but error during deleting existing tenant pods",
args: args{
ctx: context.Background(),
opClient: opClient,
clientSet: k8sClient,
namespace: "",
params: operator_api.TenantUpdateCertificateParams{
Body: &models.TLSConfiguration{
Console: &models.KeyPairConfiguration{
Crt: &crt,
Key: &key,
},
},
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{
Spec: miniov2.TenantSpec{
Console: &miniov2.ConsoleConfiguration{
ExternalCertSecret: &miniov2.LocalCertificateReference{
Name: "secret",
},
},
},
}, nil
},
mockDeleteSecret: func(ctx context.Context, namespace string, name string, opts metav1.DeleteOptions) error {
return nil
},
mockCreateSecret: func(ctx context.Context, namespace string, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error) {
return &v1.Secret{}, nil
},
mockDeletePodCollection: func(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
return errors.New("error deleting console pods")
},
},
wantErr: true,
},
}
for _, tt := range tests {
opClientTenantGetMock = tt.args.mockTenantGet

View File

@@ -468,9 +468,6 @@ func Test_TenantInfo(t *testing.T) {
Spec: miniov2.TenantSpec{
Pools: []miniov2.Pool{},
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
Console: &miniov2.ConsoleConfiguration{
Image: "minio/console:master",
},
},
Status: miniov2.TenantStatus{
CurrentState: "ready",
@@ -484,7 +481,6 @@ func Test_TenantInfo(t *testing.T) {
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
EnablePrometheus: false,
ConsoleImage: "minio/console:master",
},
},
}
@@ -1017,31 +1013,6 @@ func Test_UpdateTenantAction(t *testing.T) {
},
wantErr: false,
},
{
name: "Update minio console version no errors",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*miniov2.Tenant, error) {
return &miniov2.Tenant{}, nil
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
return nil, errors.New("use default minio")
},
params: operator_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
ConsoleImage: "minio/console:v0.9.1",
},
},
},
wantErr: false,
},
{
name: "Update minio image pull secrets no errors",
args: args{

View File

@@ -86,11 +86,12 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
nsTenant := fmt.Sprintf("%s/%s", tenant.Namespace, tenant.Name)
tenantSchema := "http"
tenantPort := ":9090"
if tenant.AutoCert() || tenant.ConsoleExternalCert() {
tenantPort := fmt.Sprintf(":%d", v2.ConsolePort)
if tenant.AutoCert() {
tenantSchema = "https"
tenantPort = ":9443"
tenantPort = fmt.Sprintf(":%d", v2.ConsoleTLSPort)
}
tenantURL := fmt.Sprintf("%s://%s.%s.svc.%s%s", tenantSchema, tenant.ConsoleCIServiceName(), tenant.Namespace, v2.GetClusterDomain(), tenantPort)
// for development
//tenantURL = "http://localhost:9091"
@@ -112,34 +113,14 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
return
}
// FIXME: abstract this to a common tenant.GetConfiguration() function
tenantConfiguration := map[string][]byte{}
for _, config := range tenant.GetEnvVars() {
tenantConfiguration[config.Name] = []byte(config.Value)
k8sClient := k8sClient{
client: clientSet,
}
if tenant.HasCredsSecret() {
minioSecret, err := clientSet.CoreV1().Secrets(tenant.Namespace).Get(req.Context(), tenant.Spec.CredsSecret.Name, metav1.GetOptions{})
if err != nil {
log.Println(err)
responseWriter.WriteHeader(500)
return
}
configFromCredsSecret := minioSecret.Data
for key, val := range configFromCredsSecret {
tenantConfiguration[key] = val
}
}
if tenant.HasConfigurationSecret() {
minioConfigurationSecret, err := clientSet.CoreV1().Secrets(tenant.Namespace).Get(req.Context(), tenant.Spec.Configuration.Name, metav1.GetOptions{})
if err == nil {
configFromFile := v2.ParseRawConfiguration(minioConfigurationSecret.Data["config.env"])
for key, val := range configFromFile {
tenantConfiguration[key] = val
}
}
tenantConfiguration, err := GetTenantConfiguration(req.Context(), &k8sClient, tenant)
if err != nil {
log.Println(err)
responseWriter.WriteHeader(500)
return
}
data := map[string]string{

65
operatorapi/utils.go Normal file
View File

@@ -0,0 +1,65 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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"
"fmt"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func GetTenantConfiguration(ctx context.Context, clientSet K8sClientI, tenant *miniov2.Tenant) (map[string]string, error) {
if tenant == nil {
return nil, errors.New("tenant cannot be nil")
}
tenantConfiguration := map[string]string{}
for _, config := range tenant.GetEnvVars() {
tenantConfiguration[config.Name] = config.Value
}
// legacy support for tenants with tenant.spec.credsSecret
if tenant.HasCredsSecret() {
minioSecret, err := clientSet.getSecret(ctx, tenant.Namespace, tenant.Spec.CredsSecret.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
configFromCredsSecret := minioSecret.Data
for key, val := range configFromCredsSecret {
tenantConfiguration[key] = string(val)
}
}
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)
}
}
}
return tenantConfiguration, nil
}
func GenerateTenantConfigurationFile(configuration map[string]string) string {
var rawConfiguration string
for key, val := range configuration {
rawConfiguration += fmt.Sprintf("export %s=\"%s\"\n", key, val)
}
return rawConfiguration
}