Move Add Tenant Configure Audit Log and Monitoring to their own tabs (#1636)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-03-01 10:32:50 -08:00
committed by GitHub
parent 645f98284d
commit 632c66539e
11 changed files with 1639 additions and 1423 deletions

View File

@@ -18,9 +18,7 @@ package operatorapi
// list of all console environment constants
const (
ConsoleSubnetLicense = "CONSOLE_SUBNET_LICENSE"
ConsoleOperatorSAToken = "CONSOLE_OPERATOR_SA_TOKEN"
MinIOSubnetLicense = "MINIO_SUBNET_LICENSE"
ConsoleMarketplace = "CONSOLE_OPERATOR_MARKETPLACE"
// Constants for prometheus annotations
@@ -31,11 +29,5 @@ const (
// Image versions
const (
KESImageVersion = "minio/kes:v0.16.1"
)
// K8s
const (
OperatorSubnetLicenseSecretName = "subnet-license"
KESImageVersion = "minio/kes:v0.17.6"
)

View File

@@ -0,0 +1,588 @@
// 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"
"encoding/base64"
"fmt"
"os"
"github.com/dustin/go-humanize"
"github.com/minio/console/restapi"
"github.com/minio/console/operatorapi/operations/operator_api"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/go-openapi/swag"
"github.com/minio/console/cluster"
"github.com/minio/console/models"
miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func getTenantCreatedResponse(session *models.Principal, params operator_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) {
tenantReq := params.Body
minioImage := tenantReq.Image
ctx := context.Background()
if minioImage == "" {
minImg, err := cluster.GetMinioImage()
// we can live without figuring out the latest version of MinIO, Operator will use a hardcoded value
if err == nil {
minioImage = *minImg
}
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
k8sClient := k8sClient{
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
}
ns := *tenantReq.Namespace
// if access/secret are provided, use them, else create a random pair
accessKey := restapi.RandomCharString(16)
secretKey := restapi.RandomCharString(32)
if tenantReq.AccessKey != "" {
accessKey = tenantReq.AccessKey
}
if tenantReq.SecretKey != "" {
secretKey = tenantReq.SecretKey
}
tenantName := *tenantReq.Name
imm := true
var instanceSecret corev1.Secret
var users []*corev1.LocalObjectReference
tenantConfigurationENV := map[string]string{}
// Create the secret for the root credentials (deprecated)
secretName := fmt.Sprintf("%s-secret", tenantName)
instanceSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"accesskey": []byte(""),
"secretkey": []byte(""),
},
}
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
// 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
// delete secrets created if an error occurred during tenant creation,
defer func() {
if mError != nil {
restapi.LogError("deleting secrets created for failed tenant: %s if any: %v", tenantName, mError)
opts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, tenantName),
}
err = clientSet.CoreV1().Secrets(ns).DeleteCollection(ctx, metav1.DeleteOptions{}, opts)
if err != nil {
restapi.LogError("error deleting tenant's secrets: %v", err)
}
}
}()
// Check the Erasure Coding Parity for validity and pass it to Tenant
if tenantReq.ErasureCodingParity > 0 {
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
return nil, prepareError(errorInvalidErasureCodingValue)
}
tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity)
}
//Construct a MinIO Instance with everything we are getting from parameters
minInst := miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: tenantName,
Labels: tenantReq.Labels,
},
Spec: miniov2.TenantSpec{
Image: minioImage,
Mountpath: "/export",
CredsSecret: &corev1.LocalObjectReference{
Name: secretName,
},
},
}
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
groupNameAttribute := tenantReq.Idp.ActiveDirectory.GroupNameAttribute
tlsSkipVerify := tenantReq.Idp.ActiveDirectory.SkipTLSVerification
serverInsecure := tenantReq.Idp.ActiveDirectory.ServerInsecure
lookupBindDN := tenantReq.Idp.ActiveDirectory.LookupBindDn
lookupBindPassword := tenantReq.Idp.ActiveDirectory.LookupBindPassword
userDNSearchBaseDN := tenantReq.Idp.ActiveDirectory.UserDnSearchBaseDn
userDNSearchFilter := tenantReq.Idp.ActiveDirectory.UserDnSearchFilter
groupSearchBaseDN := tenantReq.Idp.ActiveDirectory.GroupSearchBaseDn
groupSearchFilter := tenantReq.Idp.ActiveDirectory.GroupSearchFilter
serverStartTLS := tenantReq.Idp.ActiveDirectory.ServerStartTLS
// LDAP Server
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_ADDR"] = serverAddress
if tlsSkipVerify {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"] = "on"
}
if serverInsecure {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_INSECURE"] = "on"
}
if serverStartTLS {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_STARTTLS"] = "on"
}
// LDAP Username
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_FORMAT"] = userNameFormat
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"] = userNameSearchFilter
// LDAP Lookup
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"] = lookupBindDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD"] = lookupBindPassword
// LDAP User DN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"] = userDNSearchBaseDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"] = userDNSearchFilter
// LDAP Group
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 {
userSecretName := fmt.Sprintf("%s-user-%d", tenantName, i)
users = append(users, &corev1.LocalObjectReference{Name: userSecretName})
userSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: userSecretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"CONSOLE_ACCESS_KEY": []byte(userDN),
},
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
}
// attach the users to the tenant
minInst.Spec.Users = users
} else if tenantReq.Idp.Oidc != nil {
tenantExternalIDPConfigured = true
// Enable IDP (OIDC) for MinIO
configurationURL := *tenantReq.Idp.Oidc.ConfigurationURL
clientID := *tenantReq.Idp.Oidc.ClientID
secretID := *tenantReq.Idp.Oidc.SecretID
claimName := *tenantReq.Idp.Oidc.ClaimName
scopes := tenantReq.Idp.Oidc.Scopes
callbackURL := tenantReq.Idp.Oidc.CallbackURL
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CONFIG_URL"] = configurationURL
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["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++ {
userSecretName := fmt.Sprintf("%s-user-%d", tenantName, i)
users = append(users, &corev1.LocalObjectReference{Name: userSecretName})
userSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: userSecretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"CONSOLE_ACCESS_KEY": []byte(*tenantReq.Idp.Keys[i].AccessKey),
"CONSOLE_SECRET_KEY": []byte(*tenantReq.Idp.Keys[i].SecretKey),
},
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
}
// attach the users to the tenant
minInst.Spec.Users = users
}
}
isEncryptionEnabled := false
if tenantReq.EnableTLS != nil {
// if enableTLS is defined in the create tenant request we assign the value
// to the RequestAutoCert attribute in the tenant spec
minInst.Spec.RequestAutoCert = tenantReq.EnableTLS
if *tenantReq.EnableTLS {
// requestAutoCert is enabled, MinIO will be deployed with TLS enabled and encryption can be enabled
isEncryptionEnabled = true
}
}
// External TLS certificates for MinIO
if tenantReq.TLS != nil && len(tenantReq.TLS.Minio) > 0 {
isEncryptionEnabled = true
// Certificates used by the MinIO instance
externalCertSecretName := fmt.Sprintf("%s-instance-external-certificates", secretName)
externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, tenantReq.TLS.Minio, externalCertSecretName, tenantName)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.ExternalCertSecret = externalCertSecret
}
// If encryption configuration is present and TLS will be enabled (using AutoCert or External certificates)
if tenantReq.Encryption != nil && isEncryptionEnabled {
// KES client mTLSCertificates used by MinIO instance
if tenantReq.Encryption.Client != nil {
tenantExternalClientCertSecretName := fmt.Sprintf("%s-tenant-external-client-cert", secretName)
certificates := []*models.KeyPairConfiguration{tenantReq.Encryption.Client}
certificateSecrets, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, tenantExternalClientCertSecretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
if len(certificateSecrets) > 0 {
minInst.Spec.ExternalClientCertSecret = certificateSecrets[0]
}
}
// KES configuration for Tenant instance
minInst.Spec.KES, err = getKESConfiguration(ctx, &k8sClient, ns, tenantReq.Encryption, secretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
// Set Labels, Annotations and Node Selector for KES
minInst.Spec.KES.Labels = tenantReq.Encryption.Labels
minInst.Spec.KES.Annotations = tenantReq.Encryption.Annotations
minInst.Spec.KES.NodeSelector = tenantReq.Encryption.NodeSelector
if tenantReq.Encryption.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.Encryption.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.KES.SecurityContext = sc
}
}
// External TLS CA certificates for MinIO
if tenantReq.TLS != nil && len(tenantReq.TLS.CaCertificates) > 0 {
var caCertificates []tenantSecret
for i, caCertificate := range tenantReq.TLS.CaCertificates {
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
caCertificates = append(caCertificates, tenantSecret{
Name: fmt.Sprintf("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.ExternalCaCertSecret = certificateSecrets
}
}
// add annotations
var annotations map[string]string
if len(tenantReq.Annotations) > 0 {
annotations = tenantReq.Annotations
minInst.Annotations = annotations
}
// set the pools if they are provided
for _, pool := range tenantReq.Pools {
pool, err := parseTenantPoolRequest(pool)
if err != nil {
restapi.LogError("parseTenantPoolRequest failed: %v", err)
return nil, prepareError(err)
}
minInst.Spec.Pools = append(minInst.Spec.Pools, *pool)
}
// Set Mount Path if provided
if tenantReq.MounthPath != "" {
minInst.Spec.Mountpath = tenantReq.MounthPath
}
// We accept either `image_pull_secret` or the individual details of the `image_registry` but not both
var imagePullSecret string
if tenantReq.ImagePullSecret != "" {
imagePullSecret = tenantReq.ImagePullSecret
} else if imagePullSecret, err = setImageRegistry(ctx, tenantReq.ImageRegistry, clientSet.CoreV1(), ns, tenantName); err != nil {
return nil, prepareError(err)
}
// pass the image pull secret to the Tenant
if imagePullSecret != "" {
minInst.Spec.ImagePullSecret = corev1.LocalObjectReference{
Name: imagePullSecret,
}
}
// prometheus annotations support
if tenantReq.EnablePrometheus != nil && *tenantReq.EnablePrometheus && minInst.Annotations != nil {
minInst.Annotations[prometheusPath] = "/minio/prometheus/metrics"
minInst.Annotations[prometheusPort] = fmt.Sprint(miniov2.MinIOPort)
minInst.Annotations[prometheusScrape] = "true"
}
// Is Log Search enabled? (present in the parameters) if so configure
if tenantReq.LogSearchConfiguration != nil {
//Default class name for Log search
diskSpaceFromAPI := int64(5) * humanize.GiByte // Default is 5Gi
logSearchImage := ""
logSearchPgImage := ""
logSearchPgInitImage := ""
var logSearchStorageClass *string // Nil means use default storage class
var logSearchSecurityContext *corev1.PodSecurityContext
var logSearchPgSecurityContext *corev1.PodSecurityContext
if tenantReq.LogSearchConfiguration.StorageSize != nil {
diskSpaceFromAPI = int64(*tenantReq.LogSearchConfiguration.StorageSize) * humanize.GiByte
}
if tenantReq.LogSearchConfiguration.StorageClass != "" {
logSearchStorageClass = stringPtr(tenantReq.LogSearchConfiguration.StorageClass)
}
if tenantReq.LogSearchConfiguration.Image != "" {
logSearchImage = tenantReq.LogSearchConfiguration.Image
}
if tenantReq.LogSearchConfiguration.PostgresImage != "" {
logSearchPgImage = tenantReq.LogSearchConfiguration.PostgresImage
}
if tenantReq.LogSearchConfiguration.PostgresInitImage != "" {
logSearchPgInitImage = tenantReq.LogSearchConfiguration.PostgresInitImage
}
// if security context for logSearch is present, configure it.
if tenantReq.LogSearchConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
logSearchSecurityContext = sc
}
// if security context for logSearch is present, configure it.
if tenantReq.LogSearchConfiguration.PostgresSecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.PostgresSecurityContext)
if err != nil {
return nil, prepareError(err)
}
logSearchPgSecurityContext = sc
}
logSearchDiskSpace := resource.NewQuantity(diskSpaceFromAPI, resource.DecimalExponent)
// the audit max cap cannot be larger than disk size on the DB, else it won't trim the data
auditMaxCap := 10
if (diskSpaceFromAPI / humanize.GiByte) < int64(auditMaxCap) {
auditMaxCap = int(diskSpaceFromAPI / humanize.GiByte)
}
// default activate lgo search and prometheus
minInst.Spec.Log = &miniov2.LogConfig{
Audit: &miniov2.AuditConfig{DiskCapacityGB: swag.Int(auditMaxCap)},
Db: &miniov2.LogDbConfig{
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: tenantName + "-log",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: *logSearchDiskSpace,
},
},
StorageClassName: logSearchStorageClass,
},
},
},
}
// set log search images if any
if logSearchImage != "" {
minInst.Spec.Log.Image = logSearchImage
}
if logSearchPgImage != "" {
minInst.Spec.Log.Db.Image = logSearchPgImage
}
if logSearchPgInitImage != "" {
minInst.Spec.Log.Db.InitImage = logSearchPgInitImage
}
if logSearchSecurityContext != nil {
minInst.Spec.Log.SecurityContext = logSearchSecurityContext
}
if logSearchPgSecurityContext != nil {
minInst.Spec.Log.Db.SecurityContext = logSearchPgSecurityContext
}
}
// Is Prometheus/Monitoring enabled? (config present in the parameters) if so configure
if tenantReq.PrometheusConfiguration != nil {
prometheusDiskSpace := 5 // Default is 5 by API
prometheusImage := "" // Default is ""
prometheusSidecardImage := "" // Default is ""
prometheusInitImage := "" // Default is ""
var prometheusStorageClass *string // Nil means default storage class
if tenantReq.PrometheusConfiguration.StorageSize != nil {
prometheusDiskSpace = int(*tenantReq.PrometheusConfiguration.StorageSize)
}
if tenantReq.PrometheusConfiguration.StorageClass != "" {
prometheusStorageClass = stringPtr(tenantReq.PrometheusConfiguration.StorageClass)
}
if tenantReq.PrometheusConfiguration.Image != "" {
prometheusImage = tenantReq.PrometheusConfiguration.Image
}
if tenantReq.PrometheusConfiguration.SidecarImage != "" {
prometheusSidecardImage = tenantReq.PrometheusConfiguration.SidecarImage
}
if tenantReq.PrometheusConfiguration.InitImage != "" {
prometheusInitImage = tenantReq.PrometheusConfiguration.InitImage
}
minInst.Spec.Prometheus = &miniov2.PrometheusConfig{
DiskCapacityDB: swag.Int(prometheusDiskSpace),
StorageClassName: prometheusStorageClass,
}
if prometheusImage != "" {
minInst.Spec.Prometheus.Image = prometheusImage
}
if prometheusSidecardImage != "" {
minInst.Spec.Prometheus.SideCarImage = prometheusSidecardImage
}
if prometheusInitImage != "" {
minInst.Spec.Prometheus.InitImage = prometheusInitImage
}
// if security context for prometheus is present, configure it.
if tenantReq.PrometheusConfiguration != nil && tenantReq.PrometheusConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.PrometheusConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.Prometheus.SecurityContext = sc
}
}
// expose services
minInst.Spec.ExposeServices = &miniov2.ExposeServices{
MinIO: tenantReq.ExposeMinio,
Console: tenantReq.ExposeConsole,
}
// write tenant configuration to secret that contains config.env
tenantConfigurationName := fmt.Sprintf("%s-env-configuration", tenantName)
_, err = createOrReplaceSecrets(ctx, &k8sClient, ns, []tenantSecret{
{
Name: tenantConfigurationName,
Content: map[string][]byte{
"config.env": []byte(GenerateTenantConfigurationFile(tenantConfigurationENV)),
},
},
}, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
minInst.Spec.Configuration = &corev1.LocalObjectReference{Name: tenantConfigurationName}
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
}
_, err = opClient.MinioV2().Tenants(ns).Create(context.Background(), &minInst, metav1.CreateOptions{})
if err != nil {
restapi.LogError("Creating new tenant failed with: %v", err)
return nil, prepareError(err)
}
// Integrations
if os.Getenv("GKE_INTEGRATION") != "" {
err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
}
}
response = &models.CreateTenantResponse{
ExternalIDP: tenantExternalIDPConfigured,
}
thisClient := &operatorClient{
client: opClient,
}
minTenant, err := getTenant(ctx, thisClient, ns, tenantName)
if tenantReq.Idp != nil && !tenantExternalIDPConfigured {
for _, credential := range tenantReq.Idp.Keys {
response.Console = append(response.Console, &models.TenantResponseItem{
AccessKey: *credential.AccessKey,
SecretKey: *credential.SecretKey,
URL: GetTenantServiceURL(minTenant),
})
}
}
return response, nil
}

View File

@@ -27,7 +27,6 @@ import (
"fmt"
"net"
"net/http"
"os"
"reflect"
"sort"
"strconv"
@@ -980,550 +979,6 @@ func getListTenantsResponse(session *models.Principal, params operator_api.ListT
return listT, nil
}
func getTenantCreatedResponse(session *models.Principal, params operator_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) {
tenantReq := params.Body
minioImage := tenantReq.Image
ctx := context.Background()
if minioImage == "" {
minImg, err := cluster.GetMinioImage()
// we can live without figuring out the latest version of MinIO, Operator will use a hardcoded value
if err == nil {
minioImage = *minImg
}
}
// get Kubernetes Client
clientSet, err := cluster.K8sClient(session.STSSessionToken)
k8sClient := k8sClient{
client: clientSet,
}
if err != nil {
return nil, prepareError(err)
}
ns := *tenantReq.Namespace
// if access/secret are provided, use them, else create a random pair
accessKey := restapi.RandomCharString(16)
secretKey := restapi.RandomCharString(32)
if tenantReq.AccessKey != "" {
accessKey = tenantReq.AccessKey
}
if tenantReq.SecretKey != "" {
secretKey = tenantReq.SecretKey
}
tenantName := *tenantReq.Name
imm := true
var instanceSecret corev1.Secret
var users []*corev1.LocalObjectReference
tenantConfigurationENV := map[string]string{}
// Create the secret for the root credentials (deprecated)
secretName := fmt.Sprintf("%s-secret", tenantName)
instanceSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"accesskey": []byte(""),
"secretkey": []byte(""),
},
}
_, err = clientSet.CoreV1().Secrets(ns).Create(ctx, &instanceSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
// 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
// delete secrets created if an error occurred during tenant creation,
defer func() {
if mError != nil {
restapi.LogError("deleting secrets created for failed tenant: %s if any: %v", tenantName, mError)
opts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", miniov2.TenantLabel, tenantName),
}
err = clientSet.CoreV1().Secrets(ns).DeleteCollection(ctx, metav1.DeleteOptions{}, opts)
if err != nil {
restapi.LogError("error deleting tenant's secrets: %v", err)
}
}
}()
// Check the Erasure Coding Parity for validity and pass it to Tenant
if tenantReq.ErasureCodingParity > 0 {
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
return nil, prepareError(errorInvalidErasureCodingValue)
}
tenantConfigurationENV["MINIO_STORAGE_CLASS_STANDARD"] = fmt.Sprintf("EC:%d", tenantReq.ErasureCodingParity)
}
//Construct a MinIO Instance with everything we are getting from parameters
minInst := miniov2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: tenantName,
Labels: tenantReq.Labels,
},
Spec: miniov2.TenantSpec{
Image: minioImage,
Mountpath: "/export",
CredsSecret: &corev1.LocalObjectReference{
Name: secretName,
},
},
}
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
groupNameAttribute := tenantReq.Idp.ActiveDirectory.GroupNameAttribute
tlsSkipVerify := tenantReq.Idp.ActiveDirectory.SkipTLSVerification
serverInsecure := tenantReq.Idp.ActiveDirectory.ServerInsecure
lookupBindDN := tenantReq.Idp.ActiveDirectory.LookupBindDn
lookupBindPassword := tenantReq.Idp.ActiveDirectory.LookupBindPassword
userDNSearchBaseDN := tenantReq.Idp.ActiveDirectory.UserDnSearchBaseDn
userDNSearchFilter := tenantReq.Idp.ActiveDirectory.UserDnSearchFilter
groupSearchBaseDN := tenantReq.Idp.ActiveDirectory.GroupSearchBaseDn
groupSearchFilter := tenantReq.Idp.ActiveDirectory.GroupSearchFilter
serverStartTLS := tenantReq.Idp.ActiveDirectory.ServerStartTLS
// LDAP Server
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_ADDR"] = serverAddress
if tlsSkipVerify {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"] = "on"
}
if serverInsecure {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_INSECURE"] = "on"
}
if serverStartTLS {
tenantConfigurationENV["MINIO_IDENTITY_LDAP_SERVER_STARTTLS"] = "on"
}
// LDAP Username
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_FORMAT"] = userNameFormat
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"] = userNameSearchFilter
// LDAP Lookup
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"] = lookupBindDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD"] = lookupBindPassword
// LDAP User DN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"] = userDNSearchBaseDN
tenantConfigurationENV["MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"] = userDNSearchFilter
// LDAP Group
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 {
userSecretName := fmt.Sprintf("%s-user-%d", tenantName, i)
users = append(users, &corev1.LocalObjectReference{Name: userSecretName})
userSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: userSecretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"CONSOLE_ACCESS_KEY": []byte(userDN),
},
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
}
// attach the users to the tenant
minInst.Spec.Users = users
} else if tenantReq.Idp.Oidc != nil {
tenantExternalIDPConfigured = true
// Enable IDP (OIDC) for MinIO
configurationURL := *tenantReq.Idp.Oidc.ConfigurationURL
clientID := *tenantReq.Idp.Oidc.ClientID
secretID := *tenantReq.Idp.Oidc.SecretID
claimName := *tenantReq.Idp.Oidc.ClaimName
scopes := tenantReq.Idp.Oidc.Scopes
callbackURL := tenantReq.Idp.Oidc.CallbackURL
tenantConfigurationENV["MINIO_IDENTITY_OPENID_CONFIG_URL"] = configurationURL
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["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++ {
userSecretName := fmt.Sprintf("%s-user-%d", tenantName, i)
users = append(users, &corev1.LocalObjectReference{Name: userSecretName})
userSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: userSecretName,
Labels: map[string]string{
miniov2.TenantLabel: tenantName,
},
},
Immutable: &imm,
Data: map[string][]byte{
"CONSOLE_ACCESS_KEY": []byte(*tenantReq.Idp.Keys[i].AccessKey),
"CONSOLE_SECRET_KEY": []byte(*tenantReq.Idp.Keys[i].SecretKey),
},
}
_, err := clientSet.CoreV1().Secrets(ns).Create(ctx, &userSecret, metav1.CreateOptions{})
if err != nil {
return nil, prepareError(err)
}
}
// attach the users to the tenant
minInst.Spec.Users = users
}
}
isEncryptionEnabled := false
if tenantReq.EnableTLS != nil {
// if enableTLS is defined in the create tenant request we assign the value
// to the RequestAutoCert attribute in the tenant spec
minInst.Spec.RequestAutoCert = tenantReq.EnableTLS
if *tenantReq.EnableTLS {
// requestAutoCert is enabled, MinIO will be deployed with TLS enabled and encryption can be enabled
isEncryptionEnabled = true
}
}
// External TLS certificates for MinIO
if tenantReq.TLS != nil && len(tenantReq.TLS.Minio) > 0 {
isEncryptionEnabled = true
// Certificates used by the MinIO instance
externalCertSecretName := fmt.Sprintf("%s-instance-external-certificates", secretName)
externalCertSecret, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, tenantReq.TLS.Minio, externalCertSecretName, tenantName)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.ExternalCertSecret = externalCertSecret
}
// If encryption configuration is present and TLS will be enabled (using AutoCert or External certificates)
if tenantReq.Encryption != nil && isEncryptionEnabled {
// KES client mTLSCertificates used by MinIO instance
if tenantReq.Encryption.Client != nil {
tenantExternalClientCertSecretName := fmt.Sprintf("%s-tenant-external-client-cert", secretName)
certificates := []*models.KeyPairConfiguration{tenantReq.Encryption.Client}
certificateSecrets, err := createOrReplaceExternalCertSecrets(ctx, &k8sClient, ns, certificates, tenantExternalClientCertSecretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
if len(certificateSecrets) > 0 {
minInst.Spec.ExternalClientCertSecret = certificateSecrets[0]
}
}
// KES configuration for Tenant instance
minInst.Spec.KES, err = getKESConfiguration(ctx, &k8sClient, ns, tenantReq.Encryption, secretName, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric)
}
// Set Labels, Annotations and Node Selector for KES
minInst.Spec.KES.Labels = tenantReq.Encryption.Labels
minInst.Spec.KES.Annotations = tenantReq.Encryption.Annotations
minInst.Spec.KES.NodeSelector = tenantReq.Encryption.NodeSelector
if tenantReq.Encryption.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.Encryption.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.KES.SecurityContext = sc
}
}
// External TLS CA certificates for MinIO
if tenantReq.TLS != nil && len(tenantReq.TLS.CaCertificates) > 0 {
var caCertificates []tenantSecret
for i, caCertificate := range tenantReq.TLS.CaCertificates {
certificateContent, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
caCertificates = append(caCertificates, tenantSecret{
Name: fmt.Sprintf("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.ExternalCaCertSecret = certificateSecrets
}
}
// add annotations
var annotations map[string]string
if len(tenantReq.Annotations) > 0 {
annotations = tenantReq.Annotations
minInst.Annotations = annotations
}
// set the pools if they are provided
for _, pool := range tenantReq.Pools {
pool, err := parseTenantPoolRequest(pool)
if err != nil {
restapi.LogError("parseTenantPoolRequest failed: %v", err)
return nil, prepareError(err)
}
minInst.Spec.Pools = append(minInst.Spec.Pools, *pool)
}
// Set Mount Path if provided
if tenantReq.MounthPath != "" {
minInst.Spec.Mountpath = tenantReq.MounthPath
}
// We accept either `image_pull_secret` or the individual details of the `image_registry` but not both
var imagePullSecret string
if tenantReq.ImagePullSecret != "" {
imagePullSecret = tenantReq.ImagePullSecret
} else if imagePullSecret, err = setImageRegistry(ctx, tenantReq.ImageRegistry, clientSet.CoreV1(), ns, tenantName); err != nil {
return nil, prepareError(err)
}
// pass the image pull secret to the Tenant
if imagePullSecret != "" {
minInst.Spec.ImagePullSecret = corev1.LocalObjectReference{
Name: imagePullSecret,
}
}
// prometheus annotations support
if tenantReq.EnablePrometheus != nil && *tenantReq.EnablePrometheus && minInst.Annotations != nil {
minInst.Annotations[prometheusPath] = "/minio/prometheus/metrics"
minInst.Annotations[prometheusPort] = fmt.Sprint(miniov2.MinIOPort)
minInst.Annotations[prometheusScrape] = "true"
}
//Default class name for Log search
diskSpaceFromAPI := int64(5) * humanize.GiByte // Default is 5Gi
logSearchImage := ""
logSearchPgImage := ""
logSearchPgInitImage := ""
var logSearchStorageClass *string // Nil means use default storage class
var logSearchSecurityContext *corev1.PodSecurityContext
var logSearchPgSecurityContext *corev1.PodSecurityContext
if tenantReq.LogSearchConfiguration != nil {
if tenantReq.LogSearchConfiguration.StorageSize != nil {
diskSpaceFromAPI = int64(*tenantReq.LogSearchConfiguration.StorageSize) * humanize.GiByte
}
if tenantReq.LogSearchConfiguration.StorageClass != "" {
logSearchStorageClass = stringPtr(tenantReq.LogSearchConfiguration.StorageClass)
}
if tenantReq.LogSearchConfiguration.Image != "" {
logSearchImage = tenantReq.LogSearchConfiguration.Image
}
if tenantReq.LogSearchConfiguration.PostgresImage != "" {
logSearchPgImage = tenantReq.LogSearchConfiguration.PostgresImage
}
if tenantReq.LogSearchConfiguration.PostgresInitImage != "" {
logSearchPgInitImage = tenantReq.LogSearchConfiguration.PostgresInitImage
}
// if security context for logSearch is present, configure it.
if tenantReq.LogSearchConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
logSearchSecurityContext = sc
}
// if security context for logSearch is present, configure it.
if tenantReq.LogSearchConfiguration.PostgresSecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.LogSearchConfiguration.PostgresSecurityContext)
if err != nil {
return nil, prepareError(err)
}
logSearchPgSecurityContext = sc
}
}
logSearchDiskSpace := resource.NewQuantity(diskSpaceFromAPI, resource.DecimalExponent)
// the audit max cap cannot be larger than disk size on the DB, else it won't trim the data
auditMaxCap := 10
if (diskSpaceFromAPI / humanize.GiByte) < int64(auditMaxCap) {
auditMaxCap = int(diskSpaceFromAPI / humanize.GiByte)
}
// default activate lgo search and prometheus
minInst.Spec.Log = &miniov2.LogConfig{
Audit: &miniov2.AuditConfig{DiskCapacityGB: swag.Int(auditMaxCap)},
Db: &miniov2.LogDbConfig{
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: tenantName + "-log",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: *logSearchDiskSpace,
},
},
StorageClassName: logSearchStorageClass,
},
},
},
}
// set log search images if any
if logSearchImage != "" {
minInst.Spec.Log.Image = logSearchImage
}
if logSearchPgImage != "" {
minInst.Spec.Log.Db.Image = logSearchPgImage
}
if logSearchPgInitImage != "" {
minInst.Spec.Log.Db.InitImage = logSearchPgInitImage
}
if logSearchSecurityContext != nil {
minInst.Spec.Log.SecurityContext = logSearchSecurityContext
}
if logSearchPgSecurityContext != nil {
minInst.Spec.Log.Db.SecurityContext = logSearchPgSecurityContext
}
prometheusDiskSpace := 5 // Default is 5 by API
prometheusImage := "" // Default is ""
prometheusSidecardImage := "" // Default is ""
prometheusInitImage := "" // Default is ""
var prometheusStorageClass *string // Nil means default storage class
if tenantReq.PrometheusConfiguration != nil {
if tenantReq.PrometheusConfiguration.StorageSize != nil {
prometheusDiskSpace = int(*tenantReq.PrometheusConfiguration.StorageSize)
}
if tenantReq.PrometheusConfiguration.StorageClass != "" {
prometheusStorageClass = stringPtr(tenantReq.PrometheusConfiguration.StorageClass)
}
if tenantReq.PrometheusConfiguration.Image != "" {
prometheusImage = tenantReq.PrometheusConfiguration.Image
}
if tenantReq.PrometheusConfiguration.SidecarImage != "" {
prometheusSidecardImage = tenantReq.PrometheusConfiguration.SidecarImage
}
if tenantReq.PrometheusConfiguration.InitImage != "" {
prometheusInitImage = tenantReq.PrometheusConfiguration.InitImage
}
}
minInst.Spec.Prometheus = &miniov2.PrometheusConfig{
DiskCapacityDB: swag.Int(prometheusDiskSpace),
StorageClassName: prometheusStorageClass,
}
if prometheusImage != "" {
minInst.Spec.Prometheus.Image = prometheusImage
}
if prometheusSidecardImage != "" {
minInst.Spec.Prometheus.SideCarImage = prometheusSidecardImage
}
if prometheusInitImage != "" {
minInst.Spec.Prometheus.InitImage = prometheusInitImage
}
// if security context for prometheus is present, configure it.
if tenantReq.PrometheusConfiguration != nil && tenantReq.PrometheusConfiguration.SecurityContext != nil {
sc, err := convertModelSCToK8sSC(tenantReq.PrometheusConfiguration.SecurityContext)
if err != nil {
return nil, prepareError(err)
}
minInst.Spec.Prometheus.SecurityContext = sc
}
// expose services
minInst.Spec.ExposeServices = &miniov2.ExposeServices{
MinIO: tenantReq.ExposeMinio,
Console: tenantReq.ExposeConsole,
}
// write tenant configuration to secret that contains config.env
tenantConfigurationName := fmt.Sprintf("%s-env-configuration", tenantName)
_, err = createOrReplaceSecrets(ctx, &k8sClient, ns, []tenantSecret{
{
Name: tenantConfigurationName,
Content: map[string][]byte{
"config.env": []byte(GenerateTenantConfigurationFile(tenantConfigurationENV)),
},
},
}, tenantName)
if err != nil {
return nil, prepareError(restapi.ErrorGeneric, nil, err)
}
minInst.Spec.Configuration = &corev1.LocalObjectReference{Name: tenantConfigurationName}
opClient, err := cluster.OperatorClient(session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
}
_, err = opClient.MinioV2().Tenants(ns).Create(context.Background(), &minInst, metav1.CreateOptions{})
if err != nil {
restapi.LogError("Creating new tenant failed with: %v", err)
return nil, prepareError(err)
}
// Integrations
if os.Getenv("GKE_INTEGRATION") != "" {
err := gkeIntegration(clientSet, tenantName, ns, session.STSSessionToken)
if err != nil {
return nil, prepareError(err)
}
}
response = &models.CreateTenantResponse{
ExternalIDP: tenantExternalIDPConfigured,
}
thisClient := &operatorClient{
client: opClient,
}
minTenant, err := getTenant(ctx, thisClient, ns, tenantName)
if tenantReq.Idp != nil && !tenantExternalIDPConfigured {
for _, credential := range tenantReq.Idp.Keys {
response.Console = append(response.Console, &models.TenantResponseItem{
AccessKey: *credential.AccessKey,
SecretKey: *credential.SecretKey,
URL: GetTenantServiceURL(minTenant),
})
}
}
return response, nil
}
// setImageRegistry creates a secret to store the private registry credentials, if one exist it updates the existing one
// returns the name of the secret created/updated
func setImageRegistry(ctx context.Context, req *models.ImageRegistry, clientset v1.CoreV1Interface, namespace, tenantName string) (string, error) {

View File

@@ -53,6 +53,8 @@ import Images from "./Steps/Images";
import PageLayout from "../../Common/Layout/PageLayout";
import BackLink from "../../../../common/BackLink";
import TenantResources from "./Steps/TenantResources/TenantResources";
import ConfigLogSearch from "./Steps/ConfigLogSearch";
import ConfigPrometheus from "./Steps/ConfigPrometheus";
interface IAddTenantProps {
setErrorSnackMessage: typeof setErrorSnackMessage;
@@ -175,8 +177,8 @@ const AddTenant = ({
const ecParity = fields.tenantSize.ecParity;
const distribution = fields.tenantSize.distribution;
const tenantCustom = fields.configure.tenantCustom;
const logSearchCustom = fields.configure.logSearchCustom;
const prometheusCustom = fields.configure.prometheusCustom;
const logSearchEnabled = fields.configure.logSearchEnabled;
const prometheusEnabled = fields.configure.prometheusEnabled;
const logSearchVolumeSize = fields.configure.logSearchVolumeSize;
const logSearchSelectedStorageClass =
fields.configure.logSearchSelectedStorageClass;
@@ -313,7 +315,7 @@ const AddTenant = ({
};
}
if (logSearchCustom) {
if (logSearchEnabled) {
dataSend = {
...dataSend,
logSearchConfiguration: {
@@ -329,18 +331,9 @@ const AddTenant = ({
postgres_securityContext: logSearchPostgresSecurityContext,
},
};
} else {
dataSend = {
...dataSend,
logSearchConfiguration: {
image: logSearchImage,
postgres_image: logSearchPostgresImage,
postgres_init_image: logSearchPostgresInitImage,
},
};
}
if (prometheusCustom) {
if (prometheusEnabled) {
dataSend = {
...dataSend,
prometheusConfiguration: {
@@ -355,15 +348,6 @@ const AddTenant = ({
securityContext: prometheusSecurityContext,
},
};
} else {
dataSend = {
...dataSend,
prometheusConfiguration: {
image: prometheusImage,
sidecar_image: prometheusSidecarImage,
init_image: prometheusInitImage,
},
};
}
let tenantCerts: any = null;
@@ -753,6 +737,18 @@ const AddTenant = ({
componentRender: <Encryption />,
buttons: [cancelButton, createButton],
},
{
label: "Audit Log",
advancedOnly: true,
componentRender: <ConfigLogSearch />,
buttons: [cancelButton, createButton],
},
{
label: "Monitoring",
advancedOnly: true,
componentRender: <ConfigPrometheus />,
buttons: [cancelButton, createButton],
},
];
let filteredWizardSteps = wizardSteps;

View File

@@ -0,0 +1,556 @@
// 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 } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, Paper, SelectChangeEvent } from "@mui/material";
import {
createTenantCommon,
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { isPageValid, updateAddField } from "../../actions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { ISecurityContext } from "../../types";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IConfigureProps {
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
storageClasses: any;
classes: any;
logSearchEnabled: boolean;
logSearchVolumeSize: string;
logSearchSizeFactor: string;
logSearchSelectedStorageClass: string;
logSearchImage: string;
logSearchPostgresImage: string;
logSearchPostgresInitImage: string;
selectedStorageClass: string;
tenantSecurityContext: ISecurityContext;
logSearchSecurityContext: ISecurityContext;
logSearchPostgresSecurityContext: ISecurityContext;
}
const styles = (theme: Theme) =>
createStyles({
configSectionItem: {
marginRight: 15,
"& .multiContainer": {
border: "1px solid red",
},
},
containerItem: {
marginRight: 15,
},
fieldGroup: {
...createTenantCommon.fieldGroup,
paddingTop: 15,
marginBottom: 25,
},
responsiveSectionItem: {
"@media (max-width: 900px)": {
flexFlow: "column",
alignItems: "flex-start",
"& div > div": {
marginBottom: 5,
marginRight: 0,
},
},
},
logSearchEnabledFields: {
marginLeft: 20, // 2nd Level(15+15)
padding: 10,
width: "90%",
margin: "auto",
},
fieldSpaceTop: {
marginTop: 15,
},
...modalBasic,
...wizardCommon,
});
const ConfigLogSearch = ({
classes,
storageClasses,
logSearchEnabled,
logSearchVolumeSize,
logSearchSizeFactor,
logSearchImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
logSearchSelectedStorageClass,
updateAddField,
isPageValid,
selectedStorageClass,
tenantSecurityContext,
logSearchSecurityContext,
logSearchPostgresSecurityContext,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const configureSTClasses = [
{ label: "Default", value: "default" },
...storageClasses,
];
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("configure", field, value);
},
[updateAddField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (logSearchEnabled) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "log_search_storage_class",
required: true,
value: logSearchSelectedStorageClass,
customValidation: logSearchSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "log_search_volume_size",
required: true,
value: logSearchVolumeSize,
customValidation:
logSearchVolumeSize === "" || parseInt(logSearchVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greatter than 0`,
},
{
fieldKey: "logSearch_securityContext_runAsUser",
required: true,
value: logSearchSecurityContext.runAsUser,
customValidation:
logSearchSecurityContext.runAsUser === "" ||
parseInt(logSearchSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "logSearch_securityContext_runAsGroup",
required: true,
value: logSearchSecurityContext.runAsGroup,
customValidation:
logSearchSecurityContext.runAsGroup === "" ||
parseInt(logSearchSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "logSearch_securityContext_fsGroup",
required: true,
value: logSearchSecurityContext.fsGroup,
customValidation:
logSearchSecurityContext.fsGroup === "" ||
parseInt(logSearchSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_runAsUser",
required: true,
value: logSearchPostgresSecurityContext.runAsUser,
customValidation:
logSearchPostgresSecurityContext.runAsUser === "" ||
parseInt(logSearchPostgresSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_runAsGroup",
required: true,
value: logSearchSecurityContext.runAsGroup,
customValidation:
logSearchPostgresSecurityContext.runAsGroup === "" ||
parseInt(logSearchPostgresSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_fsGroup",
required: true,
value: logSearchPostgresSecurityContext.fsGroup,
customValidation:
logSearchPostgresSecurityContext.fsGroup === "" ||
parseInt(logSearchPostgresSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
logSearchImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
isPageValid,
logSearchEnabled,
logSearchSelectedStorageClass,
logSearchVolumeSize,
tenantSecurityContext,
logSearchSecurityContext,
logSearchPostgresSecurityContext,
]);
useEffect(() => {
// New default values in current selection is invalid
if (storageClasses.length > 0) {
const filterLogSearch = storageClasses.filter(
(item: any) => item.value === logSearchSelectedStorageClass
);
if (filterLogSearch.length === 0) {
updateField("logSearchSelectedStorageClass", "default");
}
}
}, [
logSearchSelectedStorageClass,
selectedStorageClass,
storageClasses,
updateField,
]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Audit Log</h3>
<span className={classes.descriptionText}>
Audit log deploys a small PostgreSQL database and store access logs of
all calls into the tenant.
</span>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="logSearchConfig"
id="log_search_configuration"
name="log_search_configuration"
checked={logSearchEnabled}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchEnabled", checked);
}}
label={"Enabled"}
/>
</Grid>
{logSearchEnabled && (
<Grid xs={12} className={classes.logSearchEnabledFields}>
<Grid item xs={12}>
<SelectWrapper
id="log_search_storage_class"
name="log_search_storage_class"
onChange={(e: SelectChangeEvent<string>) => {
updateField(
"logSearchSelectedStorageClass",
e.target.value as string
);
}}
label="Log Search Storage Class"
value={logSearchSelectedStorageClass}
options={configureSTClasses}
disabled={configureSTClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<InputBoxWrapper
type="number"
id="log_search_volume_size"
name="log_search_volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchVolumeSize", e.target.value);
cleanValidation("log_search_volume_size");
}}
label="Storage Size"
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
value={logSearchVolumeSize}
required
error={validationErrors["log_search_volume_size"] || ""}
min="0"
/>
</div>
</Grid>
<fieldset
className={`${classes.fieldGroup} ${classes.fieldSpaceTop}`}
>
<legend className={classes.descriptionText}>
SecurityContext for LogSearch
</legend>
<Grid item xs={12}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_runAsUser"
name="logSearch_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("logSearch_securityContext_runAsUser");
}}
label="Run As User"
value={logSearchSecurityContext.runAsUser}
required
error={
validationErrors["logSearch_securityContext_runAsUser"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_runAsGroup"
name="logSearch_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("logSearch_securityContext_runAsGroup");
}}
label="Run As Group"
value={logSearchSecurityContext.runAsGroup}
required
error={
validationErrors[
"logSearch_securityContext_runAsGroup"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_fsGroup"
name="logSearch_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("logSearch_securityContext_fsGroup");
}}
label="FsGroup"
value={logSearchSecurityContext.fsGroup}
required
error={
validationErrors["logSearch_securityContext_fsGroup"] ||
""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="logSearchSecurityContextRunAsNonRoot"
id="logSearch_securityContext_runAsNonRoot"
name="logSearch_securityContext_runAsNonRoot"
checked={logSearchSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
SecurityContext for PostgreSQL
</legend>
<Grid item xs={12}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_runAsUser"
name="postgres_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("postgres_securityContext_runAsUser");
}}
label="Run As User"
value={logSearchPostgresSecurityContext.runAsUser}
required
error={
validationErrors["postgres_securityContext_runAsUser"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_runAsGroup"
name="postgres_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("postgres_securityContext_runAsGroup");
}}
label="Run As Group"
value={logSearchPostgresSecurityContext.runAsGroup}
required
error={
validationErrors["postgres_securityContext_runAsGroup"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_fsGroup"
name="postgres_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("postgres_securityContext_fsGroup");
}}
label="FsGroup"
value={logSearchPostgresSecurityContext.fsGroup}
required
error={
validationErrors["postgres_securityContext_fsGroup"] || ""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="postgresSecurityContextRunAsNonRoot"
id="postgres_securityContext_runAsNonRoot"
name="postgres_securityContext_runAsNonRoot"
checked={logSearchPostgresSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
</Paper>
);
};
const mapState = (state: AppState) => ({
storageClasses: state.tenants.createTenant.storageClasses,
logSearchEnabled:
state.tenants.createTenant.fields.configure.logSearchEnabled,
logSearchVolumeSize:
state.tenants.createTenant.fields.configure.logSearchVolumeSize,
logSearchSizeFactor:
state.tenants.createTenant.fields.configure.logSearchSizeFactor,
logSearchSelectedStorageClass:
state.tenants.createTenant.fields.configure.logSearchSelectedStorageClass,
logSearchImage: state.tenants.createTenant.fields.configure.logSearchImage,
logSearchPostgresImage:
state.tenants.createTenant.fields.configure.logSearchPostgresImage,
logSearchPostgresInitImage:
state.tenants.createTenant.fields.configure.logSearchPostgresInitImage,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
tenantSecurityContext:
state.tenants.createTenant.fields.configure.tenantSecurityContext,
logSearchSecurityContext:
state.tenants.createTenant.fields.configure.logSearchSecurityContext,
logSearchPostgresSecurityContext:
state.tenants.createTenant.fields.configure
.logSearchPostgresSecurityContext,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(ConfigLogSearch));

View File

@@ -0,0 +1,424 @@
// 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 } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, Paper, SelectChangeEvent } from "@mui/material";
import {
createTenantCommon,
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { isPageValid, updateAddField } from "../../actions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { ISecurityContext } from "../../types";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IConfigureProps {
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
storageClasses: any;
classes: any;
prometheusEnabled: boolean;
prometheusVolumeSize: string;
prometheusSizeFactor: string;
prometheusSelectedStorageClass: string;
prometheusImage: string;
prometheusSidecarImage: string;
prometheusInitImage: string;
selectedStorageClass: string;
tenantSecurityContext: ISecurityContext;
prometheusSecurityContext: ISecurityContext;
}
const styles = (theme: Theme) =>
createStyles({
configSectionItem: {
marginRight: 15,
"& .multiContainer": {
border: "1px solid red",
},
},
containerItem: {
marginRight: 15,
},
fieldGroup: {
...createTenantCommon.fieldGroup,
paddingTop: 15,
marginBottom: 25,
},
responsiveSectionItem: {
"@media (max-width: 900px)": {
flexFlow: "column",
alignItems: "flex-start",
"& div > div": {
marginBottom: 5,
marginRight: 0,
},
},
},
fieldSpaceTop: {
marginTop: 15,
},
prometheusEnabledFields: {
marginLeft: 20, // 2nd Level(15+15)
padding: 10,
width: "90%",
margin: "auto",
},
...modalBasic,
...wizardCommon,
});
const ConfigPrometheus = ({
classes,
storageClasses,
prometheusEnabled,
prometheusVolumeSize,
prometheusSizeFactor,
prometheusSelectedStorageClass,
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
updateAddField,
isPageValid,
selectedStorageClass,
tenantSecurityContext,
prometheusSecurityContext,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const configureSTClasses = [
{ label: "Default", value: "default" },
...storageClasses,
];
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("configure", field, value);
},
[updateAddField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (prometheusEnabled) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "prometheus_storage_class",
required: true,
value: prometheusSelectedStorageClass,
customValidation: prometheusSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "prometheus_volume_size",
required: true,
value: prometheusVolumeSize,
customValidation:
prometheusVolumeSize === "" || parseInt(prometheusVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greater than 0`,
},
{
fieldKey: "prometheus_securityContext_runAsUser",
required: true,
value: prometheusSecurityContext.runAsUser,
customValidation:
prometheusSecurityContext.runAsUser === "" ||
parseInt(prometheusSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "prometheus_securityContext_runAsGroup",
required: true,
value: prometheusSecurityContext.runAsGroup,
customValidation:
prometheusSecurityContext.runAsGroup === "" ||
parseInt(prometheusSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "prometheus_securityContext_fsGroup",
required: true,
value: prometheusSecurityContext.fsGroup,
customValidation:
prometheusSecurityContext.fsGroup === "" ||
parseInt(prometheusSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
isPageValid,
prometheusEnabled,
prometheusSelectedStorageClass,
prometheusVolumeSize,
tenantSecurityContext,
prometheusSecurityContext,
]);
useEffect(() => {
// New default values in current selection is invalid
if (storageClasses.length > 0) {
const filterPrometheus = storageClasses.filter(
(item: any) => item.value === prometheusSelectedStorageClass
);
if (filterPrometheus.length === 0) {
updateField("prometheusSelectedStorageClass", "default");
}
}
}, [
prometheusSelectedStorageClass,
selectedStorageClass,
storageClasses,
updateField,
]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Monitoring</h3>
<span className={classes.descriptionText}>
A small Prometheus will be deployed to keep metrics about the tenant.
</span>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="prometheusConfig"
id="prometheus_configuration"
name="prometheus_configuration"
checked={prometheusEnabled}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("prometheusEnabled", checked);
}}
label={"Enabled"}
/>
</Grid>
{prometheusEnabled && (
<Grid xs={12} className={classes.prometheusEnabledFields}>
<Grid item xs={12}>
<SelectWrapper
id="prometheus_storage_class"
name="prometheus_storage_class"
onChange={(e: SelectChangeEvent<string>) => {
updateField(
"prometheusSelectedStorageClass",
e.target.value as string
);
}}
label="Prometheus Storage Class"
value={prometheusSelectedStorageClass}
options={configureSTClasses}
disabled={configureSTClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<InputBoxWrapper
type="number"
id="prometheus_volume_size"
name="prometheus_volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusVolumeSize", e.target.value);
cleanValidation("prometheus_volume_size");
}}
label="Storage Size"
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
value={prometheusVolumeSize}
required
error={validationErrors["prometheus_volume_size"] || ""}
min="0"
/>
</div>
</Grid>
<fieldset
className={`${classes.fieldGroup} ${classes.fieldSpaceTop}`}
>
<legend className={classes.descriptionText}>
SecurityContext for Prometheus
</legend>
<Grid item xs={12} className={classes.configSectionItem}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_runAsUser"
name="prometheus_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("prometheus_securityContext_runAsUser");
}}
label="Run As User"
value={prometheusSecurityContext.runAsUser}
required
error={
validationErrors[
"prometheus_securityContext_runAsUser"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_runAsGroup"
name="prometheus_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("prometheus_securityContext_runAsGroup");
}}
label="Run As Group"
value={prometheusSecurityContext.runAsGroup}
required
error={
validationErrors[
"prometheus_securityContext_runAsGroup"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_fsGroup"
name="prometheus_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("prometheus_securityContext_fsGroup");
}}
label="FsGroup"
value={prometheusSecurityContext.fsGroup}
required
error={
validationErrors["prometheus_securityContext_fsGroup"] ||
""
}
min="0"
/>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.configSectionItem}>
<div
className={`${classes.multiContainer} ${classes.fieldSpaceTop}`}
>
<FormSwitchWrapper
value="prometheusSecurityContextRunAsNonRoot"
id="prometheus_securityContext_runAsNonRoot"
name="prometheus_securityContext_runAsNonRoot"
checked={prometheusSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
</Paper>
);
};
const mapState = (state: AppState) => ({
storageClasses: state.tenants.createTenant.storageClasses,
prometheusEnabled:
state.tenants.createTenant.fields.configure.prometheusEnabled,
prometheusVolumeSize:
state.tenants.createTenant.fields.configure.prometheusVolumeSize,
prometheusSizeFactor:
state.tenants.createTenant.fields.configure.prometheusSizeFactor,
prometheusSelectedStorageClass:
state.tenants.createTenant.fields.configure.prometheusSelectedStorageClass,
prometheusImage: state.tenants.createTenant.fields.configure.prometheusImage,
prometheusSidecarImage:
state.tenants.createTenant.fields.configure.prometheusSidecarImage,
prometheusInitImage:
state.tenants.createTenant.fields.configure.prometheusInitImage,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
tenantSecurityContext:
state.tenants.createTenant.fields.configure.tenantSecurityContext,
prometheusSecurityContext:
state.tenants.createTenant.fields.configure.prometheusSecurityContext,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(ConfigPrometheus));

View File

@@ -19,7 +19,7 @@ import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, Paper, SelectChangeEvent } from "@mui/material";
import { Grid, Paper } from "@mui/material";
import {
createTenantCommon,
modalBasic,
@@ -34,44 +34,16 @@ import {
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { ISecurityContext } from "../../types";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IConfigureProps {
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
storageClasses: any;
classes: any;
customImage: boolean;
imageName: string;
customDockerhub: boolean;
imageRegistry: string;
imageRegistryUsername: string;
imageRegistryPassword: string;
exposeMinIO: boolean;
exposeConsole: boolean;
prometheusCustom: boolean;
tenantCustom: boolean;
logSearchCustom: boolean;
logSearchVolumeSize: string;
logSearchSizeFactor: string;
prometheusVolumeSize: string;
prometheusSizeFactor: string;
logSearchSelectedStorageClass: string;
logSearchImage: string;
kesImage: string;
logSearchPostgresImage: string;
logSearchPostgresInitImage: string;
prometheusSelectedStorageClass: string;
prometheusImage: string;
prometheusSidecarImage: string;
prometheusInitImage: string;
selectedStorageClass: string;
tenantSecurityContext: ISecurityContext;
logSearchSecurityContext: ISecurityContext;
logSearchPostgresSecurityContext: ISecurityContext;
prometheusSecurityContext: ISecurityContext;
}
const styles = (theme: Theme) =>
@@ -108,67 +80,25 @@ const styles = (theme: Theme) =>
},
},
logSearchCustomFields: {
marginLeft: 20, // 2nd Level(15+15)
padding: 10,
width: "90%",
margin: "auto",
},
fieldSpaceTop: {
marginTop: 15,
},
prometheusCustomFields: {
marginLeft: 20, // 2nd Level(15+15)
padding: 10,
width: "90%",
margin: "auto",
},
...modalBasic,
...wizardCommon,
});
const Configure = ({
classes,
storageClasses,
customImage,
imageName,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
exposeMinIO,
exposeConsole,
prometheusCustom,
tenantCustom,
logSearchCustom,
logSearchVolumeSize,
logSearchSizeFactor,
logSearchImage,
kesImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
prometheusVolumeSize,
prometheusSizeFactor,
logSearchSelectedStorageClass,
prometheusSelectedStorageClass,
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
updateAddField,
isPageValid,
selectedStorageClass,
tenantSecurityContext,
logSearchSecurityContext,
logSearchPostgresSecurityContext,
prometheusSecurityContext,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const configureSTClasses = [
{ label: "Default", value: "default" },
...storageClasses,
];
// Common
const updateField = useCallback(
(field: string, value: any) => {
@@ -212,273 +142,13 @@ const Configure = ({
},
];
}
if (prometheusCustom) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "prometheus_storage_class",
required: true,
value: prometheusSelectedStorageClass,
customValidation: prometheusSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "prometheus_volume_size",
required: true,
value: prometheusVolumeSize,
customValidation:
prometheusVolumeSize === "" || parseInt(prometheusVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greater than 0`,
},
{
fieldKey: "prometheus_securityContext_runAsUser",
required: true,
value: prometheusSecurityContext.runAsUser,
customValidation:
prometheusSecurityContext.runAsUser === "" ||
parseInt(prometheusSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "prometheus_securityContext_runAsGroup",
required: true,
value: prometheusSecurityContext.runAsGroup,
customValidation:
prometheusSecurityContext.runAsGroup === "" ||
parseInt(prometheusSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "prometheus_securityContext_fsGroup",
required: true,
value: prometheusSecurityContext.fsGroup,
customValidation:
prometheusSecurityContext.fsGroup === "" ||
parseInt(prometheusSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
if (logSearchCustom) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "log_search_storage_class",
required: true,
value: logSearchSelectedStorageClass,
customValidation: logSearchSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "log_search_volume_size",
required: true,
value: logSearchVolumeSize,
customValidation:
logSearchVolumeSize === "" || parseInt(logSearchVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greatter than 0`,
},
{
fieldKey: "logSearch_securityContext_runAsUser",
required: true,
value: logSearchSecurityContext.runAsUser,
customValidation:
logSearchSecurityContext.runAsUser === "" ||
parseInt(logSearchSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "logSearch_securityContext_runAsGroup",
required: true,
value: logSearchSecurityContext.runAsGroup,
customValidation:
logSearchSecurityContext.runAsGroup === "" ||
parseInt(logSearchSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "logSearch_securityContext_fsGroup",
required: true,
value: logSearchSecurityContext.fsGroup,
customValidation:
logSearchSecurityContext.fsGroup === "" ||
parseInt(logSearchSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_runAsUser",
required: true,
value: logSearchPostgresSecurityContext.runAsUser,
customValidation:
logSearchPostgresSecurityContext.runAsUser === "" ||
parseInt(logSearchPostgresSecurityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_runAsGroup",
required: true,
value: prometheusSecurityContext.runAsGroup,
customValidation:
logSearchPostgresSecurityContext.runAsGroup === "" ||
parseInt(logSearchPostgresSecurityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "postgres_securityContext_fsGroup",
required: true,
value: logSearchPostgresSecurityContext.fsGroup,
customValidation:
logSearchPostgresSecurityContext.fsGroup === "" ||
parseInt(logSearchPostgresSecurityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
if (customImage) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "image",
required: false,
value: imageName,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/minio:VERSION'",
},
{
fieldKey: "logSearchImage",
required: false,
value: logSearchImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/logsearchapi:VERSION'",
},
{
fieldKey: "kesImage",
required: false,
value: kesImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/kes:VERSION'",
},
{
fieldKey: "logSearchPostgresImage",
required: false,
value: logSearchPostgresImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/postgres:VERSION'",
},
{
fieldKey: "logSearchPostgresInitImage",
required: false,
value: logSearchPostgresInitImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/busybox:VERSION'",
},
{
fieldKey: "prometheusImage",
required: false,
value: prometheusImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/prometheus:VERSION'",
},
{
fieldKey: "prometheusSidecarImage",
required: false,
value: prometheusSidecarImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'project/container:VERSION'",
},
{
fieldKey: "prometheusInitImage",
required: false,
value: prometheusInitImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/busybox:VERSION'",
},
];
if (customDockerhub) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "registry",
required: true,
value: imageRegistry,
},
{
fieldKey: "registryUsername",
required: true,
value: imageRegistryUsername,
},
{
fieldKey: "registryPassword",
required: true,
value: imageRegistryPassword,
},
];
}
}
const commonVal = commonFormValidation(customAccountValidation);
isPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
customImage,
imageName,
logSearchImage,
kesImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
isPageValid,
prometheusCustom,
tenantCustom,
logSearchCustom,
prometheusSelectedStorageClass,
prometheusVolumeSize,
logSearchSelectedStorageClass,
logSearchVolumeSize,
tenantSecurityContext,
logSearchSecurityContext,
logSearchPostgresSecurityContext,
prometheusSecurityContext,
]);
useEffect(() => {
// New default values in current selection is invalid
if (storageClasses.length > 0) {
const filterPrometheus = storageClasses.filter(
(item: any) => item.value === prometheusSelectedStorageClass
);
if (filterPrometheus.length === 0) {
updateField("prometheusSelectedStorageClass", "default");
}
const filterLogSearch = storageClasses.filter(
(item: any) => item.value === logSearchSelectedStorageClass
);
if (filterLogSearch.length === 0) {
updateField("logSearchSelectedStorageClass", "default");
}
}
}, [
logSearchSelectedStorageClass,
prometheusSelectedStorageClass,
selectedStorageClass,
storageClasses,
updateField,
]);
}, [isPageValid, tenantCustom, tenantSecurityContext]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
@@ -493,9 +163,10 @@ const Configure = ({
</span>
</div>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Expose Services</h3>
<h3 className={classes.h3Section}>Services</h3>
<span className={classes.descriptionText}>
Whether the tenant's services should request an external IP.
Whether the tenant's services should request an external IP via
LoadBalancer service type.
</span>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
@@ -529,13 +200,6 @@ const Configure = ({
/>
</Grid>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Additional Configurations</h3>
<span className={classes.descriptionText}>
Configure SecurityContext, Storage Classes & Storage size for Log
Search, Prometheus add-ons and your Tenant
</span>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="tenantConfig"
@@ -548,7 +212,7 @@ const Configure = ({
updateField("tenantCustom", checked);
}}
label={"Override Tenant defaults"}
label={"Security Context"}
/>
</Grid>
{tenantCustom && (
@@ -650,485 +314,16 @@ const Configure = ({
</fieldset>
</Grid>
)}
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="logSearchConfig"
id="log_search_configuration"
name="log_search_configuration"
checked={logSearchCustom}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchCustom", checked);
}}
label={"Override Log Search defaults"}
/>
</Grid>
{logSearchCustom && (
<Grid xs={12} className={classes.logSearchCustomFields}>
<Grid item xs={12}>
<SelectWrapper
id="log_search_storage_class"
name="log_search_storage_class"
onChange={(e: SelectChangeEvent<string>) => {
updateField(
"logSearchSelectedStorageClass",
e.target.value as string
);
}}
label="Log Search Storage Class"
value={logSearchSelectedStorageClass}
options={configureSTClasses}
disabled={configureSTClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<InputBoxWrapper
type="number"
id="log_search_volume_size"
name="log_search_volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchVolumeSize", e.target.value);
cleanValidation("log_search_volume_size");
}}
label="Storage Size"
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
value={logSearchVolumeSize}
required
error={validationErrors["log_search_volume_size"] || ""}
min="0"
/>
</div>
</Grid>
<fieldset
className={`${classes.fieldGroup} ${classes.fieldSpaceTop}`}
>
<legend className={classes.descriptionText}>
SecurityContext for LogSearch
</legend>
<Grid item xs={12}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_runAsUser"
name="logSearch_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("logSearch_securityContext_runAsUser");
}}
label="Run As User"
value={logSearchSecurityContext.runAsUser}
required
error={
validationErrors["logSearch_securityContext_runAsUser"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_runAsGroup"
name="logSearch_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("logSearch_securityContext_runAsGroup");
}}
label="Run As Group"
value={logSearchSecurityContext.runAsGroup}
required
error={
validationErrors[
"logSearch_securityContext_runAsGroup"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="logSearch_securityContext_fsGroup"
name="logSearch_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("logSearch_securityContext_fsGroup");
}}
label="FsGroup"
value={logSearchSecurityContext.fsGroup}
required
error={
validationErrors["logSearch_securityContext_fsGroup"] ||
""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="logSearchSecurityContextRunAsNonRoot"
id="logSearch_securityContext_runAsNonRoot"
name="logSearch_securityContext_runAsNonRoot"
checked={logSearchSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchSecurityContext", {
...logSearchSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
SecurityContext for PostgreSQL
</legend>
<Grid item xs={12}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_runAsUser"
name="postgres_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("postgres_securityContext_runAsUser");
}}
label="Run As User"
value={logSearchPostgresSecurityContext.runAsUser}
required
error={
validationErrors["postgres_securityContext_runAsUser"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_runAsGroup"
name="postgres_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("postgres_securityContext_runAsGroup");
}}
label="Run As Group"
value={logSearchPostgresSecurityContext.runAsGroup}
required
error={
validationErrors["postgres_securityContext_runAsGroup"] ||
""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="postgres_securityContext_fsGroup"
name="postgres_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("postgres_securityContext_fsGroup");
}}
label="FsGroup"
value={logSearchPostgresSecurityContext.fsGroup}
required
error={
validationErrors["postgres_securityContext_fsGroup"] || ""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="postgresSecurityContextRunAsNonRoot"
id="postgres_securityContext_runAsNonRoot"
name="postgres_securityContext_runAsNonRoot"
checked={logSearchPostgresSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("logSearchPostgresSecurityContext", {
...logSearchPostgresSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="prometheusConfig"
id="prometheus_configuration"
name="prometheus_configuration"
checked={prometheusCustom}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("prometheusCustom", checked);
}}
label={"Override Prometheus defaults"}
/>
</Grid>
{prometheusCustom && (
<Grid xs={12} className={classes.prometheusCustomFields}>
<Grid item xs={12}>
<SelectWrapper
id="prometheus_storage_class"
name="prometheus_storage_class"
onChange={(e: SelectChangeEvent<string>) => {
updateField(
"prometheusSelectedStorageClass",
e.target.value as string
);
}}
label="Prometheus Storage Class"
value={prometheusSelectedStorageClass}
options={configureSTClasses}
disabled={configureSTClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<InputBoxWrapper
type="number"
id="prometheus_volume_size"
name="prometheus_volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusVolumeSize", e.target.value);
cleanValidation("prometheus_volume_size");
}}
label="Storage Size"
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
value={prometheusVolumeSize}
required
error={validationErrors["prometheus_volume_size"] || ""}
min="0"
/>
</div>
</Grid>
<fieldset
className={`${classes.fieldGroup} ${classes.fieldSpaceTop}`}
>
<legend className={classes.descriptionText}>
SecurityContext for Prometheus
</legend>
<Grid item xs={12} className={classes.configSectionItem}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_runAsUser"
name="prometheus_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsUser: e.target.value,
});
cleanValidation("prometheus_securityContext_runAsUser");
}}
label="Run As User"
value={prometheusSecurityContext.runAsUser}
required
error={
validationErrors[
"prometheus_securityContext_runAsUser"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_runAsGroup"
name="prometheus_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsGroup: e.target.value,
});
cleanValidation("prometheus_securityContext_runAsGroup");
}}
label="Run As Group"
value={prometheusSecurityContext.runAsGroup}
required
error={
validationErrors[
"prometheus_securityContext_runAsGroup"
] || ""
}
min="0"
/>
</div>
<div className={classes.configSectionItem}>
<InputBoxWrapper
type="number"
id="prometheus_securityContext_fsGroup"
name="prometheus_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
fsGroup: e.target.value,
});
cleanValidation("prometheus_securityContext_fsGroup");
}}
label="FsGroup"
value={prometheusSecurityContext.fsGroup}
required
error={
validationErrors["prometheus_securityContext_fsGroup"] ||
""
}
min="0"
/>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.configSectionItem}>
<div
className={`${classes.multiContainer} ${classes.fieldSpaceTop}`}
>
<FormSwitchWrapper
value="prometheusSecurityContextRunAsNonRoot"
id="prometheus_securityContext_runAsNonRoot"
name="prometheus_securityContext_runAsNonRoot"
checked={prometheusSecurityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("prometheusSecurityContext", {
...prometheusSecurityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
</Paper>
);
};
const mapState = (state: AppState) => ({
storageClasses: state.tenants.createTenant.storageClasses,
customImage: state.tenants.createTenant.fields.configure.customImage,
imageName: state.tenants.createTenant.fields.configure.imageName,
customDockerhub: state.tenants.createTenant.fields.configure.customDockerhub,
imageRegistry: state.tenants.createTenant.fields.configure.imageRegistry,
imageRegistryUsername:
state.tenants.createTenant.fields.configure.imageRegistryUsername,
imageRegistryPassword:
state.tenants.createTenant.fields.configure.imageRegistryPassword,
exposeMinIO: state.tenants.createTenant.fields.configure.exposeMinIO,
exposeConsole: state.tenants.createTenant.fields.configure.exposeConsole,
prometheusCustom:
state.tenants.createTenant.fields.configure.prometheusCustom,
tenantCustom: state.tenants.createTenant.fields.configure.tenantCustom,
logSearchCustom: state.tenants.createTenant.fields.configure.logSearchCustom,
logSearchVolumeSize:
state.tenants.createTenant.fields.configure.logSearchVolumeSize,
logSearchSizeFactor:
state.tenants.createTenant.fields.configure.logSearchSizeFactor,
prometheusVolumeSize:
state.tenants.createTenant.fields.configure.prometheusVolumeSize,
prometheusSizeFactor:
state.tenants.createTenant.fields.configure.prometheusSizeFactor,
logSearchSelectedStorageClass:
state.tenants.createTenant.fields.configure.logSearchSelectedStorageClass,
logSearchImage: state.tenants.createTenant.fields.configure.logSearchImage,
kesImage: state.tenants.createTenant.fields.configure.kesImage,
logSearchPostgresImage:
state.tenants.createTenant.fields.configure.logSearchPostgresImage,
logSearchPostgresInitImage:
state.tenants.createTenant.fields.configure.logSearchPostgresInitImage,
prometheusSelectedStorageClass:
state.tenants.createTenant.fields.configure.prometheusSelectedStorageClass,
prometheusImage: state.tenants.createTenant.fields.configure.prometheusImage,
prometheusSidecarImage:
state.tenants.createTenant.fields.configure.prometheusSidecarImage,
prometheusInitImage:
state.tenants.createTenant.fields.configure.prometheusInitImage,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
tenantSecurityContext:
state.tenants.createTenant.fields.configure.tenantSecurityContext,
logSearchSecurityContext:
state.tenants.createTenant.fields.configure.logSearchSecurityContext,
logSearchPostgresSecurityContext:
state.tenants.createTenant.fields.configure
.logSearchPostgresSecurityContext,
prometheusSecurityContext:
state.tenants.createTenant.fields.configure.prometheusSecurityContext,
});
const connector = connect(mapState, {

View File

@@ -176,7 +176,7 @@ const Images = ({
value: logSearchImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/logsearchapi:VERSION'",
"Format must be of form: 'minio/operator:VERSION'",
},
{
fieldKey: "kesImage",
@@ -286,7 +286,7 @@ const Images = ({
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Container Images</h3>
<span className={classes.descriptionText}>
Images used by the Tenant Deployment
Specify the container images used by the Tenant and it's features.
</span>
</div>
@@ -299,12 +299,30 @@ const Images = ({
updateField("imageName", e.target.value);
cleanValidation("image");
}}
label="MinIO's Image"
label="MinIO"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="E.g. minio/minio:RELEASE.2022-01-08T03-11-54Z"
placeholder="E.g. minio/minio:RELEASE.2022-02-26T02-54-46Z"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="kesImage"
name="kesImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("kesImage", e.target.value);
cleanValidation("kesImage");
}}
label="KES"
value={kesImage}
error={validationErrors["kesImage"] || ""}
placeholder="E.g. minio/kes:v0.17.6"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<h4>Log Search</h4>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="logSearchImage"
@@ -313,24 +331,10 @@ const Images = ({
updateField("logSearchImage", e.target.value);
cleanValidation("logSearchImage");
}}
label="Log Search API's Image"
label="API"
value={logSearchImage}
error={validationErrors["logSearchImage"] || ""}
placeholder="E.g. minio/logsearchapi:v4.1.1"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="kesImage"
name="kesImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("kesImage", e.target.value);
cleanValidation("kesImage");
}}
label="KES Image"
value={kesImage}
error={validationErrors["kesImage"] || ""}
placeholder="E.g. minio/kes:v0.14.0"
placeholder="E.g. minio/operator:v4.4.10"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
@@ -341,7 +345,7 @@ const Images = ({
updateField("logSearchPostgresImage", e.target.value);
cleanValidation("logSearchPostgresImage");
}}
label="Log Search Postgres's Image"
label="PostgreSQL"
value={logSearchPostgresImage}
error={validationErrors["logSearchPostgresImage"] || ""}
placeholder="E.g. library/postgres:13"
@@ -355,12 +359,15 @@ const Images = ({
updateField("logSearchPostgresInitImage", e.target.value);
cleanValidation("logSearchPostgresInitImage");
}}
label="Log Search Postgres's Init Image"
label="PostgreSQL Init"
value={logSearchPostgresInitImage}
error={validationErrors["logSearchPostgresInitImage"] || ""}
placeholder="E.g. library/busybox:1.33.1"
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<h4>Monitoring</h4>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="prometheusImage"
@@ -369,7 +376,7 @@ const Images = ({
updateField("prometheusImage", e.target.value);
cleanValidation("prometheusImage");
}}
label="Prometheus Image"
label="Prometheus"
value={prometheusImage}
error={validationErrors["prometheusImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
@@ -383,7 +390,7 @@ const Images = ({
updateField("prometheusSidecarImage", e.target.value);
cleanValidation("prometheusSidecarImage");
}}
label="Prometheus Sidecar Image"
label="Prometheus Sidecar"
value={prometheusSidecarImage}
error={validationErrors["prometheusSidecarImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
@@ -397,7 +404,7 @@ const Images = ({
updateField("prometheusInitImage", e.target.value);
cleanValidation("prometheusInitImage");
}}
label="Prometheus Init Image"
label="Prometheus Init"
value={prometheusInitImage}
error={validationErrors["prometheusInitImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
@@ -407,6 +414,9 @@ const Images = ({
{customImage && (
<Fragment>
<Grid item xs={12} className={classes.formFieldRow}>
<h4>Custom Container Registry</h4>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<FormSwitchWrapper
value="custom_docker_hub"
@@ -419,7 +429,7 @@ const Images = ({
updateField("customDockerhub", checked);
}}
label={"Set/Update Image Registry"}
label={"Use a private container registry"}
/>
</Grid>
</Fragment>
@@ -485,9 +495,9 @@ const mapState = (state: AppState) => ({
exposeMinIO: state.tenants.createTenant.fields.configure.exposeMinIO,
exposeConsole: state.tenants.createTenant.fields.configure.exposeConsole,
prometheusCustom:
state.tenants.createTenant.fields.configure.prometheusCustom,
state.tenants.createTenant.fields.configure.prometheusEnabled,
tenantCustom: state.tenants.createTenant.fields.configure.tenantCustom,
logSearchCustom: state.tenants.createTenant.fields.configure.logSearchCustom,
logSearchCustom: state.tenants.createTenant.fields.configure.logSearchEnabled,
logSearchVolumeSize:
state.tenants.createTenant.fields.configure.logSearchVolumeSize,
logSearchSizeFactor:

View File

@@ -158,7 +158,7 @@ const UpdateTenantModal = ({
label={"MinIO's Image"}
id={"minioImage"}
name={"minioImage"}
placeholder={"E.g. minio/minio:RELEASE.2022-01-08T03-11-54Z"}
placeholder={"E.g. minio/minio:RELEASE.2022-02-26T02-54-46Z"}
onChange={(e) => {
setMinioImage(e.target.value);
}}

View File

@@ -80,8 +80,8 @@ const initialState: ITenantState = {
exposeMinIO: true,
exposeConsole: true,
tenantCustom: false,
logSearchCustom: false,
prometheusCustom: false,
logSearchEnabled: true,
prometheusEnabled: true,
logSearchVolumeSize: "5",
logSearchSizeFactor: "Gi",
logSearchImage: "",
@@ -632,8 +632,8 @@ export function tenantsReducer(
exposeMinIO: true,
exposeConsole: true,
tenantCustom: false,
logSearchCustom: false,
prometheusCustom: false,
logSearchEnabled: true,
prometheusEnabled: true,
logSearchVolumeSize: "5",
logSearchSizeFactor: "Gi",
logSearchSelectedStorageClass: "default",

View File

@@ -205,9 +205,9 @@ export interface IConfigureFields {
imageRegistryPassword: string;
exposeMinIO: boolean;
exposeConsole: boolean;
prometheusCustom: boolean;
prometheusEnabled: boolean;
tenantCustom: boolean;
logSearchCustom: boolean;
logSearchEnabled: boolean;
logSearchVolumeSize: string;
logSearchSizeFactor: string;
logSearchSelectedStorageClass: string;