Compare commits

...

24 Commits

Author SHA1 Message Date
Minio Trusted
858d363e97 update to version v0.3.26 2020-09-23 08:52:43 -07:00
Lenin Alevski
47704189d1 fix kes empty configuration (#286)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-22 20:49:25 -07:00
Daniel Valdivia
b72d424ec9 UI: Tweaks to multiple elements (#284) 2020-09-22 20:31:00 -07:00
Lenin Alevski
86426e95f7 Added Annotations, Labels and NodeSelector fields (#285)
For Console/Encryption objects in the  CreateTenant Api
2020-09-22 15:50:37 -07:00
Daniel Valdivia
e5f7870f5e Parity API (#280) 2020-09-22 11:15:21 -07:00
Lenin Alevski
c0ee739624 IV generation for ChaCha20 poly auth scheme (#283)
Generate 16 bytes IV instead of an IV of 32 bytes (and then use half of it) when using ChaCha20 to
encrypt tokens, this is to prevent tokens to become malleable.
2020-09-22 10:49:34 -07:00
Minio Trusted
1dc99498d9 update v0.3.25 2020-09-21 22:07:11 -07:00
Cesar N
319d96c725 Use operator port variables (#282) 2020-09-21 21:31:30 -07:00
Minio Trusted
6d58290a89 update to v0.3.24 2020-09-17 18:30:56 -07:00
Lenin Alevski
666904f902 fix regression when calculating token using chacha20 (#281) 2020-09-17 18:21:54 -07:00
Cesar N
064533d8aa Set annotations and labels at Tenant level (#279)
on Tenant Creation request api
2020-09-17 06:44:16 -07:00
Lenin Alevski
1768af9026 Fix tenant details screen (#277) 2020-09-16 23:01:28 -07:00
Alex
cb7513e9f0 Replaces create tenant functionality (#278)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-09-16 21:47:38 -07:00
Lenin Alevski
645b45cf35 fix tenant details screen (#276) 2020-09-15 14:00:28 -07:00
Cesar N
9f6d965ba2 Add missing validations on tenant info test (#273)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-10 15:43:43 -07:00
Cesar N
5348400665 Delete secrets created if it fails on tenant creation (#274)
Also a fix on a parity condition has been fixed.
2020-09-09 17:08:34 -07:00
Minio Trusted
812fd5f253 update to v0.3.23 2020-09-08 12:28:44 -07:00
Lenin Alevski
da9b393e1b fix regression on update update cert and encryption config endpoint (#272) 2020-09-08 12:20:38 -07:00
Minio Trusted
aeaa1a23ce update to v0.3.22 2020-09-07 17:14:21 -07:00
Lenin Alevski
a8d403a216 fix bug for tenant image pull credentials (#271)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-09-06 23:20:27 -07:00
Minio Trusted
7bd898b2c7 update to v0.3.21 2020-09-05 23:50:46 -07:00
Lenin Alevski
dad66db49a Support for adding prometheus annotations on update minio tenant (#269) 2020-09-05 23:48:51 -07:00
Daniel Valdivia
adf3f929a4 Add Tenant Deletion Date to tenant responses (#270) 2020-09-05 23:37:01 -07:00
Lenin Alevski
3b23e877b5 delete unnecessary logs (#268) 2020-09-05 17:39:21 -07:00
54 changed files with 4671 additions and 1172 deletions

2
go.mod
View File

@@ -19,7 +19,7 @@ require (
github.com/minio/mc v0.0.0-20200808005614-7e52c104bee1
github.com/minio/minio v0.0.0-20200808024306-2a9819aff876
github.com/minio/minio-go/v7 v7.0.5-0.20200807085956-d7db33ea7618
github.com/minio/operator v0.0.0-20200904194631-b8aa01dc5d70
github.com/minio/operator v0.0.0-20200922064400-af3315add727
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/secure-io/sio-go v0.3.1
github.com/stretchr/testify v1.6.1

12
go.sum
View File

@@ -227,10 +227,13 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ
github.com/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg=
github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 h1:2HQmlpI3yI9deH18Q6xiSOIjXD4sLI55Y/gfpa8/558=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7 h1:Cvj7S8I4Xpx78KAl6TwTmMHuHlZ/0SM60NUneGJQ7IE=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
@@ -491,6 +494,7 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-containerregistry v0.1.2 h1:YjFNKqxzWUVZND8d4ItF9wuYlE75WQfECE7yKX/Nu3o=
github.com/google/go-containerregistry v0.1.2/go.mod h1:GPivBPgdAyd2SU+vf6EpsgOtWDuPqjW0hJZt4rNdTZ4=
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -622,6 +626,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
@@ -776,8 +781,10 @@ github.com/minio/minio-go/v7 v7.0.2/go.mod h1:dJ80Mv2HeGkYLH1sqS/ksz07ON6csH3S6J
github.com/minio/minio-go/v7 v7.0.3/go.mod h1:TA0CQCjJZHM5SJj9IjqR0NmpmQJ6bCbXifAJ3mUU6Hw=
github.com/minio/minio-go/v7 v7.0.5-0.20200807085956-d7db33ea7618 h1:8iTb0TFs6kDGAUnhI/s2QCZOYcSTtYmY9dF+Cbc0WJo=
github.com/minio/minio-go/v7 v7.0.5-0.20200807085956-d7db33ea7618/go.mod h1:CSt2ETZNs+bIIhWTse0mcZKZWMGrFU7Er7RR0TmkDYk=
github.com/minio/operator v0.0.0-20200904194631-b8aa01dc5d70 h1:FjyhnnrOHMzhJryqNoOISgp8p1dmmn1IMOlgBAaf8r4=
github.com/minio/operator v0.0.0-20200904194631-b8aa01dc5d70/go.mod h1:NVl1+c7TYxJB22opK/m2L5SkTvlEYd1ZFPuL6SX5fCg=
github.com/minio/operator v0.0.0-20200921211523-69f9eef5b7b5 h1:yQ7WHA2wbTTGHz0Z4xXSRxfUTm5fafM0ajvlZlQ2Zl4=
github.com/minio/operator v0.0.0-20200921211523-69f9eef5b7b5/go.mod h1:6lavbNo2YuJWeQR5bZYsEWdbpRCO2KrTyfQ0PtC/AN4=
github.com/minio/operator v0.0.0-20200922064400-af3315add727 h1:MN8Knn7yxrd76Y3uL38DZh4RhihTZTck5u51NoEar/c=
github.com/minio/operator v0.0.0-20200922064400-af3315add727/go.mod h1:6lavbNo2YuJWeQR5bZYsEWdbpRCO2KrTyfQ0PtC/AN4=
github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs=
github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
@@ -1252,6 +1259,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@@ -7,7 +7,6 @@ rules:
- ""
resources:
- namespaces
- secrets
- pods
- services
- events
@@ -18,6 +17,18 @@ rules:
- create
- list
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- create
- list
- patch
- deletecollection
- delete
- apiGroups:
- "storage.k8s.io"
resources:

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.3.20
image: minio/console:v0.3.26
imagePullPolicy: "IfNotPresent"
args:
- server

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.3.20
image: minio/console:v0.3.26
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_OPERATOR_MODE

View File

@@ -8,4 +8,4 @@ resources:
- console-configmap.yaml
- console-service.yaml
- console-deployment.yaml
- https://github.com/minio/operator/?ref=v3.0.10
- https://github.com/minio/operator/?ref=v3.0.19

View File

@@ -0,0 +1,117 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// ConsoleConfiguration console configuration
//
// swagger:model consoleConfiguration
type ConsoleConfiguration struct {
MetadataFields
// image
Image string `json:"image,omitempty"`
}
// UnmarshalJSON unmarshals this object from a JSON structure
func (m *ConsoleConfiguration) UnmarshalJSON(raw []byte) error {
// AO0
var aO0 MetadataFields
if err := swag.ReadJSON(raw, &aO0); err != nil {
return err
}
m.MetadataFields = aO0
// AO1
var dataAO1 struct {
Image string `json:"image,omitempty"`
}
if err := swag.ReadJSON(raw, &dataAO1); err != nil {
return err
}
m.Image = dataAO1.Image
return nil
}
// MarshalJSON marshals this object to a JSON structure
func (m ConsoleConfiguration) MarshalJSON() ([]byte, error) {
_parts := make([][]byte, 0, 2)
aO0, err := swag.WriteJSON(m.MetadataFields)
if err != nil {
return nil, err
}
_parts = append(_parts, aO0)
var dataAO1 struct {
Image string `json:"image,omitempty"`
}
dataAO1.Image = m.Image
jsonDataAO1, errAO1 := swag.WriteJSON(dataAO1)
if errAO1 != nil {
return nil, errAO1
}
_parts = append(_parts, jsonDataAO1)
return swag.ConcatJSON(_parts...), nil
}
// Validate validates this console configuration
func (m *ConsoleConfiguration) Validate(formats strfmt.Registry) error {
var res []error
// validation for a type composition with MetadataFields
if err := m.MetadataFields.Validate(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// MarshalBinary interface implementation
func (m *ConsoleConfiguration) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *ConsoleConfiguration) UnmarshalBinary(b []byte) error {
var res ConsoleConfiguration
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -42,6 +42,9 @@ type CreateTenantRequest struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// console
Console *ConsoleConfiguration `json:"console,omitempty"`
// console image
ConsoleImage string `json:"console_image,omitempty"`
@@ -72,6 +75,9 @@ type CreateTenantRequest struct {
// image registry
ImageRegistry *ImageRegistry `json:"image_registry,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`
// mounth path
MounthPath string `json:"mounth_path,omitempty"`
@@ -102,6 +108,10 @@ type CreateTenantRequest struct {
func (m *CreateTenantRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateConsole(formats); err != nil {
res = append(res, err)
}
if err := m.validateEncryption(formats); err != nil {
res = append(res, err)
}
@@ -136,6 +146,24 @@ func (m *CreateTenantRequest) Validate(formats strfmt.Registry) error {
return nil
}
func (m *CreateTenantRequest) validateConsole(formats strfmt.Registry) error {
if swag.IsZero(m.Console) { // not required
return nil
}
if m.Console != nil {
if err := m.Console.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("console")
}
return err
}
}
return nil
}
func (m *CreateTenantRequest) validateEncryption(formats strfmt.Registry) error {
if swag.IsZero(m.Encryption) { // not required

View File

@@ -32,6 +32,7 @@ import (
//
// swagger:model encryptionConfiguration
type EncryptionConfiguration struct {
MetadataFields
// aws
Aws *AwsConfiguration `json:"aws,omitempty"`
@@ -52,10 +53,100 @@ type EncryptionConfiguration struct {
Vault *VaultConfiguration `json:"vault,omitempty"`
}
// UnmarshalJSON unmarshals this object from a JSON structure
func (m *EncryptionConfiguration) UnmarshalJSON(raw []byte) error {
// AO0
var aO0 MetadataFields
if err := swag.ReadJSON(raw, &aO0); err != nil {
return err
}
m.MetadataFields = aO0
// AO1
var dataAO1 struct {
Aws *AwsConfiguration `json:"aws,omitempty"`
Client *KeyPairConfiguration `json:"client,omitempty"`
Gemalto *GemaltoConfiguration `json:"gemalto,omitempty"`
Image string `json:"image,omitempty"`
Server *KeyPairConfiguration `json:"server,omitempty"`
Vault *VaultConfiguration `json:"vault,omitempty"`
}
if err := swag.ReadJSON(raw, &dataAO1); err != nil {
return err
}
m.Aws = dataAO1.Aws
m.Client = dataAO1.Client
m.Gemalto = dataAO1.Gemalto
m.Image = dataAO1.Image
m.Server = dataAO1.Server
m.Vault = dataAO1.Vault
return nil
}
// MarshalJSON marshals this object to a JSON structure
func (m EncryptionConfiguration) MarshalJSON() ([]byte, error) {
_parts := make([][]byte, 0, 2)
aO0, err := swag.WriteJSON(m.MetadataFields)
if err != nil {
return nil, err
}
_parts = append(_parts, aO0)
var dataAO1 struct {
Aws *AwsConfiguration `json:"aws,omitempty"`
Client *KeyPairConfiguration `json:"client,omitempty"`
Gemalto *GemaltoConfiguration `json:"gemalto,omitempty"`
Image string `json:"image,omitempty"`
Server *KeyPairConfiguration `json:"server,omitempty"`
Vault *VaultConfiguration `json:"vault,omitempty"`
}
dataAO1.Aws = m.Aws
dataAO1.Client = m.Client
dataAO1.Gemalto = m.Gemalto
dataAO1.Image = m.Image
dataAO1.Server = m.Server
dataAO1.Vault = m.Vault
jsonDataAO1, errAO1 := swag.WriteJSON(dataAO1)
if errAO1 != nil {
return nil, errAO1
}
_parts = append(_parts, jsonDataAO1)
return swag.ConcatJSON(_parts...), nil
}
// Validate validates this encryption configuration
func (m *EncryptionConfiguration) Validate(formats strfmt.Registry) error {
var res []error
// validation for a type composition with MetadataFields
if err := m.MetadataFields.Validate(formats); err != nil {
res = append(res, err)
}
if err := m.validateAws(formats); err != nil {
res = append(res, err)
}

View File

@@ -28,26 +28,33 @@ import (
"github.com/go-openapi/swag"
)
// UpdateCertificatesRequest update certificates request
// MetadataConfiguration metadata configuration
//
// swagger:model updateCertificatesRequest
type UpdateCertificatesRequest struct {
// swagger:model metadataConfiguration
type MetadataConfiguration struct {
// console
Console *KeyPairConfiguration `json:"console,omitempty"`
Console *MetadataFields `json:"console,omitempty"`
// kes
Kes *MetadataFields `json:"kes,omitempty"`
// minio
Minio *KeyPairConfiguration `json:"minio,omitempty"`
Minio *MetadataFields `json:"minio,omitempty"`
}
// Validate validates this update certificates request
func (m *UpdateCertificatesRequest) Validate(formats strfmt.Registry) error {
// Validate validates this metadata configuration
func (m *MetadataConfiguration) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateConsole(formats); err != nil {
res = append(res, err)
}
if err := m.validateKes(formats); err != nil {
res = append(res, err)
}
if err := m.validateMinio(formats); err != nil {
res = append(res, err)
}
@@ -58,7 +65,7 @@ func (m *UpdateCertificatesRequest) Validate(formats strfmt.Registry) error {
return nil
}
func (m *UpdateCertificatesRequest) validateConsole(formats strfmt.Registry) error {
func (m *MetadataConfiguration) validateConsole(formats strfmt.Registry) error {
if swag.IsZero(m.Console) { // not required
return nil
@@ -76,7 +83,25 @@ func (m *UpdateCertificatesRequest) validateConsole(formats strfmt.Registry) err
return nil
}
func (m *UpdateCertificatesRequest) validateMinio(formats strfmt.Registry) error {
func (m *MetadataConfiguration) validateKes(formats strfmt.Registry) error {
if swag.IsZero(m.Kes) { // not required
return nil
}
if m.Kes != nil {
if err := m.Kes.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("kes")
}
return err
}
}
return nil
}
func (m *MetadataConfiguration) validateMinio(formats strfmt.Registry) error {
if swag.IsZero(m.Minio) { // not required
return nil
@@ -95,7 +120,7 @@ func (m *UpdateCertificatesRequest) validateMinio(formats strfmt.Registry) error
}
// MarshalBinary interface implementation
func (m *UpdateCertificatesRequest) MarshalBinary() ([]byte, error) {
func (m *MetadataConfiguration) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
@@ -103,8 +128,8 @@ func (m *UpdateCertificatesRequest) MarshalBinary() ([]byte, error) {
}
// UnmarshalBinary interface implementation
func (m *UpdateCertificatesRequest) UnmarshalBinary(b []byte) error {
var res UpdateCertificatesRequest
func (m *MetadataConfiguration) UnmarshalBinary(b []byte) error {
var res MetadataConfiguration
if err := swag.ReadJSON(b, &res); err != nil {
return err
}

66
models/metadata_fields.go Normal file
View File

@@ -0,0 +1,66 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// MetadataFields metadata fields
//
// swagger:model metadataFields
type MetadataFields struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`
// node selector
NodeSelector map[string]string `json:"node_selector,omitempty"`
}
// Validate validates this metadata fields
func (m *MetadataFields) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *MetadataFields) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *MetadataFields) UnmarshalBinary(b []byte) error {
var res MetadataFields
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

37
models/parity_response.go Normal file
View File

@@ -0,0 +1,37 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
)
// ParityResponse parity response
//
// swagger:model parityResponse
type ParityResponse []string
// Validate validates this parity response
func (m ParityResponse) Validate(formats strfmt.Registry) error {
return nil
}

View File

@@ -35,12 +35,21 @@ import (
// swagger:model tenant
type Tenant struct {
// console image
ConsoleImage string `json:"console_image,omitempty"`
// creation date
CreationDate string `json:"creation_date,omitempty"`
// current state
CurrentState string `json:"currentState,omitempty"`
// deletion date
DeletionDate string `json:"deletion_date,omitempty"`
// enable prometheus
EnablePrometheus bool `json:"enable_prometheus,omitempty"`
// image
Image string `json:"image,omitempty"`

View File

@@ -38,6 +38,9 @@ type TenantList struct {
// current state
CurrentState string `json:"currentState,omitempty"`
// deletion date
DeletionDate string `json:"deletion_date,omitempty"`
// instance count
InstanceCount int64 `json:"instance_count,omitempty"`

View File

@@ -38,6 +38,9 @@ type UpdateTenantRequest struct {
// Pattern: ^((.*?)/(.*?):(.+))$
ConsoleImage string `json:"console_image,omitempty"`
// enable prometheus
EnablePrometheus bool `json:"enable_prometheus,omitempty"`
// image
// Pattern: ^((.*?)/(.*?):(.+))$
Image string `json:"image,omitempty"`

View File

@@ -207,6 +207,9 @@ func (m *Zone) UnmarshalBinary(b []byte) error {
// swagger:model ZoneVolumeConfiguration
type ZoneVolumeConfiguration struct {
// annotations
Annotations map[string]string `json:"annotations,omitempty"`
// labels
Labels map[string]string `json:"labels,omitempty"`

View File

@@ -36,7 +36,7 @@ var (
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
heal = "/heal"
)

View File

@@ -155,10 +155,10 @@ const (
// or data key provided as plaintext.
//
// The returned ciphertext data consists of:
// iv | AEAD ID | nonce | encrypted data
// 32 1 12 ~ len(data)
// AEAD ID | iv | nonce | encrypted data
// 1 16 12 ~ len(data)
func encrypt(plaintext, associatedData []byte) ([]byte, error) {
iv, err := sioutil.Random(32) // 32 bit IV
iv, err := sioutil.Random(16) // 16 bytes IV
if err != nil {
return nil, err
}
@@ -186,7 +186,7 @@ func encrypt(plaintext, associatedData []byte) ([]byte, error) {
}
case c20p1305:
var sealingKey []byte
sealingKey, err = chacha20.HChaCha20(derivedKey, iv)
sealingKey, err = chacha20.HChaCha20(derivedKey, iv) // HChaCha20 expects nonce of 16 bytes
if err != nil {
return nil, err
}
@@ -202,11 +202,11 @@ func encrypt(plaintext, associatedData []byte) ([]byte, error) {
sealedBytes := aead.Seal(nil, nonce, plaintext, associatedData)
// ciphertext = iv | AEAD ID | nonce | sealed bytes
// ciphertext = AEAD ID | iv | nonce | sealed bytes
var buf bytes.Buffer
buf.Write(iv)
buf.WriteByte(algorithm)
buf.Write(iv)
buf.Write(nonce)
buf.Write(sealedBytes)
@@ -218,16 +218,16 @@ func encrypt(plaintext, associatedData []byte) ([]byte, error) {
// and a pbkdf2 derived key
func decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
var (
iv [32]byte
algorithm [1]byte
iv [16]byte
nonce [12]byte // This depends on the AEAD but both used ciphers have the same nonce length.
)
r := bytes.NewReader(ciphertext)
if _, err := io.ReadFull(r, iv[:]); err != nil {
if _, err := io.ReadFull(r, algorithm[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, algorithm[:]); err != nil {
if _, err := io.ReadFull(r, iv[:]); err != nil {
return nil, err
}
if _, err := io.ReadFull(r, nonce[:]); err != nil {
@@ -249,7 +249,7 @@ func decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
return nil, err
}
case c20p1305:
sealingKey, err := chacha20.HChaCha20(derivedKey, iv[:])
sealingKey, err := chacha20.HChaCha20(derivedKey, iv[:]) // HChaCha20 expects nonce of 16 bytes
if err != nil {
return nil, err
}

219
pkg/utils/parity.go Normal file
View File

@@ -0,0 +1,219 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package utils
import (
"errors"
"fmt"
"sort"
"github.com/minio/minio/pkg/ellipses"
)
// This file implements and supports ellipses pattern for
// `minio server` command line arguments.
// Supported set sizes this is used to find the optimal
// single set size.
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
// getDivisibleSize - returns a greatest common divisor of
// all the ellipses sizes.
func getDivisibleSize(totalSizes []uint64) (result uint64) {
gcd := func(x, y uint64) uint64 {
for y != 0 {
x, y = y, x%y
}
return x
}
result = totalSizes[0]
for i := 1; i < len(totalSizes); i++ {
result = gcd(result, totalSizes[i])
}
return result
}
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
var isValidSetSize = func(count uint64) bool {
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
}
// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
// input argument patterns, the symmetry calculation is to ensure that
// we also use uniform number of drives common across all ellipses patterns.
func possibleSetCountsWithSymmetry(setCounts []uint64, argPatterns []ellipses.ArgPattern) []uint64 {
var newSetCounts = make(map[uint64]struct{})
for _, ss := range setCounts {
var symmetry bool
for _, argPattern := range argPatterns {
for _, p := range argPattern {
if uint64(len(p.Seq)) > ss {
symmetry = uint64(len(p.Seq))%ss == 0
} else {
symmetry = ss%uint64(len(p.Seq)) == 0
}
}
}
// With no arg patterns, it is expected that user knows
// the right symmetry, so either ellipses patterns are
// provided (recommended) or no ellipses patterns.
if _, ok := newSetCounts[ss]; !ok && (symmetry || argPatterns == nil) {
newSetCounts[ss] = struct{}{}
}
}
setCounts = []uint64{}
for setCount := range newSetCounts {
setCounts = append(setCounts, setCount)
}
// Not necessarily needed but it ensures to the readers
// eyes that we prefer a sorted setCount slice for the
// subsequent function to figure out the right common
// divisor, it avoids loops.
sort.Slice(setCounts, func(i, j int) bool {
return setCounts[i] < setCounts[j]
})
return setCounts
}
func commonSetDriveCount(divisibleSize uint64, setCounts []uint64) (setSize uint64) {
// prefers setCounts to be sorted for optimal behavior.
if divisibleSize < setCounts[len(setCounts)-1] {
return divisibleSize
}
// Figure out largest value of total_drives_in_erasure_set which results
// in least number of total_drives/total_drives_erasure_set ratio.
prevD := divisibleSize / setCounts[0]
for _, cnt := range setCounts {
if divisibleSize%cnt == 0 {
d := divisibleSize / cnt
if d <= prevD {
prevD = d
setSize = cnt
}
}
}
return setSize
}
// getSetIndexes returns list of indexes which provides the set size
// on each index, this function also determines the final set size
// The final set size has the affinity towards choosing smaller
// indexes (total sets)
func getSetIndexes(args []string, totalSizes []uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
if len(totalSizes) == 0 || len(args) == 0 {
return nil, errors.New("invalid argument")
}
setIndexes = make([][]uint64, len(totalSizes))
for _, totalSize := range totalSizes {
// Check if totalSize has minimum range upto setSize
if totalSize < setSizes[0] {
return nil, fmt.Errorf("incorrect number of endpoints provided %s", args)
}
}
commonSize := getDivisibleSize(totalSizes)
possibleSetCounts := func(setSize uint64) (ss []uint64) {
for _, s := range setSizes {
if setSize%s == 0 {
ss = append(ss, s)
}
}
return ss
}
setCounts := possibleSetCounts(commonSize)
if len(setCounts) == 0 {
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
// Returns possible set counts with symmetry.
setCounts = possibleSetCountsWithSymmetry(setCounts, argPatterns)
if len(setCounts) == 0 {
err = fmt.Errorf("no symmetric distribution detected with input endpoints provided %s, disks %d cannot be spread symmetrically by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
// Final set size with all the symmetry accounted for.
setSize := commonSetDriveCount(commonSize, setCounts)
// Check whether setSize is with the supported range.
if !isValidSetSize(setSize) {
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
return nil, err
}
for i := range totalSizes {
for j := uint64(0); j < totalSizes[i]/setSize; j++ {
setIndexes[i] = append(setIndexes[i], setSize)
}
}
return setIndexes, nil
}
// Return the total size for each argument patterns.
func getTotalSizes(argPatterns []ellipses.ArgPattern) []uint64 {
var totalSizes []uint64
for _, argPattern := range argPatterns {
var totalSize uint64 = 1
for _, p := range argPattern {
totalSize = totalSize * uint64(len(p.Seq))
}
totalSizes = append(totalSizes, totalSize)
}
return totalSizes
}
// PossibleParityValues returns possible parities for input args,
// parties are calculated in uniform manner for one zone or
// multiple zones, ensuring that parities returned are common
// and applicable across all zones.
func PossibleParityValues(args ...string) ([]string, error) {
setIndexes, err := parseEndpointSet(args...)
if err != nil {
return nil, err
}
maximumParity := setIndexes[0][0] / 2
var parities []string
for maximumParity >= 2 {
parities = append(parities, fmt.Sprintf("EC:%d", maximumParity))
maximumParity--
}
return parities, nil
}
// Parses all arguments and returns an endpointSet which is a collection
// of endpoints following the ellipses pattern, this is what is used
// by the object layer for initializing itself.
func parseEndpointSet(args ...string) (setIndexes [][]uint64, err error) {
var argPatterns = make([]ellipses.ArgPattern, len(args))
for i, arg := range args {
patterns, err := ellipses.FindEllipsesPatterns(arg)
if err != nil {
return nil, err
}
argPatterns[i] = patterns
}
return getSetIndexes(args, getTotalSizes(argPatterns), argPatterns)
}

281
pkg/utils/parity_test.go Normal file
View File

@@ -0,0 +1,281 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// All rights reserved
package utils
import (
"reflect"
"testing"
"github.com/minio/minio/pkg/ellipses"
)
func TestGetDivisibleSize(t *testing.T) {
testCases := []struct {
totalSizes []uint64
result uint64
}{{[]uint64{24, 32, 16}, 8},
{[]uint64{32, 8, 4}, 4},
{[]uint64{8, 8, 8}, 8},
{[]uint64{24}, 24},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
gotGCD := getDivisibleSize(testCase.totalSizes)
if testCase.result != gotGCD {
t.Errorf("Expected %v, got %v", testCase.result, gotGCD)
}
})
}
}
// Test tests calculating set indexes.
func TestGetSetIndexes(t *testing.T) {
testCases := []struct {
args []string
totalSizes []uint64
indexes [][]uint64
success bool
}{
// Invalid inputs.
{
[]string{"data{1...3}"},
[]uint64{3},
nil,
false,
},
{
[]string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
[]uint64{2, 4, 8},
nil,
false,
},
{
[]string{"data{1...17}/export{1...52}"},
[]uint64{14144},
nil,
false,
},
// Valid inputs.
{
[]string{"data{1...27}"},
[]uint64{27},
[][]uint64{{9, 9, 9}},
true,
},
{
[]string{"http://host{1...3}/data{1...180}"},
[]uint64{540},
[][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
true,
},
{
[]string{"http://host{1...2}.rack{1...4}/data{1...180}"},
[]uint64{1440},
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
true,
},
{
[]string{"http://host{1...2}/data{1...180}"},
[]uint64{360},
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
true,
},
{
[]string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
[]uint64{4, 8, 12},
[][]uint64{{4}, {4, 4}, {4, 4, 4}},
true,
},
{
[]string{"data{1...64}"},
[]uint64{64},
[][]uint64{{16, 16, 16, 16}},
true,
},
{
[]string{"data{1...24}"},
[]uint64{24},
[][]uint64{{12, 12}},
true,
},
{
[]string{"data/controller{1...11}/export{1...8}"},
[]uint64{88},
[][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}},
true,
},
{
[]string{"data{1...4}"},
[]uint64{4},
[][]uint64{{4}},
true,
},
{
[]string{"data/controller1/export{1...10}, data/controller2/export{1...10}, data/controller3/export{1...10}"},
[]uint64{10, 10, 10},
[][]uint64{{10}, {10}, {10}},
true,
},
{
[]string{"data{1...16}/export{1...52}"},
[]uint64{832},
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
true,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
var argPatterns = make([]ellipses.ArgPattern, len(testCase.args))
for i, arg := range testCase.args {
patterns, err := ellipses.FindEllipsesPatterns(arg)
if err != nil {
t.Fatalf("Unexpected failure %s", err)
}
argPatterns[i] = patterns
}
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, argPatterns)
if err != nil && testCase.success {
t.Errorf("Expected success but failed instead %s", err)
}
if err == nil && !testCase.success {
t.Errorf("Expected failure but passed instead")
}
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
}
})
}
}
// Test tests possible parities returned for any input args
func TestPossibleParities(t *testing.T) {
testCases := []struct {
arg string
parities []string
success bool
}{
// Tests invalid inputs.
{
"...",
nil,
false,
},
// No range specified.
{
"{...}",
nil,
false,
},
// Invalid range.
{
"http://minio{2...3}/export/set{1...0}",
nil,
false,
},
// Range cannot be smaller than 4 minimum.
{
"/export{1..2}",
nil,
false,
},
// Unsupported characters.
{
"/export/test{1...2O}",
nil,
false,
},
// Tests valid inputs.
{
"{1...27}",
[]string{"EC:4", "EC:3", "EC:2"},
true,
},
{
"/export/set{1...64}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Valid input for distributed setup.
{
"http://minio{2...3}/export/set{1...64}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Supporting some advanced cases.
{
"http://minio{1...64}.mydomain.net/data",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
{
"http://rack{1...4}.mydomain.minio{1...16}/data",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// Supporting kubernetes cases.
{
"http://minio{0...15}.mydomain.net/data{0...1}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// No host regex, just disks.
{
"http://server1/data{1...32}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// No host regex, just disks with two position numerics.
{
"http://server1/data{01...32}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// More than 2 ellipses are supported as well.
{
"http://minio{2...3}/export/set{1...64}/test{1...2}",
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// More than 1 ellipses per argument for standalone setup.
{
"/export{1...10}/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// IPv6 ellipses with hexadecimal expansion
{
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
{
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
true,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run("", func(t *testing.T) {
gotPs, err := PossibleParityValues(testCase.arg)
if err != nil && testCase.success {
t.Errorf("Expected success but failed instead %s", err)
}
if err == nil && !testCase.success {
t.Errorf("Expected failure but passed instead")
}
if !reflect.DeepEqual(testCase.parities, gotPs) {
t.Errorf("Expected %v, got %v", testCase.parities, gotPs)
}
})
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,333 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright (c) 2020 MinIO, Inc. All rights reserved. */
export interface ITenantsObject {
tenants: ITenant[];
}
export interface ITenant {
creation_date: string;
deletion_date: string;
currentState: string;
image: string;
console_image: string;
instance_count: string;
name: string;
namespace?: string;
total_size: string;
used_size: string;
volume_count: string;
volume_size: string;
volumes_per_server?: string;
zone_count: string;
zones?: IZoneModel[];
used_capacity?: string;
endpoint?: string;
storage_class?: string;
enable_prometheus: boolean;
}
export interface IVolumeConfiguration {
size: string;
storage_class_name: string;
labels?: any;
}
export interface ITenantCreator {
name: string;
service_name: string;
enable_console: boolean;
enable_prometheus: boolean;
enable_tls: boolean;
access_key: string;
secret_key: string;
image: string;
console_image: string;
zones: IZoneModel[];
namespace: string;
erasureCodingParity: number;
tls?: ITLSTenantConfiguration;
encryption?: IEncryptionConfiguration;
idp?: IIDPConfiguration;
annotations?: Object;
}
export interface ITenantUpdateObject {
image: string;
image_registry?: IRegistryObject;
}
export interface IRegistryObject {
registry: string;
username: string;
password: string;
}
export interface ITenantUsage {
used: string;
disk_used: string;
}
export interface IAffinityModel {
podAntiAffinity: IPodAntiAffinityModel;
}
export interface IPodAntiAffinityModel {
requiredDuringSchedulingIgnoredDuringExecution: IPodAffinityTerm[];
}
export interface IPodAffinityTerm {
labelSelector: IPodAffinityTermLabelSelector;
topologyKey: string;
}
export interface IPodAffinityTermLabelSelector {
matchExpressions: IMatchExpressionItem[];
}
export interface IMatchExpressionItem {
key: string;
operator: string;
values: string[];
}
export interface ITolerationModel {
effect: string;
key: string;
operator: string;
value?: string;
tolerationSeconds?: ITolerationSeconds;
}
export interface ITolerationSeconds {
seconds: number;
}
export interface IResourceModel {
requests: IResourceRequests;
limits?: IResourceLimits;
}
export interface IResourceRequests {
memory: number;
}
export interface IResourceLimits {
memory: number;
}
export interface ITLSTenantConfiguration {
minio: ITLSConfiguration;
console: ITLSConfiguration;
}
export interface ITLSConfiguration {
crt: string;
key: string;
}
export interface IEncryptionConfiguration {
server: ITLSConfiguration;
client: ITLSConfiguration;
master_key?: string;
gemalto?: IGemaltoConfig;
aws?: IAWSConfig;
vault?: IVaultConfig;
}
export interface IVaultConfig {
endpoint: string;
engine?: string;
namespace?: string;
prefix?: string;
approle: IApproleConfig;
tls: IVaultTLSConfig;
status: IVaultStatusConfig;
}
export interface IGemaltoConfig {
keysecure: IKeysecureConfig;
}
export interface IAWSConfig {
secretsmanager: ISecretsManagerConfig;
}
export interface IApproleConfig {
engine: string;
id: string;
secret: string;
retry: number;
}
export interface IVaultTLSConfig {
key: string;
crt: string;
ca: string;
}
export interface IVaultStatusConfig {
ping: number;
}
export interface IKeysecureConfig {
endpoint: string;
credentials: IGemaltoCredentials;
tls: IGemaltoTLS;
}
export interface IGemaltoCredentials {
token: string;
domain: string;
retry?: number;
}
export interface IGemaltoTLS {
ca: string;
}
export interface ISecretsManagerConfig {
endpoint: string;
region: string;
kmskey?: string;
credentials: IAWSCredentials;
}
export interface IAWSCredentials {
accesskey: string;
secretkey: string;
token?: string;
}
export interface IIDPConfiguration {
oidc?: IOpenIDConfiguration;
active_directory: IActiveDirectoryConfiguration;
}
export interface IOpenIDConfiguration {
url: string;
client_id: string;
secret_id: string;
}
export interface IActiveDirectoryConfiguration {
url: string;
skip_tls_verification: boolean;
server_insecure: boolean;
user_search_filter: string;
group_Search_base_dn: string;
group_search_filter: string;
group_name_attribute: string;
}
export interface IStorageDistribution {
error: number;
nodes: number;
persistentVolumes: number;
disks: number;
pvSize: number;
}
export interface IErasureCodeCalc {
error: number;
maxEC: string;
erasureCodeSet: number;
rawCapacity: string;
storageFactors: IStorageFactors[];
defaultEC: string;
}
export interface IStorageFactors {
erasureCode: string;
storageFactor: number;
maxCapacity: string;
}
export interface ITenantHealthInList {
name: string;
namespace: string;
status?: string;
message?: string;
}
export interface ITenantsListHealthRequest {
tenants: ITenantHealthInList[];
}
export interface IMaxAllocatableMemoryRequest {
num_nodes: number;
}
export interface IMaxAllocatableMemoryResponse {
max_memory: number;
}
export interface IEncryptionUpdateRequest {
encryption: IEncryptionConfiguration;
}
export interface IArchivedTenantsList {
tenants: IArchivedTenant[];
}
export interface IArchivedTenant {
namespace: string;
tenant: string;
number_volumes: number;
capacity: number;
}
export interface IZoneModel {
name?: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
affinity?: IAffinityModel;
tolerations?: ITolerationModel[];
resources?: IResourceModel;
}
export interface IUpdateZone {
zones: IZoneModel[];
}
export interface INode {
name: string;
freeSpace: string;
totalSpace: string;
disks: IDisk[];
}
export interface IStorageType {
freeSpace: string;
totalSpace: string;
storageClasses: string[];
nodes: INode[];
schedulableNodes: INode[];
}
export interface IDisk {
name: string;
freeSpace: string;
totalSpace: string;
}
export interface ICapacity {
value: string;
unit: string;
}

View File

@@ -15,6 +15,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import storage from "local-storage-fallback";
import { ICapacity, IStorageType, IZoneModel } from "./types";
const minStReq = 1073741824; // Minimal Space required for MinIO
const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes
export const units = [
"B",
@@ -28,6 +32,8 @@ export const units = [
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const k8sCalcUnits = ["B", ...k8sUnits];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -90,12 +96,19 @@ export const k8sfactorForDropdown = () => {
};
//getBytes, converts from a value and a unit from units array to bytes
export const getBytes = (value: string, unit: string) => {
export const getBytes = (
value: string,
unit: string,
fork8s: boolean = false
) => {
const vl: number = parseFloat(value);
const powFactor = units.findIndex((element) => element === unit);
if (powFactor == -1) {
return 0;
const unitsTake = fork8s ? k8sCalcUnits : units;
const powFactor = unitsTake.findIndex((element) => element === unit);
if (powFactor === -1) {
return "0";
}
const factor = Math.pow(1024, powFactor);
const total = vl * factor;
@@ -105,6 +118,220 @@ export const getBytes = (value: string, unit: string) => {
//getTotalSize gets the total size of a value & unit
export const getTotalSize = (value: string, unit: string) => {
const bytes = getBytes(value, unit).toString(10);
const bytes = getBytes(value, unit, true).toString();
return niceBytes(bytes);
};
export const setMemoryResource = (
memorySize: number,
capacitySize: string,
maxMemorySize: number
) => {
// value always comes as Gi
const requestedSizeBytes = getBytes(memorySize.toString(10), "Gi", true);
const memReqSize = parseInt(requestedSizeBytes, 10);
if (maxMemorySize === 0) {
return {
error: "There is no memory available for the selected number of nodes",
request: 0,
limit: 0,
};
}
if (maxMemorySize < minMemReq) {
return {
error: "There are not enough memory resources available",
request: 0,
limit: 0,
};
}
if (memReqSize < minMemReq) {
return {
error: "The requested memory size must be greater than 2Gi",
request: 0,
limit: 0,
};
}
if (memReqSize > maxMemorySize) {
return {
error:
"The requested memory is greater than the max available memory for the selected number of nodes",
request: 0,
limit: 0,
};
}
const capSize = parseInt(capacitySize, 10);
let memLimitSize = memReqSize;
// set memory limit based on the capacitySize
// if capacity size is lower than 1TiB we use the limit equal to request
if (capSize >= parseInt(getBytes("1", "Pi", true), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("64", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("100", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("32", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("10", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("16", "Gi", true), 10)
);
} else if (capSize >= parseInt(getBytes("1", "Ti"), 10)) {
memLimitSize = Math.max(
memReqSize,
parseInt(getBytes("8", "Gi", true), 10)
);
}
return {
error: "",
request: memReqSize,
limit: memLimitSize,
};
};
export const calculateDistribution = (
capacityToUse: ICapacity,
forcedNodes: number = 0,
limitSize: number = 0
) => {
let numberOfNodes = {};
const requestedSizeBytes = getBytes(
capacityToUse.value,
capacityToUse.unit,
true
);
if (parseInt(requestedSizeBytes, 10) < minStReq) {
return {
error: "The zone size must be greater than 1Gi",
nodes: 0,
persistentVolumes: 0,
disks: 0,
pvSize: 0,
};
}
if (forcedNodes < 4) {
return {
error: "Number of nodes cannot be less than 4",
nodes: 0,
persistentVolumes: 0,
disks: 0,
pvSize: 0,
};
}
numberOfNodes = calculateStorage(requestedSizeBytes, forcedNodes, limitSize);
return numberOfNodes;
};
const calculateStorage = (
requestedBytes: string,
forcedNodes: number,
limitSize: number
) => {
// Size validation
const intReqBytes = parseInt(requestedBytes, 10);
const maxDiskSize = minStReq * 256; // 256 GiB
// We get the distribution
return structureCalc(forcedNodes, intReqBytes, maxDiskSize, limitSize);
};
const structureCalc = (
nodes: number,
desiredCapacity: number,
maxDiskSize: number,
maxClusterSize: number,
disksPerNode: number = 0
) => {
if (
isNaN(nodes) ||
isNaN(desiredCapacity) ||
isNaN(maxDiskSize) ||
isNaN(maxClusterSize)
) {
return {
error: "Some provided data is invalid, please try again.",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Invalid Data
}
let persistentVolumeSize = 0;
let numberPersistentVolumes = 0;
let volumesPerServer = 0;
if (disksPerNode === 0) {
persistentVolumeSize = Math.floor(
Math.min(desiredCapacity / Math.max(4, nodes), maxDiskSize)
); // pVS = min((desiredCapacity / max(4 | nodes)) | maxDiskSize)
numberPersistentVolumes = desiredCapacity / persistentVolumeSize; // nPV = dC / pVS
volumesPerServer = numberPersistentVolumes / nodes; // vPS = nPV / n
}
if (disksPerNode) {
volumesPerServer = disksPerNode;
numberPersistentVolumes = volumesPerServer * nodes;
persistentVolumeSize = Math.floor(
desiredCapacity / numberPersistentVolumes
);
}
// Volumes are not exact, we force the volumes number & minimize the volume size
if (volumesPerServer % 1 > 0) {
volumesPerServer = Math.ceil(volumesPerServer); // Increment of volumes per server
numberPersistentVolumes = volumesPerServer * nodes; // nPV = vPS * n
persistentVolumeSize = Math.floor(
desiredCapacity / numberPersistentVolumes
); // pVS = dC / nPV
const limitSize = persistentVolumeSize * volumesPerServer * nodes; // lS = pVS * vPS * n
if (limitSize > maxClusterSize) {
return {
error: "We were not able to allocate this server.",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Cannot allocate this server
}
}
if (persistentVolumeSize < minStReq) {
return {
error:
"Disk Size with this combination would be less than 1Gi, please try another combination",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
}; // Cannot allocate this volume size
}
return {
error: "",
nodes,
persistentVolumes: numberPersistentVolumes,
disks: volumesPerServer,
pvSize: persistentVolumeSize,
};
};
// Zone Name Generator
export const generateZoneName = (zones: IZoneModel[]) => {
const zoneCounter = zones.length;
return `zone-${zoneCounter}`;
};

View File

@@ -23,7 +23,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
LinearProgress,
} from "@material-ui/core";
import api from "../../../../common/api";
import { BucketList } from "../types";
@@ -32,8 +32,8 @@ import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IDeleteBucketProps {
@@ -54,7 +54,7 @@ class DeleteBucket extends React.Component<
> {
state: IDeleteBucketState = {
deleteLoading: false,
deleteError: ""
deleteError: "",
};
removeRecord() {
@@ -66,23 +66,23 @@ class DeleteBucket extends React.Component<
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/buckets/${selectedBucket}`, {
name: selectedBucket
name: selectedBucket,
})
.then((res: BucketList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
deleteError: "",
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
deleteLoading: false,
deleteError: err
deleteError: err,
});
});
});

View File

@@ -339,7 +339,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Bucket > {match.params["bucketName"]}
{`Bucket > ${match.params["bucketName"]}`}
</Typography>
</Grid>
<Grid item xs={12}>

View File

@@ -0,0 +1,113 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Grid, InputLabel, Tooltip } from "@material-ui/core";
import HelpIcon from "@material-ui/icons/Help";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import { fileProcess } from "./utils";
interface InputBoxProps {
label: string;
classes: any;
onChange: (e: string) => void;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
required?: boolean;
error?: string;
accept?: string;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
textBoxContainer: {
flexGrow: 1,
position: "relative",
},
errorState: {
color: "#b53b4b",
fontSize: 14,
position: "absolute",
top: 7,
right: 7,
},
});
const FileSelector = ({
label,
classes,
onChange,
id,
name,
disabled = false,
tooltip = "",
required,
error = "",
accept = "",
}: InputBoxProps) => {
return (
<React.Fragment>
<Grid
item
xs={12}
className={`${classes.fieldContainer} ${
error !== "" ? classes.errorInField : ""
}`}
>
{label !== "" && (
<InputLabel
htmlFor={id}
className={`${error !== "" ? classes.fieldLabelError : ""} ${
classes.inputLabel
}`}
>
<span>
{label}
{required ? "*" : ""}
</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<input
type="file"
name={name}
onChange={(e) => {
fileProcess(e, (data: any) => {
onChange(data);
});
}}
accept={accept}
required
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(FileSelector);

View File

@@ -0,0 +1,34 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const fileProcess = (evt: any, callback: any) => {
const file = evt.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
// reader.readAsDataURL(file) output will be something like: data:application/x-x509-ca-cert;base64,LS0tLS1CRUdJTiBDRVJUSU
// we care only about the actual base64 part (everything after "data:application/x-x509-ca-cert;base64,")
const fileBase64 = reader.result;
if (fileBase64) {
const fileArray = fileBase64.toString().split("base64,");
if (fileArray.length === 2) {
callback(fileArray[1]);
}
}
};
};

View File

@@ -42,6 +42,7 @@ interface SelectProps {
onChange: (
e: React.ChangeEvent<{ name?: string | undefined; value: unknown }>
) => void;
disabled?: boolean;
classes: any;
}
@@ -51,7 +52,7 @@ const styles = (theme: Theme) =>
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
width: 215,
},
});
@@ -88,6 +89,7 @@ const SelectWrapper = ({
label,
tooltip = "",
value,
disabled = false,
}: SelectProps) => {
return (
<React.Fragment>
@@ -111,6 +113,7 @@ const SelectWrapper = ({
value={value}
onChange={onChange}
input={<SelectStyled />}
disabled={disabled}
>
{options.map((option) => (
<MenuItem

View File

@@ -265,7 +265,7 @@ const TableWrapper = ({
</Grid>
</Grid>
)}
{records && records.length > 0 ? (
{records && !isLoading && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
<TableRow>

View File

@@ -311,7 +311,7 @@ const Console = ({
},
{
component: TenantDetails,
path: "/tenants/:tenantName",
path: "/namespaces/:tenantNamespace/tenants/:tenantName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,12 @@ import {
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import { ITenant } from "./types";
interface IDeleteTenant {
classes: any;
deleteOpen: boolean;
selectedTenant: string;
selectedTenant: ITenant;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
@@ -54,7 +55,10 @@ const DeleteTenant = ({
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/tenants/${selectedTenant}`)
.invoke(
"DELETE",
`/api/v1/namespaces/${selectedTenant.namespace}/tenants/${selectedTenant.name}`
)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
@@ -85,7 +89,7 @@ const DeleteTenant = ({
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete tenant <b>{selectedTenant}</b>?
Are you sure you want to delete tenant <b>{selectedTenant.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />

View File

@@ -20,7 +20,7 @@ import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { Button } from "@material-ui/core";
import { Button, IconButton } from "@material-ui/core";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
@@ -32,6 +32,10 @@ import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import RefreshIcon from "@material-ui/icons/Refresh";
import TenantCredentialsPrompt from "./TenantCredentialsPrompt/TenantCredentialsPrompt";
import { NewTenantCredential } from "./TenantCredentialsPrompt/types";
interface ITenantsList {
classes: any;
@@ -96,11 +100,11 @@ const ListTenants = ({ classes }: ITenantsList) => {
const [
createdAccount,
setCreatedAccount,
] = useState<NewServiceAccount | null>(null);
] = useState<NewTenantCredential | null>(null);
const closeAddModalAndRefresh = (
reloadData: boolean,
res: NewServiceAccount | null
res: NewTenantCredential | null
) => {
setCreateTenantOpen(false);
@@ -122,11 +126,16 @@ const ListTenants = ({ classes }: ITenantsList) => {
}
};
const confirmDeleteTenant = (tenant: string) => {
const confirmDeleteTenant = (tenant: ITenant) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
const redirectToTenantDetails = (tenant: ITenant) => {
history.push(`/namespaces/${tenant.namespace}/tenants/${tenant.name}`);
return;
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setCreatedAccount(null);
@@ -149,8 +158,8 @@ const ListTenants = ({ classes }: ITenantsList) => {
};
const tableActions = [
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
{ type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant },
];
const filteredRecords = records
@@ -187,9 +196,7 @@ const ListTenants = ({ classes }: ITenantsList) => {
}
for (let i = 0; i < resTenants.length; i++) {
const total =
resTenants[i].volume_count * resTenants[i].volume_size;
resTenants[i].capacity = niceBytes(total + "");
resTenants[i].capacity = niceBytes(resTenants[i].total_size + "");
}
setRecords(resTenants);
@@ -231,7 +238,7 @@ const ListTenants = ({ classes }: ITenantsList) => {
/>
)}
{showNewCredentials && (
<CredentialsPrompt
<TenantCredentialsPrompt
newServiceAccount={createdAccount}
open={showNewCredentials}
closeModal={() => {
@@ -248,6 +255,17 @@ const ListTenants = ({ classes }: ITenantsList) => {
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<TextField
placeholder="Search Tenants"
className={classes.searchField}

View File

@@ -86,7 +86,14 @@ const ZonesMultiSelector = ({
onChange,
classes,
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0, capacity: "", volumes: 0 };
const defaultZone: IZone = {
name: "",
servers: 0,
capacity: "",
volumes: 0,
volumes_per_server: 0,
volume_configuration: { size: 0, storage_class: "", labels: null },
};
const [currentElements, setCurrentElements] = useState<IZone[]>([]);
const [internalCounter, setInternalCounter] = useState<number>(1);

View File

@@ -17,18 +17,32 @@
export interface IZone {
name: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
// computed
capacity: string;
volumes: number;
}
export interface IAddZoneRequest {
name: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
}
export interface IVolumeConfiguration {
size: number;
storage_class: string;
labels: { [key: string]: any } | null;
}
export interface ITenant {
total_size: number;
name: string;
namespace: string;
image: string;
console_image: string;
zone_count: number;
currentState: string;
instance_count: 4;

View File

@@ -11,13 +11,14 @@ import {
niceBytes,
} from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import { IAddZoneRequest, ITenant } from "../ListTenants/types";
interface IAddZoneProps {
tenant: ITenant;
classes: any;
open: boolean;
onCloseZoneAndReload: (shouldReload: boolean) => void;
volumesPerInstance: number;
volumeSize: number;
}
const styles = (theme: Theme) =>
@@ -63,18 +64,18 @@ const styles = (theme: Theme) =>
});
const AddZoneModal = ({
tenant,
classes,
open,
onCloseZoneAndReload,
volumesPerInstance,
volumeSize,
}: IAddZoneProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [zoneName, setZoneName] = useState<string>("");
const [numberOfInstances, setNumberOfInstances] = useState<number>(0);
const [numberOfNodes, setNumberOfNodes] = useState<number>(0);
const [volumeSize, setVolumeSize] = useState<number>(0);
const [volumesPerServer, setVolumesPerSever] = useState<number>(0);
const instanceCapacity: number = volumeSize * volumesPerInstance;
const totalCapacity: number = instanceCapacity * numberOfInstances;
const instanceCapacity: number = volumeSize * 1073741824 * volumesPerServer;
const totalCapacity: number = instanceCapacity * numberOfNodes;
return (
<ModalWrapper
@@ -88,30 +89,66 @@ const AddZoneModal = ({
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
const data: IAddZoneRequest = {
name: "",
servers: numberOfNodes,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: volumeSize * 1073741824,
storage_class: "",
labels: null,
},
};
api
.invoke(
"POST",
`/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/zones`,
data
)
.then(() => {
setAddSending(false);
onCloseZoneAndReload(true);
})
.catch((err) => {
setAddSending(false);
// setDeleteError(err);
});
}}
>
<Grid item xs={12}>
<InputBoxWrapper
id="zone_name"
name="zone_name"
type="string"
id="number_of_nodes"
name="number_of_nodes"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setZoneName(e.target.value);
setNumberOfNodes(parseInt(e.target.value));
}}
label="Name"
value={zoneName}
label="Number o Nodes"
value={numberOfNodes.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="number_instances"
name="number_instances"
id="zone_size"
name="zone_size"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
setVolumeSize(parseInt(e.target.value));
}}
label="Drives per Server"
value={numberOfInstances.toString(10)}
label="Volume Size (Gi)"
value={volumeSize.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_sever"
name="volumes_per_sever"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerSever(parseInt(e.target.value));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12}>

View File

@@ -91,19 +91,6 @@ const styles = (theme: Theme) =>
...modalBasic,
});
const mainPagination = {
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
ActionsComponent: MinTablePaginationActions,
};
const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
@@ -142,24 +129,38 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const loadInfo = () => {
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
setLoading(true);
api
.invoke("GET", `/api/v1/tenants/${tenantName}`)
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}`
)
.then((res: ITenant) => {
const total = res.volume_count * res.volume_size;
setCapacity(total);
setZoneCount(res.zone_count);
setVolumes(res.volume_count);
setInstances(res.instance_count);
const resZones = !res.zones ? [] : res.zones;
const total = res.volume_count * res.volume_size;
let totalInstances = 0;
let totalVolumes = 0;
let count = 1;
for (let zone of resZones) {
zone.volumes = res.volumes_per_server;
const cap = res.volumes_per_server * res.volume_size * zone.servers;
const cap =
zone.volumes_per_server *
zone.servers *
zone.volume_configuration.size;
zone.name = `zone-${count}`;
zone.capacity = niceBytes(cap + "");
zone.volumes = zone.servers * zone.volumes_per_server;
totalInstances += zone.servers;
totalVolumes += zone.volumes;
count += 1;
}
setCapacity(res.total_size);
setZoneCount(resZones.length);
setVolumes(totalVolumes);
setInstances(totalInstances);
setZones(resZones);
setTenant(res);
@@ -182,8 +183,7 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
<AddZoneModal
open={addZoneOpen}
onCloseZoneAndReload={onCloseZoneAndRefresh}
volumeSize={tenant.volume_size}
volumesPerInstance={tenant.volumes_per_server}
tenant={tenant}
/>
)}
{addBucketOpen && (
@@ -201,7 +201,7 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Tenant > {match.params["tenantName"]}
{`Tenant > ${match.params["tenantName"]}`}
</Typography>
</Grid>
<Grid item xs={12}>
@@ -212,40 +212,18 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
<div className={classes.infoGrid}>
<div>Capacity:</div>
<div>{niceBytes(capacity.toString(10))}</div>
<div>Minio:</div>
<div>{tenant ? tenant.image : ""}</div>
<div>Zones:</div>
<div>{zoneCount}</div>
<div>External IDP:</div>
<div>
{externalIDP ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Console:</div>
<div>{tenant ? tenant.console_image : ""}</div>
<div>Instances:</div>
<div>{instances}</div>
<div>External KMS:</div>
<div>{externalKMS ? "Yes" : "No"}&nbsp;&nbsp;</div>
<div>Volumes:</div>
<div>{volumes}</div>
</div>
</Paper>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
Warp
</Button>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
@@ -261,185 +239,59 @@ const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
aria-label="tenant-tabs"
>
<Tab label="Zones" />
<Tab label="Buckets" />
<Tab label="Replication" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{selectedTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddZone(true);
}}
>
Add Zone
</Button>
)}
{selectedTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddBucketOpen(true);
}}
>
Create Bucket
</Button>
)}
{selectedTab === 2 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddReplicationOpen(true);
}}
>
Add Replication
</Button>
)}
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddZone(true);
}}
>
Add Zone
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
{selectedTab === 0 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
<TableWrapper
itemActions={[
{
type: "delete",
onClick: (element) => {
console.log(element);
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={false}
records={zones}
entityName="Zones"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 1 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "replicate",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "mirror",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "AccessPolicy", elementKey: "access_policy" },
]}
isLoading={false}
records={[]}
entityName="Buckets"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 2 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Source",
elementKey: "source",
},
{ label: "Source Bucket", elementKey: "source_bucket" },
{ label: "Destination", elementKey: "destination" },
{
label: "Destination Bucket",
elementKey: "destination_bucket",
},
]}
isLoading={false}
records={[]}
entityName="Replication"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: () => {},
onChangeRowsPerPage: () => {},
ActionsComponent: MinTablePaginationActions,
}}
/>
)}
sendOnlyId: true,
},
]}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "servers" },
{ label: "# of Drives", elementKey: "volumes" },
]}
isLoading={false}
records={zones}
entityName="Zones"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: zoneCount,
rowsPerPage: 10,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
ActionsComponent: MinTablePaginationActions,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -19,6 +19,8 @@ export interface IValidation {
required: boolean;
pattern?: RegExp;
customPatternMessage?: string;
customValidation?: boolean;
customValidationMessage?: string;
value: string;
}
@@ -31,12 +33,18 @@ export const commonFormValidation = (fieldsValidate: IValidation[]) => {
return;
}
if (field.customValidation && field.customValidationMessage) {
returnErrors[field.fieldKey] = field.customValidationMessage;
return;
}
if (field.pattern && field.customPatternMessage) {
const rgx = new RegExp(field.pattern, "g");
if (!field.value.match(rgx)) {
returnErrors[field.fieldKey] = field.customPatternMessage;
}
return;
}
});

View File

@@ -4119,7 +4119,7 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
debuglog@*, debuglog@^1.0.1:
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -6145,7 +6145,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
imurmurhash@*, imurmurhash@^0.1.4:
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -7709,11 +7709,6 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
lodash._baseindexof@*:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -7722,33 +7717,11 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
lodash._bindcallback@*:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
lodash._cacheindexof@*:
version "3.0.2"
resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
lodash._createcache@*:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
dependencies:
lodash._getnative "^3.0.0"
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
lodash._getnative@*, lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -7774,11 +7747,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.restparam@*:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"

63
restapi/admin_parity.go Normal file
View File

@@ -0,0 +1,63 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"fmt"
"log"
"github.com/minio/console/pkg/utils"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
"github.com/minio/console/restapi/operations/admin_api"
)
func registerParityHandlers(api *operations.ConsoleAPI) {
api.AdminAPIGetParityHandler = admin_api.GetParityHandlerFunc(func(params admin_api.GetParityParams, principal *models.Principal) middleware.Responder {
resp, err := getParityResponse(params)
if err != nil {
return admin_api.NewGetParityDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewGetParityOK().WithPayload(resp)
})
}
func GetParityInfo(nodes int64, disksPerNode int64) (models.ParityResponse, error) {
parityVals, err := utils.PossibleParityValues(fmt.Sprintf(`http://minio{1...%d}/export/set{1...%d}`, nodes, disksPerNode))
if err != nil {
return nil, err
}
return parityVals, nil
}
func getParityResponse(params admin_api.GetParityParams) (models.ParityResponse, *models.Error) {
nodes := params.Nodes
disksPerNode := params.DisksPerNode
parityValues, err := GetParityInfo(nodes, disksPerNode)
if err != nil {
log.Println("error getting parity info:", err)
return nil, prepareError(err)
}
return parityValues, nil
}

View File

@@ -0,0 +1,79 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"encoding/json"
"reflect"
"testing"
"github.com/minio/console/models"
)
func Test_getParityInfo(t *testing.T) {
tests := []struct {
description string
wantErr bool
nodes int64
disksPerNode int64
expectedResp models.ParityResponse
}{
{
description: "Incorrect Number of endpoints provided",
wantErr: true,
nodes: 1,
disksPerNode: 1,
expectedResp: nil,
},
{
description: "Number of endpoints is valid",
wantErr: false,
nodes: 4,
disksPerNode: 10,
expectedResp: models.ParityResponse{"EC:4", "EC:3", "EC:2"},
},
{
description: "More nodes than disks",
wantErr: false,
nodes: 4,
disksPerNode: 1,
expectedResp: models.ParityResponse{"EC:2"},
},
{
description: "More disks than nodes",
wantErr: false,
nodes: 2,
disksPerNode: 50,
expectedResp: models.ParityResponse{"EC:5", "EC:4", "EC:3", "EC:2"},
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
parity, err := GetParityInfo(tt.nodes, tt.disksPerNode)
if (err != nil) != tt.wantErr {
t.Errorf("GetParityInfo() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(parity, tt.expectedResp) {
ji, _ := json.Marshal(parity)
vi, _ := json.Marshal(tt.expectedResp)
t.Errorf("\ngot: %s \nwant: %s", ji, vi)
}
})
}
}

View File

@@ -119,7 +119,6 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
api.AdminAPITenantAddZoneHandler = admin_api.TenantAddZoneHandlerFunc(func(params admin_api.TenantAddZoneParams, session *models.Principal) middleware.Responder {
err := getTenantAddZoneResponse(session, params)
if err != nil {
log.Println(err)
return admin_api.NewTenantAddZoneDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewTenantAddZoneCreated()
@@ -129,7 +128,6 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
api.AdminAPIGetTenantUsageHandler = admin_api.GetTenantUsageHandlerFunc(func(params admin_api.GetTenantUsageParams, session *models.Principal) middleware.Responder {
payload, err := getTenantUsageResponse(session, params)
if err != nil {
log.Println(err)
return admin_api.NewGetTenantUsageDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewGetTenantUsageOK().WithPayload(payload)
@@ -139,7 +137,6 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
api.AdminAPITenantUpdateZonesHandler = admin_api.TenantUpdateZonesHandlerFunc(func(params admin_api.TenantUpdateZonesParams, session *models.Principal) middleware.Responder {
resp, err := getTenantUpdateZoneResponse(session, params)
if err != nil {
log.Println(err)
return admin_api.NewTenantUpdateZonesDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewTenantUpdateZonesOK().WithPayload(resp)
@@ -149,8 +146,7 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
api.AdminAPITenantUpdateCertificateHandler = admin_api.TenantUpdateCertificateHandlerFunc(func(params admin_api.TenantUpdateCertificateParams, session *models.Principal) middleware.Responder {
err := getTenantUpdateCertificatesResponse(session, params)
if err != nil {
log.Println(err)
return admin_api.NewGetTenantUsageDefault(int(err.Code)).WithPayload(err)
return admin_api.NewTenantUpdateCertificateDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewTenantUpdateCertificateCreated()
})
@@ -159,10 +155,9 @@ func registerTenantHandlers(api *operations.ConsoleAPI) {
api.AdminAPITenantUpdateEncryptionHandler = admin_api.TenantUpdateEncryptionHandlerFunc(func(params admin_api.TenantUpdateEncryptionParams, session *models.Principal) middleware.Responder {
err := getTenantUpdateEncryptionResponse(session, params)
if err != nil {
log.Println(err)
return admin_api.NewGetTenantUsageDefault(int(err.Code)).WithPayload(err)
return admin_api.NewTenantUpdateEncryptionDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewTenantUpdateCertificateCreated()
return admin_api.NewTenantUpdateEncryptionCreated()
})
}
@@ -223,15 +218,19 @@ func deleteTenantAction(
return nil
}
func getTenantScheme(mi *operator.Tenant) string {
// GetTenantServiceURL gets tenant's service url with the proper scheme and port
func GetTenantServiceURL(mi *operator.Tenant) (svcURL string) {
scheme := "http"
port := operator.MinIOPortLoadBalancerSVC
if mi.AutoCert() || mi.ExternalCert() {
scheme = "https"
port = operator.MinIOTLSPortLoadBalancerSVC
}
return scheme
svc := fmt.Sprintf("%s.%s.svc.cluster.local", mi.MinIOCIServiceName(), mi.Namespace)
return fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(svc, strconv.Itoa(port)))
}
func getTenantAdminClient(ctx context.Context, client K8sClientI, namespace, tenantName, serviceName, scheme string, insecure bool) (*madmin.AdminClient, error) {
func getTenantAdminClient(ctx context.Context, client K8sClientI, namespace, tenantName, svcURL string, insecure bool) (*madmin.AdminClient, error) {
// get admin credentials from secret
creds, err := client.getSecret(ctx, namespace, fmt.Sprintf("%s-secret", tenantName), metav1.GetOptions{})
if err != nil {
@@ -247,7 +246,7 @@ func getTenantAdminClient(ctx context.Context, client K8sClientI, namespace, ten
log.Println("tenant's secret doesn't contain secretkey")
return nil, errorGeneric
}
mAdmin, pErr := NewAdminClientWithInsecure(scheme+"://"+net.JoinHostPort(serviceName, strconv.Itoa(operator.MinIOPort)), string(accessKey), string(secretkey), insecure)
mAdmin, pErr := NewAdminClientWithInsecure(svcURL, string(accessKey), string(secretkey), insecure)
if pErr != nil {
return nil, pErr.Cause
}
@@ -262,24 +261,53 @@ func getTenant(ctx context.Context, operatorClient OperatorClientI, namespace, t
return minInst, nil
}
func isPrometheusEnabled(annotations map[string]string) bool {
if annotations == nil {
return false
}
// if one of the following prometheus annotations are not present
// we consider the tenant as not integrated with prometheus
if _, ok := annotations[prometheusPath]; !ok {
return false
}
if _, ok := annotations[prometheusPort]; !ok {
return false
}
if _, ok := annotations[prometheusScrape]; !ok {
return false
}
return true
}
func getTenantInfo(tenant *operator.Tenant) *models.Tenant {
var zones []*models.Zone
consoleImage := ""
var totalSize int64
for _, z := range tenant.Spec.Zones {
zones = append(zones, parseTenantZone(&z))
zoneSize := int64(z.Servers) * int64(z.VolumesPerServer) * z.VolumeClaimTemplate.Spec.Resources.Requests.Storage().Value()
totalSize = totalSize + zoneSize
}
var deletion string
if tenant.ObjectMeta.DeletionTimestamp != nil {
deletion = tenant.ObjectMeta.DeletionTimestamp.String()
}
if tenant.HasConsoleEnabled() {
consoleImage = tenant.Spec.Console.Image
}
return &models.Tenant{
CreationDate: tenant.ObjectMeta.CreationTimestamp.String(),
Name: tenant.Name,
TotalSize: totalSize,
CurrentState: tenant.Status.CurrentState,
Zones: zones,
Namespace: tenant.ObjectMeta.Namespace,
Image: tenant.Spec.Image,
CreationDate: tenant.ObjectMeta.CreationTimestamp.String(),
DeletionDate: deletion,
Name: tenant.Name,
TotalSize: totalSize,
CurrentState: tenant.Status.CurrentState,
Zones: zones,
Namespace: tenant.ObjectMeta.Namespace,
Image: tenant.Spec.Image,
ConsoleImage: consoleImage,
EnablePrometheus: isPrometheusEnabled(tenant.Annotations),
}
}
@@ -335,8 +363,14 @@ func listTenants(ctx context.Context, operatorClient OperatorClientI, namespace
}
}
var deletion string
if tenant.ObjectMeta.DeletionTimestamp != nil {
deletion = tenant.ObjectMeta.DeletionTimestamp.String()
}
tenants = append(tenants, &models.TenantList{
CreationDate: tenant.ObjectMeta.CreationTimestamp.String(),
DeletionDate: deletion,
Name: tenant.ObjectMeta.Name,
ZoneCount: int64(len(tenant.Spec.Zones)),
InstanceCount: instanceCount,
@@ -386,7 +420,7 @@ func getListTenantsResponse(session *models.Principal, params admin_api.ListTena
return listT, nil
}
func getTenantCreatedResponse(session *models.Principal, params admin_api.CreateTenantParams) (*models.CreateTenantResponse, *models.Error) {
func getTenantCreatedResponse(session *models.Principal, params admin_api.CreateTenantParams) (response *models.CreateTenantResponse, mError *models.Error) {
tenantReq := params.Body
minioImage := tenantReq.Image
ctx := context.Background()
@@ -442,11 +476,24 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
if err != nil {
return nil, prepareError(err)
}
// delete secrets created if an error occurred during tenant creation,
defer func() {
if mError != nil {
log.Printf("deleting secrets created for failed tenant: %s if any\n", tenantName)
opts := metav1.ListOptions{
LabelSelector: fmt.Sprintf("%s=%s", operator.TenantLabel, tenantName),
}
err = clientSet.CoreV1().Secrets(ns).DeleteCollection(ctx, metav1.DeleteOptions{}, opts)
if err != nil {
log.Println("error deleting tenant's secrets:", err)
}
}
}()
var envrionmentVariables []corev1.EnvVar
// Check the Erasure Coding Parity for validity and pass it to Tenant
if tenantReq.ErasureCodingParity > 0 {
if tenantReq.ErasureCodingParity < 2 && tenantReq.ErasureCodingParity > 8 {
if tenantReq.ErasureCodingParity < 2 || tenantReq.ErasureCodingParity > 8 {
return nil, prepareError(errorInvalidErasureCodingValue)
}
envrionmentVariables = append(envrionmentVariables, corev1.EnvVar{
@@ -458,7 +505,8 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
//Construct a MinIO Instance with everything we are getting from parameters
minInst := operator.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: tenantName,
Name: tenantName,
Labels: tenantReq.Labels,
},
Spec: operator.TenantSpec{
Image: minioImage,
@@ -521,16 +569,16 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
}
}
isEncryptionAvailable := false
isEncryptionEnabled := false
if tenantReq.EnableTLS != nil && *tenantReq.EnableTLS {
// If user request autoCert, Operator will generate certificate keypair for MinIO (server), Console (server) and KES (server and app mTLS)
isEncryptionAvailable = true
isEncryptionEnabled = true
minInst.Spec.RequestAutoCert = *tenantReq.EnableTLS
}
if !minInst.Spec.RequestAutoCert && tenantReq.TLS != nil && tenantReq.TLS.Minio != nil {
// User provided TLS certificates for MinIO
isEncryptionAvailable = true
isEncryptionEnabled = true
// disable autoCert
minInst.Spec.RequestAutoCert = false
// Certificates used by the MinIO instance
@@ -542,7 +590,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
minInst.Spec.ExternalCertSecret = externalCertSecret
}
if tenantReq.Encryption != nil && isEncryptionAvailable {
if tenantReq.Encryption != nil && isEncryptionEnabled {
// Enable auto encryption
minInst.Spec.Env = append(minInst.Spec.Env, corev1.EnvVar{
Name: "MINIO_KMS_AUTO_ENCRYPTION",
@@ -561,6 +609,10 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
if err != nil {
return nil, prepareError(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
}
// optionals are set below
@@ -621,7 +673,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
return nil, prepareError(errorGeneric)
}
const consoleVersion = "minio/console:v0.3.20"
const consoleVersion = "minio/console:v0.3.26"
minInst.Spec.Console = &operator.ConsoleConfiguration{
Replicas: 1,
Image: consoleVersion,
@@ -641,6 +693,13 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
}
minInst.Spec.Console.ExternalCertSecret = externalCertSecret
}
// Set Labels, Annotations and Node Selector for Console
if tenantReq.Console != nil {
minInst.Spec.Console.Annotations = tenantReq.Console.Annotations
minInst.Spec.Console.Labels = tenantReq.Console.Labels
minInst.Spec.Console.NodeSelector = tenantReq.Console.NodeSelector
}
}
// set the service name if provided
@@ -649,18 +708,14 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
}
// add annotations
var annotations map[string]string
if minInst.Spec.Metadata == nil {
minInst.Spec.Metadata = &metav1.ObjectMeta{
Annotations: map[string]string{},
}
}
if len(tenantReq.Annotations) > 0 {
annotations = tenantReq.Annotations
minInst.Spec.Metadata.Annotations = annotations
minInst.Annotations = annotations
}
// set the zones if they are provided
for _, zone := range tenantReq.Zones {
zone, err := parseTenantZoneRequest(zone, annotations)
zone, err := parseTenantZoneRequest(zone)
if err != nil {
return nil, prepareError(err)
}
@@ -677,7 +732,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
if tenantReq.ImagePullSecret != "" {
imagePullSecret = tenantReq.ImagePullSecret
} else if imagePullSecret, err = setImageRegistry(ctx, tenantName, tenantReq.ImageRegistry, clientSet.CoreV1(), ns); err != nil {
} 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
@@ -688,10 +743,10 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
}
// prometheus annotations support
if tenantReq.EnablePrometheus != nil && *tenantReq.EnablePrometheus && minInst.Spec.Metadata != nil && minInst.Spec.Metadata.Annotations != nil {
minInst.Spec.Metadata.Annotations["prometheus.io/path"] = "/minio/prometheus/metrics"
minInst.Spec.Metadata.Annotations["prometheus.io/port"] = fmt.Sprint(operator.MinIOPort)
minInst.Spec.Metadata.Annotations["prometheus.io/scrape"] = "true"
if tenantReq.EnablePrometheus != nil && *tenantReq.EnablePrometheus && minInst.Annotations != nil {
minInst.Annotations[prometheusPath] = "/minio/prometheus/metrics"
minInst.Annotations[prometheusPort] = fmt.Sprint(operator.MinIOPort)
minInst.Annotations[prometheusScrape] = "true"
}
// set console image if provided
@@ -716,7 +771,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
return nil, prepareError(err)
}
}
response := &models.CreateTenantResponse{
response = &models.CreateTenantResponse{
AccessKey: accessKey,
SecretKey: secretKey,
}
@@ -732,7 +787,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
// 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, tenantName string, req *models.ImageRegistry, clientset v1.CoreV1Interface, namespace string) (string, error) {
func setImageRegistry(ctx context.Context, req *models.ImageRegistry, clientset v1.CoreV1Interface, namespace, tenantName string) (string, error) {
if req == nil || req.Registry == nil || req.Username == nil || req.Password == nil {
return "", nil
}
@@ -756,24 +811,23 @@ func setImageRegistry(ctx context.Context, tenantName string, req *models.ImageR
}
pullSecretName := fmt.Sprintf("%s-regcred", tenantName)
instanceSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: pullSecretName,
Labels: map[string]string{
operator.TenantLabel: tenantName,
},
},
Data: map[string][]byte{
corev1.DockerConfigJsonKey: []byte(string(imRegistryJSON)),
},
Type: corev1.SecretTypeDockerConfigJson,
secretCredentials := map[string][]byte{
corev1.DockerConfigJsonKey: []byte(string(imRegistryJSON)),
}
// Get or Create secret if it doesn't exist
_, err = clientset.Secrets(namespace).Get(ctx, pullSecretName, metav1.GetOptions{})
currentSecret, err := clientset.Secrets(namespace).Get(ctx, pullSecretName, metav1.GetOptions{})
if err != nil {
if k8sErrors.IsNotFound(err) {
instanceSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: pullSecretName,
Labels: map[string]string{
operator.TenantLabel: tenantName,
},
},
Data: secretCredentials,
Type: corev1.SecretTypeDockerConfigJson,
}
_, err = clientset.Secrets(namespace).Create(ctx, &instanceSecret, metav1.CreateOptions{})
if err != nil {
return "", err
@@ -782,7 +836,8 @@ func setImageRegistry(ctx context.Context, tenantName string, req *models.ImageR
}
return "", err
}
_, err = clientset.Secrets(namespace).Update(ctx, &instanceSecret, metav1.UpdateOptions{})
currentSecret.Data = secretCredentials
_, err = clientset.Secrets(namespace).Update(ctx, currentSecret, metav1.UpdateOptions{})
if err != nil {
return "", err
}
@@ -803,7 +858,7 @@ func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, cli
minInst.Spec.ImagePullSecret.Name = params.Body.ImagePullSecret
} else {
// update the image pull secret content
if _, err := setImageRegistry(ctx, params.Tenant, imageRegistryReq, clientset, namespace); err != nil {
if _, err := setImageRegistry(ctx, imageRegistryReq, clientset, namespace, params.Tenant); err != nil {
log.Println("error setting image registry secret:", err)
return err
}
@@ -825,6 +880,35 @@ func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, cli
}
}
// Prometheus Annotations
currentAnnotations := minInst.Annotations
prometheusAnnotations := map[string]string{
prometheusPath: "/minio/prometheus/metrics",
prometheusPort: fmt.Sprint(operator.MinIOPort),
prometheusScrape: "true",
}
if params.Body.EnablePrometheus && currentAnnotations != nil {
// add prometheus annotations to the tenant
minInst.Annotations = addAnnotations(currentAnnotations, prometheusAnnotations)
// add prometheus annotations to the each zone
if minInst.Spec.Zones != nil {
for _, zone := range minInst.Spec.Zones {
zoneAnnotations := zone.VolumeClaimTemplate.GetObjectMeta().GetAnnotations()
zone.VolumeClaimTemplate.GetObjectMeta().SetAnnotations(addAnnotations(zoneAnnotations, prometheusAnnotations))
}
}
} else {
// remove prometheus annotations to the tenant
minInst.Annotations = removeAnnotations(currentAnnotations, prometheusAnnotations)
// add prometheus annotations from each zone
if minInst.Spec.Zones != nil {
for _, zone := range minInst.Spec.Zones {
zoneAnnotations := zone.VolumeClaimTemplate.GetObjectMeta().GetAnnotations()
zone.VolumeClaimTemplate.GetObjectMeta().SetAnnotations(removeAnnotations(zoneAnnotations, prometheusAnnotations))
}
}
}
payloadBytes, err := json.Marshal(minInst)
if err != nil {
return err
@@ -836,6 +920,28 @@ func updateTenantAction(ctx context.Context, operatorClient OperatorClientI, cli
return nil
}
// addAnnotations will merge two annotation maps
func addAnnotations(annotationsOne, annotationsTwo map[string]string) map[string]string {
if annotationsOne == nil {
annotationsOne = map[string]string{}
}
for key, value := range annotationsTwo {
annotationsOne[key] = value
}
return annotationsOne
}
// removeAnnotations will remove keys from the first annotations map based on the second one
func removeAnnotations(annotationsOne, annotationsTwo map[string]string) map[string]string {
if annotationsOne == nil {
annotationsOne = map[string]string{}
}
for key := range annotationsTwo {
delete(annotationsOne, key)
}
return annotationsOne
}
func getUpdateTenantResponse(session *models.Principal, params admin_api.UpdateTenantParams) *models.Error {
ctx := context.Background()
opClientClientSet, err := cluster.OperatorClient(session.SessionToken)
@@ -869,7 +975,7 @@ func addTenantZone(ctx context.Context, operatorClient OperatorClientI, params a
}
zoneParams := params.Body
zone, err := parseTenantZoneRequest(zoneParams, tenant.ObjectMeta.Annotations)
zone, err := parseTenantZoneRequest(zoneParams)
if err != nil {
return err
}
@@ -928,17 +1034,15 @@ func getTenantUsageResponse(session *models.Principal, params admin_api.GetTenan
return nil, prepareError(err, errorUnableToGetTenantUsage)
}
minTenant.EnsureDefaults()
tenantScheme := getTenantScheme(minTenant)
svcName := fmt.Sprintf("%s.%s.svc.cluster.local", minTenant.MinIOCIServiceName(), minTenant.Namespace)
svcURL := GetTenantServiceURL(minTenant)
mAdmin, err := getTenantAdminClient(
ctx,
k8sClient,
params.Namespace,
params.Tenant,
svcName,
tenantScheme,
svcURL,
true)
if err != nil {
return nil, prepareError(err, errorUnableToGetTenantUsage)
@@ -957,7 +1061,7 @@ func getTenantUsageResponse(session *models.Principal, params admin_api.GetTenan
// parseTenantZoneRequest parse zone request and returns the equivalent
// operator.Zone object
func parseTenantZoneRequest(zoneParams *models.Zone, annotations map[string]string) (*operator.Zone, error) {
func parseTenantZoneRequest(zoneParams *models.Zone) (*operator.Zone, error) {
if zoneParams.VolumeConfiguration == nil {
return nil, errors.New("a volume configuration must be specified")
}
@@ -1106,14 +1210,12 @@ func parseTenantZoneRequest(zoneParams *models.Zone, annotations map[string]stri
// Pass annotations to the volume
vct := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
Labels: zoneParams.VolumeConfiguration.Labels,
Name: "data",
Labels: zoneParams.VolumeConfiguration.Labels,
Annotations: zoneParams.VolumeConfiguration.Annotations,
},
Spec: volTemp,
}
if len(annotations) > 0 {
vct.ObjectMeta.Annotations = annotations
}
zone := &operator.Zone{
Name: zoneParams.Name,
@@ -1407,16 +1509,10 @@ func updateTenantZones(
return nil, err
}
if minInst.Spec.Metadata == nil {
minInst.Spec.Metadata = &metav1.ObjectMeta{
Annotations: map[string]string{},
}
}
// set the zones if they are provided
var newZoneArray []operator.Zone
for _, zone := range zonesReq {
zone, err := parseTenantZoneRequest(zone, minInst.Spec.Metadata.Annotations)
zone, err := parseTenantZoneRequest(zone)
if err != nil {
return nil, err
}
@@ -1426,6 +1522,9 @@ func updateTenantZones(
// replace zones array
minInst.Spec.Zones = newZoneArray
minInst = minInst.DeepCopy()
minInst.EnsureDefaults()
payloadBytes, err := json.Marshal(minInst)
if err != nil {
return nil, err

View File

@@ -183,10 +183,6 @@ func getTenantUpdateEncryptionResponse(session *models.Principal, params admin_a
// getKESConfiguration will generate the KES server certificate secrets, the tenant client secrets for mTLS authentication between MinIO and KES and the
// kes-configuration.yaml file used by the KES service (how to connect to the external KMS, eg: Vault, AWS, Gemalto, etc)
func getKESConfiguration(ctx context.Context, clientSet K8sClientI, ns string, encryptionCfg *models.EncryptionConfiguration, secretName, tenantName string, autoCert bool) (kesConfiguration *operator.KESConfig, err error) {
// Secrets used by the MiniO tenant service
//
// tenantExternalClientCertSecretName is the name of the secret that will store the certificates for mTLS between MinIO and the KES, eg: app.key and app.crt
tenantExternalClientCertSecretName := fmt.Sprintf("%s-tenant-external-client-cert", secretName)
// Secrets used by the KES service
//
// kesExternalCertSecretName is the name of the secret that will store the certificates for TLS in the KES server, eg: server.key and server.crt
@@ -195,32 +191,10 @@ func getKESConfiguration(ctx context.Context, clientSet K8sClientI, ns string, e
kesClientCertSecretName := fmt.Sprintf("%s-kes-client-cert", secretName)
// kesConfigurationSecretName is the name of the secret that will store the configuration file, eg: kes-configuration.yaml
kesConfigurationSecretName := fmt.Sprintf("%s-kes-configuration", secretName)
// if there's an error during this process we delete all KES configuration secrets
defer func() {
if err != nil {
errDelete := clientSet.deleteSecret(ctx, ns, tenantExternalClientCertSecretName, metav1.DeleteOptions{})
if errDelete != nil {
log.Print(errDelete)
}
errDelete = clientSet.deleteSecret(ctx, ns, kesExternalCertSecretName, metav1.DeleteOptions{})
if errDelete != nil {
log.Print(errDelete)
}
errDelete = clientSet.deleteSecret(ctx, ns, kesClientCertSecretName, metav1.DeleteOptions{})
if errDelete != nil {
log.Print(errDelete)
}
errDelete = clientSet.deleteSecret(ctx, ns, kesConfigurationSecretName, metav1.DeleteOptions{})
if errDelete != nil {
log.Print(errDelete)
}
return
}
}()
kesConfiguration = &operator.KESConfig{
Image: "minio/kes:v0.11.0",
Replicas: 1,
Metadata: nil,
}
// Using custom image for KES
if encryptionCfg.Image != "" {

View File

@@ -87,13 +87,12 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
ctx := context.Background()
kClient := k8sClientMock{}
type args struct {
ctx context.Context
client K8sClientI
namespace string
tenantName string
serviceName string
scheme string
insecure bool
ctx context.Context
client K8sClientI
namespace string
tenantName string
serviceURL string
insecure bool
}
tests := []struct {
name string
@@ -105,12 +104,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Return Tenant Admin, no errors",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
@@ -134,12 +132,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Access key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
@@ -162,12 +159,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Secret key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
@@ -190,12 +186,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Handle error on getService",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
@@ -214,12 +209,11 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Handle error on getSecret",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("error")
@@ -239,7 +233,7 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
k8sclientGetSecretMock = tt.mockGetSecret
k8sclientGetServiceMock = tt.mockGetService
t.Run(tt.name, func(t *testing.T) {
got, err := getTenantAdminClient(tt.args.ctx, tt.args.client, tt.args.namespace, tt.args.tenantName, tt.args.serviceName, tt.args.scheme, tt.args.insecure)
got, err := getTenantAdminClient(tt.args.ctx, tt.args.client, tt.args.namespace, tt.args.tenantName, tt.args.serviceURL, tt.args.insecure)
if err != nil {
if tt.wantErr {
return
@@ -257,7 +251,6 @@ func Test_TenantInfo(t *testing.T) {
testTimeStamp := metav1.Now()
type args struct {
minioTenant *operator.Tenant
tenantInfo *usageInfo
}
tests := []struct {
name string
@@ -298,9 +291,6 @@ func Test_TenantInfo(t *testing.T) {
CurrentState: "ready",
},
},
tenantInfo: &usageInfo{
DisksUsage: 1024,
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
@@ -318,8 +308,143 @@ func Test_TenantInfo(t *testing.T) {
},
},
},
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
EnablePrometheus: false,
},
},
{
// Description if DeletionTimeStamp is present, value should be returned as string
// If Prometheus annotations are present, EnablePrometheus should be returned as true
// All three annotations should be defined to consider Prometheus enabled
name: "Get tenant Info w DeletionTimeStamp and Prometheus",
args: args{
minioTenant: &operator.Tenant{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: testTimeStamp,
Name: "tenant1",
Namespace: "minio-ns",
DeletionTimestamp: &testTimeStamp,
Annotations: map[string]string{
prometheusPath: "some/path",
prometheusPort: "other/path",
prometheusScrape: "other/path",
},
},
Spec: operator.TenantSpec{
Zones: []operator.Zone{
{
Name: "zone1",
Servers: int32(2),
VolumesPerServer: 4,
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{
Spec: corev1.PersistentVolumeClaimSpec{
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: resource.MustParse("1Mi"),
},
},
StorageClassName: swag.String("standard"),
},
},
},
},
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
},
Status: operator.TenantStatus{
CurrentState: "ready",
},
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
DeletionDate: testTimeStamp.String(),
Name: "tenant1",
TotalSize: int64(8388608),
CurrentState: "ready",
Zones: []*models.Zone{
{
Name: "zone1",
Servers: swag.Int64(int64(2)),
VolumesPerServer: swag.Int32(4),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
StorageClassName: "standard",
Size: swag.Int64(1024 * 1024),
},
},
},
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
EnablePrometheus: true,
},
},
{
// If Prometheus annotations are present, EnablePrometheus should be returned as true
// All three annotations should be defined to consider Prometheus enabled
name: "Get tenant Info, not all Prometheus annotations",
args: args{
minioTenant: &operator.Tenant{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: testTimeStamp,
Name: "tenant1",
Namespace: "minio-ns",
Annotations: map[string]string{
prometheusPath: "some/path",
prometheusScrape: "other/path",
},
},
Spec: operator.TenantSpec{
Zones: []operator.Zone{},
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
},
Status: operator.TenantStatus{
CurrentState: "ready",
},
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
Name: "tenant1",
CurrentState: "ready",
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
EnablePrometheus: false,
},
},
{
// If console image is set, it should be returned on tenant info
name: "Get tenant Info, Console image set",
args: args{
minioTenant: &operator.Tenant{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: testTimeStamp,
Name: "tenant1",
Namespace: "minio-ns",
Annotations: map[string]string{
prometheusPath: "some/path",
prometheusScrape: "other/path",
},
},
Spec: operator.TenantSpec{
Zones: []operator.Zone{},
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
Console: &operator.ConsoleConfiguration{
Image: "minio/console:master",
},
},
Status: operator.TenantStatus{
CurrentState: "ready",
},
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
Name: "tenant1",
CurrentState: "ready",
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
EnablePrometheus: false,
ConsoleImage: "minio/console:master",
},
},
}
@@ -871,7 +996,7 @@ func Test_UpdateTenantAction(t *testing.T) {
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
ConsoleImage: "minio/console:v0.3.20",
ConsoleImage: "minio/console:v0.3.26",
},
},
},

View File

@@ -114,6 +114,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
registerResourceQuotaHandlers(api)
// Register Nodes' handlers
registerNodesHandlers(api)
// Register Parity' handlers
registerParityHandlers(api)
api.PreServerShutdown = func() {}

View File

@@ -50,3 +50,11 @@ const (
ConsoleSecureFeaturePolicy = "CONSOLE_SECURE_FEATURE_POLICY"
ConsoleSecureExpectCTHeader = "CONSOLE_SECURE_EXPECT_CT_HEADER"
)
// prometheus annotations
const (
prometheusPath = "prometheus.io/path"
prometheusPort = "prometheus.io/port"
prometheusScrape = "prometheus.io/scrape"
)

View File

@@ -572,6 +572,45 @@ func init() {
}
}
},
"/get-parity/{nodes}/{disksPerNode}": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Gets parity by sending number of nodes \u0026 number of disks",
"operationId": "GetParity",
"parameters": [
{
"minimum": 2,
"type": "integer",
"name": "nodes",
"in": "path",
"required": true
},
{
"minimum": 1,
"type": "integer",
"name": "disksPerNode",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/parityResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/groups": {
"get": {
"tags": [
@@ -2174,6 +2213,21 @@ func init() {
}
}
},
"consoleConfiguration": {
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
{
"type": "object",
"properties": {
"image": {
"type": "string"
}
}
}
]
},
"createTenantRequest": {
"type": "object",
"required": [
@@ -2191,6 +2245,10 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/consoleConfiguration"
},
"console_image": {
"type": "string"
},
@@ -2226,6 +2284,12 @@ func init() {
"image_registry": {
"$ref": "#/definitions/imageRegistry"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"mounth_path": {
"type": "string"
},
@@ -2285,32 +2349,39 @@ func init() {
}
},
"encryptionConfiguration": {
"type": "object",
"properties": {
"aws": {
"type": "object",
"$ref": "#/definitions/awsConfiguration"
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
"client": {
{
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"gemalto": {
"type": "object",
"$ref": "#/definitions/gemaltoConfiguration"
},
"image": {
"type": "string"
},
"server": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"vault": {
"type": "object",
"$ref": "#/definitions/vaultConfiguration"
"properties": {
"aws": {
"type": "object",
"$ref": "#/definitions/awsConfiguration"
},
"client": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"gemalto": {
"type": "object",
"$ref": "#/definitions/gemaltoConfiguration"
},
"image": {
"type": "string"
},
"server": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"vault": {
"type": "object",
"$ref": "#/definitions/vaultConfiguration"
}
}
}
}
]
},
"error": {
"type": "object",
@@ -2686,6 +2757,29 @@ func init() {
}
}
},
"metadataFields": {
"type": "object",
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"node_selector": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"nodeSelectorTerm": {
"description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.",
"type": "object",
@@ -2875,6 +2969,12 @@ func init() {
"get"
]
},
"parityResponse": {
"type": "array",
"items": {
"type": "string"
}
},
"podAffinityTerm": {
"description": "Required. A pod affinity term, associated with the corresponding weight.",
"type": "object",
@@ -3150,12 +3250,21 @@ func init() {
"tenant": {
"type": "object",
"properties": {
"console_image": {
"type": "string"
},
"creation_date": {
"type": "string"
},
"currentState": {
"type": "string"
},
"deletion_date": {
"type": "string"
},
"enable_prometheus": {
"type": "boolean"
},
"image": {
"type": "string"
},
@@ -3186,6 +3295,9 @@ func init() {
"currentState": {
"type": "string"
},
"deletion_date": {
"type": "string"
},
"instance_count": {
"type": "integer"
},
@@ -3257,6 +3369,9 @@ func init() {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
},
"enable_prometheus": {
"type": "boolean"
},
"image": {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
@@ -3423,6 +3538,12 @@ func init() {
"size"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"type": "object",
"additionalProperties": {
@@ -4196,6 +4317,45 @@ func init() {
}
}
},
"/get-parity/{nodes}/{disksPerNode}": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Gets parity by sending number of nodes \u0026 number of disks",
"operationId": "GetParity",
"parameters": [
{
"minimum": 2,
"type": "integer",
"name": "nodes",
"in": "path",
"required": true
},
{
"minimum": 1,
"type": "integer",
"name": "disksPerNode",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/parityResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/groups": {
"get": {
"tags": [
@@ -6074,6 +6234,12 @@ func init() {
"size"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"type": "object",
"additionalProperties": {
@@ -6315,6 +6481,21 @@ func init() {
}
}
},
"consoleConfiguration": {
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
{
"type": "object",
"properties": {
"image": {
"type": "string"
}
}
}
]
},
"createTenantRequest": {
"type": "object",
"required": [
@@ -6332,6 +6513,10 @@ func init() {
"type": "string"
}
},
"console": {
"type": "object",
"$ref": "#/definitions/consoleConfiguration"
},
"console_image": {
"type": "string"
},
@@ -6367,6 +6552,12 @@ func init() {
"image_registry": {
"$ref": "#/definitions/imageRegistry"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"mounth_path": {
"type": "string"
},
@@ -6426,32 +6617,39 @@ func init() {
}
},
"encryptionConfiguration": {
"type": "object",
"properties": {
"aws": {
"type": "object",
"$ref": "#/definitions/awsConfiguration"
"allOf": [
{
"$ref": "#/definitions/metadataFields"
},
"client": {
{
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"gemalto": {
"type": "object",
"$ref": "#/definitions/gemaltoConfiguration"
},
"image": {
"type": "string"
},
"server": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"vault": {
"type": "object",
"$ref": "#/definitions/vaultConfiguration"
"properties": {
"aws": {
"type": "object",
"$ref": "#/definitions/awsConfiguration"
},
"client": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"gemalto": {
"type": "object",
"$ref": "#/definitions/gemaltoConfiguration"
},
"image": {
"type": "string"
},
"server": {
"type": "object",
"$ref": "#/definitions/keyPairConfiguration"
},
"vault": {
"type": "object",
"$ref": "#/definitions/vaultConfiguration"
}
}
}
}
]
},
"error": {
"type": "object",
@@ -6827,6 +7025,29 @@ func init() {
}
}
},
"metadataFields": {
"type": "object",
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"node_selector": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"nodeSelectorTerm": {
"description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.",
"type": "object",
@@ -6972,6 +7193,12 @@ func init() {
"get"
]
},
"parityResponse": {
"type": "array",
"items": {
"type": "string"
}
},
"podAffinityTerm": {
"description": "Required. A pod affinity term, associated with the corresponding weight.",
"type": "object",
@@ -7225,12 +7452,21 @@ func init() {
"tenant": {
"type": "object",
"properties": {
"console_image": {
"type": "string"
},
"creation_date": {
"type": "string"
},
"currentState": {
"type": "string"
},
"deletion_date": {
"type": "string"
},
"enable_prometheus": {
"type": "boolean"
},
"image": {
"type": "string"
},
@@ -7261,6 +7497,9 @@ func init() {
"currentState": {
"type": "string"
},
"deletion_date": {
"type": "string"
},
"instance_count": {
"type": "integer"
},
@@ -7332,6 +7571,9 @@ func init() {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
},
"enable_prometheus": {
"type": "boolean"
},
"image": {
"type": "string",
"pattern": "^((.*?)/(.*?):(.+))$"
@@ -7498,6 +7740,12 @@ func init() {
"size"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"labels": {
"type": "object",
"additionalProperties": {

View File

@@ -87,12 +87,12 @@ func prepareError(err ...error) *models.Error {
errorMessage = errorGenericInvalidSession.Error()
}
// if we received a second error take that as friendly message but dont override the code
if len(err) > 1 {
if len(err) > 1 && err[1] != nil {
log.Print("friendly error: ", err[1].Error())
errorMessage = err[1].Error()
}
// if we receive third error we just print that as debugging
if len(err) > 2 {
if len(err) > 2 && err[2] != nil {
log.Print("debugging error: ", err[2].Error())
}
}

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// GetParityHandlerFunc turns a function with the right signature into a get parity handler
type GetParityHandlerFunc func(GetParityParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn GetParityHandlerFunc) Handle(params GetParityParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// GetParityHandler interface for that can handle valid get parity params
type GetParityHandler interface {
Handle(GetParityParams, *models.Principal) middleware.Responder
}
// NewGetParity creates a new http.Handler for the get parity operation
func NewGetParity(ctx *middleware.Context, handler GetParityHandler) *GetParity {
return &GetParity{Context: ctx, Handler: handler}
}
/*GetParity swagger:route GET /get-parity/{nodes}/{disksPerNode} AdminAPI getParity
Gets parity by sending number of nodes & number of disks
*/
type GetParity struct {
Context *middleware.Context
Handler GetParityHandler
}
func (o *GetParity) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewGetParityParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,154 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// NewGetParityParams creates a new GetParityParams object
// no default values defined in spec.
func NewGetParityParams() GetParityParams {
return GetParityParams{}
}
// GetParityParams contains all the bound params for the get parity operation
// typically these are obtained from a http.Request
//
// swagger:parameters GetParity
type GetParityParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
Minimum: 1
In: path
*/
DisksPerNode int64
/*
Required: true
Minimum: 2
In: path
*/
Nodes int64
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewGetParityParams() beforehand.
func (o *GetParityParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rDisksPerNode, rhkDisksPerNode, _ := route.Params.GetOK("disksPerNode")
if err := o.bindDisksPerNode(rDisksPerNode, rhkDisksPerNode, route.Formats); err != nil {
res = append(res, err)
}
rNodes, rhkNodes, _ := route.Params.GetOK("nodes")
if err := o.bindNodes(rNodes, rhkNodes, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindDisksPerNode binds and validates parameter DisksPerNode from path.
func (o *GetParityParams) bindDisksPerNode(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
value, err := swag.ConvertInt64(raw)
if err != nil {
return errors.InvalidType("disksPerNode", "path", "int64", raw)
}
o.DisksPerNode = value
if err := o.validateDisksPerNode(formats); err != nil {
return err
}
return nil
}
// validateDisksPerNode carries on validations for parameter DisksPerNode
func (o *GetParityParams) validateDisksPerNode(formats strfmt.Registry) error {
if err := validate.MinimumInt("disksPerNode", "path", int64(o.DisksPerNode), 1, false); err != nil {
return err
}
return nil
}
// bindNodes binds and validates parameter Nodes from path.
func (o *GetParityParams) bindNodes(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
value, err := swag.ConvertInt64(raw)
if err != nil {
return errors.InvalidType("nodes", "path", "int64", raw)
}
o.Nodes = value
if err := o.validateNodes(formats); err != nil {
return err
}
return nil
}
// validateNodes carries on validations for parameter Nodes
func (o *GetParityParams) validateNodes(formats strfmt.Registry) error {
if err := validate.MinimumInt("nodes", "path", int64(o.Nodes), 2, false); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,136 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// GetParityOKCode is the HTTP code returned for type GetParityOK
const GetParityOKCode int = 200
/*GetParityOK A successful response.
swagger:response getParityOK
*/
type GetParityOK struct {
/*
In: Body
*/
Payload models.ParityResponse `json:"body,omitempty"`
}
// NewGetParityOK creates GetParityOK with default headers values
func NewGetParityOK() *GetParityOK {
return &GetParityOK{}
}
// WithPayload adds the payload to the get parity o k response
func (o *GetParityOK) WithPayload(payload models.ParityResponse) *GetParityOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get parity o k response
func (o *GetParityOK) SetPayload(payload models.ParityResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetParityOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
payload := o.Payload
if payload == nil {
// return empty array
payload = models.ParityResponse{}
}
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
/*GetParityDefault Generic error response.
swagger:response getParityDefault
*/
type GetParityDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewGetParityDefault creates GetParityDefault with default headers values
func NewGetParityDefault(code int) *GetParityDefault {
if code <= 0 {
code = 500
}
return &GetParityDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the get parity default response
func (o *GetParityDefault) WithStatusCode(code int) *GetParityDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the get parity default response
func (o *GetParityDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the get parity default response
func (o *GetParityDefault) WithPayload(payload *models.Error) *GetParityDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get parity default response
func (o *GetParityDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetParityDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,126 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
"github.com/go-openapi/swag"
)
// GetParityURL generates an URL for the get parity operation
type GetParityURL struct {
DisksPerNode int64
Nodes int64
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetParityURL) WithBasePath(bp string) *GetParityURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetParityURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *GetParityURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/get-parity/{nodes}/{disksPerNode}"
disksPerNode := swag.FormatInt64(o.DisksPerNode)
if disksPerNode != "" {
_path = strings.Replace(_path, "{disksPerNode}", disksPerNode, -1)
} else {
return nil, errors.New("disksPerNode is required on GetParityURL")
}
nodes := swag.FormatInt64(o.Nodes)
if nodes != "" {
_path = strings.Replace(_path, "{nodes}", nodes, -1)
} else {
return nil, errors.New("nodes is required on GetParityURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *GetParityURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *GetParityURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *GetParityURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on GetParityURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on GetParityURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *GetParityURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -117,6 +117,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIGetMaxAllocatableMemHandler: admin_api.GetMaxAllocatableMemHandlerFunc(func(params admin_api.GetMaxAllocatableMemParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetMaxAllocatableMem has not yet been implemented")
}),
AdminAPIGetParityHandler: admin_api.GetParityHandlerFunc(func(params admin_api.GetParityParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetParity has not yet been implemented")
}),
AdminAPIGetResourceQuotaHandler: admin_api.GetResourceQuotaHandlerFunc(func(params admin_api.GetResourceQuotaParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetResourceQuota has not yet been implemented")
}),
@@ -319,6 +322,8 @@ type ConsoleAPI struct {
AdminAPIDeleteTenantHandler admin_api.DeleteTenantHandler
// AdminAPIGetMaxAllocatableMemHandler sets the operation handler for the get max allocatable mem operation
AdminAPIGetMaxAllocatableMemHandler admin_api.GetMaxAllocatableMemHandler
// AdminAPIGetParityHandler sets the operation handler for the get parity operation
AdminAPIGetParityHandler admin_api.GetParityHandler
// AdminAPIGetResourceQuotaHandler sets the operation handler for the get resource quota operation
AdminAPIGetResourceQuotaHandler admin_api.GetResourceQuotaHandler
// AdminAPIGetTenantUsageHandler sets the operation handler for the get tenant usage operation
@@ -524,6 +529,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIGetMaxAllocatableMemHandler == nil {
unregistered = append(unregistered, "admin_api.GetMaxAllocatableMemHandler")
}
if o.AdminAPIGetParityHandler == nil {
unregistered = append(unregistered, "admin_api.GetParityHandler")
}
if o.AdminAPIGetResourceQuotaHandler == nil {
unregistered = append(unregistered, "admin_api.GetResourceQuotaHandler")
}
@@ -816,6 +824,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/get-parity/{nodes}/{disksPerNode}"] = admin_api.NewGetParity(o.context, o.AdminAPIGetParityHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/namespaces/{namespace}/resourcequotas/{resource-quota-name}"] = admin_api.NewGetResourceQuota(o.context, o.AdminAPIGetResourceQuotaHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)

View File

@@ -1148,7 +1148,7 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
put:
put:
summary: Tenant Update Zones
operationId: TenantUpdateZones
parameters:
@@ -1282,7 +1282,7 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
/cluster/max-allocatable-memory:
get:
summary: Get maximum allocatable memory for given number of nodes
@@ -1306,6 +1306,33 @@ paths:
tags:
- AdminAPI
/get-parity/{nodes}/{disksPerNode}:
get:
summary: Gets parity by sending number of nodes & number of disks
operationId: GetParity
parameters:
- name: nodes
in: path
required: true
type: integer
minimum: 2
- name: disksPerNode
in: path
required: true
type: integer
minimum: 1
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/parityResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- AdminAPI
definitions:
bucketAccess:
type: string
@@ -1828,6 +1855,8 @@ definitions:
type: string
creation_date:
type: string
deletion_date:
type: string
currentState:
type: string
zones:
@@ -1836,11 +1865,15 @@ definitions:
$ref: "#/definitions/zone"
image:
type: string
console_image:
type: string
namespace:
type: string
total_size:
type: integer
format: int64
enable_prometheus:
type: boolean
tenantUsage:
type: object
@@ -1867,6 +1900,8 @@ definitions:
type: integer
creation_date:
type: string
deletion_date:
type: string
currentState:
type: string
namespace:
@@ -1898,7 +1933,9 @@ definitions:
$ref: "#/definitions/imageRegistry"
image_pull_secret:
type: string
enable_prometheus:
type: boolean
imageRegistry:
type: object
required:
@@ -1912,7 +1949,7 @@ definitions:
type: string
password:
type: string
createTenantRequest:
type: object
required:
@@ -1956,6 +1993,10 @@ definitions:
type: object
additionalProperties:
type: string
labels:
type: object
additionalProperties:
type: string
image_registry:
$ref: "#/definitions/imageRegistry"
image_pull_secret:
@@ -1969,6 +2010,25 @@ definitions:
encryption:
type: object
$ref: "#/definitions/encryptionConfiguration"
console:
type: object
$ref: '#/definitions/consoleConfiguration'
metadataFields:
type: object
properties:
annotations:
type: object
additionalProperties:
type: string
labels:
type: object
additionalProperties:
type: string
node_selector:
type: object
additionalProperties:
type: string
keyPairConfiguration:
type: object
@@ -2031,26 +2091,36 @@ definitions:
server_insecure:
type: boolean
consoleConfiguration:
allOf:
- $ref: '#/definitions/metadataFields'
- type: object
properties:
image:
type: string
encryptionConfiguration:
type: object
properties:
image:
type: string
server:
type: object
$ref: "#/definitions/keyPairConfiguration"
client:
type: object
$ref: "#/definitions/keyPairConfiguration"
gemalto:
type: object
$ref: "#/definitions/gemaltoConfiguration"
aws:
type: object
$ref: "#/definitions/awsConfiguration"
vault:
type: object
$ref: "#/definitions/vaultConfiguration"
allOf:
- $ref: '#/definitions/metadataFields'
- type: object
properties:
image:
type: string
server:
type: object
$ref: "#/definitions/keyPairConfiguration"
client:
type: object
$ref: "#/definitions/keyPairConfiguration"
gemalto:
type: object
$ref: "#/definitions/gemaltoConfiguration"
aws:
type: object
$ref: "#/definitions/awsConfiguration"
vault:
type: object
$ref: "#/definitions/vaultConfiguration"
vaultConfiguration:
type: object
@@ -2203,6 +2273,10 @@ definitions:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
resources:
$ref: "#/definitions/zoneResources"
node_selector:
@@ -2253,14 +2327,14 @@ definitions:
type: string
type: object
type: array
zoneTolerationSeconds:
description: TolerationSeconds represents the period of
time the toleration (which must be of effect NoExecute,
otherwise this field is ignored) tolerates the taint.
By default, it is not set, which means tolerate the taint
forever (do not evict). Zero and negative values will
be treated as 0 (evict immediately) by the system.
time the toleration (which must be of effect NoExecute,
otherwise this field is ignored) tolerates the taint.
By default, it is not set, which means tolerate the taint
forever (do not evict). Zero and negative values will
be treated as 0 (evict immediately) by the system.
type: object
required:
- seconds
@@ -2636,13 +2710,13 @@ definitions:
used:
type: integer
format: int64
deleteTenantRequest:
type: object
properties:
delete_pvcs:
type: boolean
zoneUpdateRequest:
type: object
required:
@@ -2652,10 +2726,15 @@ definitions:
type: array
items:
$ref: "#/definitions/zone"
maxAllocatableMemResponse:
type: object
properties:
max_memory:
type: integer
format: int64
parityResponse:
type: array
items:
type: string