From cf907752bd5c7e3bd89d179ccbcf749ffbd3dcd6 Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Mon, 2 May 2022 15:22:29 -0700 Subject: [PATCH] Parse .spec.features.domains and pass as environment variables (#1106) Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- go.mod | 1 + go.sum | 12 ++ main.go | 3 +- pkg/apis/minio.min.io/v2/helper.go | 115 ++++++++++ pkg/apis/minio.min.io/v2/helper_test.go | 202 ++++++++++++++++++ pkg/apis/minio.min.io/v2/types.go | 2 + .../statefulsets/minio-statefulset.go | 47 ++-- 7 files changed, 357 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index e3cd9951e0e..1d1d4a0d944 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-containerregistry v0.1.2 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.3.0 + github.com/miekg/dns v1.1.48 github.com/minio/madmin-go v1.3.11 github.com/minio/minio-go/v7 v7.0.23 github.com/minio/pkg v1.1.21 diff --git a/go.sum b/go.sum index 50a3c9e8e47..f59d1a21aaf 100644 --- a/go.sum +++ b/go.sum @@ -604,6 +604,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.48 h1:Ucfr7IIVyMBz4lRE8qmGUuZ4Wt3/ZGu9hmcMT3Uu4tQ= +github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/madmin-go v1.3.11 h1:Cj02kzG2SD1pnZW2n1joe00yqb6NFE40Jt2gp+5mWFQ= github.com/minio/madmin-go v1.3.11/go.mod h1:ez87VmMtsxP7DRxjKJKD4RDNW+nhO2QF9KSzwxBDQ98= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= @@ -840,6 +842,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -918,6 +921,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -967,6 +971,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1046,6 +1052,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827 h1:A0Qkn7Z/n8zC1xd9LTw17AiKlBRK64tw3ejWQiEqca0= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1058,6 +1068,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1145,6 +1156,7 @@ golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4X golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index ab1c3198f61..e17aab3a065 100644 --- a/main.go +++ b/main.go @@ -24,12 +24,11 @@ import ( "syscall" "time" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/minio/minio-go/v7/pkg/set" - "k8s.io/client-go/rest" - miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/apis/minio.min.io/v2/helper.go b/pkg/apis/minio.min.io/v2/helper.go index add50c5da4b..e7dfeeb5528 100644 --- a/pkg/apis/minio.min.io/v2/helper.go +++ b/pkg/apis/minio.min.io/v2/helper.go @@ -31,12 +31,15 @@ import ( "os" "path" "reflect" + "sort" "strconv" "strings" "sync" "text/template" "time" + "github.com/miekg/dns" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -869,6 +872,10 @@ func (t *Tenant) Validate() error { return err } } + // make sure all the domains are valid + if err := t.ValidateDomains(); err != nil { + return err + } return nil } @@ -1083,3 +1090,111 @@ func (t *Tenant) HasMinIODomains() bool { func (t *Tenant) HasConsoleDomains() bool { return t.Spec.Features != nil && t.Spec.Features.Domains != nil && t.Spec.Features.Domains.Console != "" } + +// ValidateDomains checks the validity of the domains configured on the tenant +func (t *Tenant) ValidateDomains() error { + if t.HasMinIODomains() { + domains := t.Spec.Features.Domains.Minio + if len(domains) != 0 { + for _, domainName := range domains { + _, err := url.Parse(domainName) + if err != nil { + return err + } + + if _, ok := dns.IsDomainName(domainName); !ok { + return fmt.Errorf("invalid domain `%s`", domainName) + } + } + sort.Strings(domains) + lcpSuf := lcpSuffix(domains) + for _, domainName := range domains { + if domainName == lcpSuf && len(domains) > 1 { + return fmt.Errorf("overlapping domains `%s` not allowed", domainName) + } + } + } + } + return nil +} + +// GetDomainHosts returns a list of hosts in the .spec.features.domains.minio list to configure MINIO_DOMAIN +func (t *Tenant) GetDomainHosts() []string { + if t.HasMinIODomains() { + domains := t.Spec.Features.Domains.Minio + var hosts []string + for _, d := range domains { + u, err := url.Parse(d) + if err != nil { + continue + } + // remove ports if any + hostParts := strings.Split(u.Host, ":") + hosts = append(hosts, hostParts[0]) + } + return hosts + } + return nil +} + +// HasEnv returns whether an environment variable is defined in the .spec.env field +func (t *Tenant) HasEnv(envName string) bool { + for _, env := range t.Spec.Env { + if env.Name == envName { + return true + } + } + return false +} + +// Suffix returns the longest common suffix of the provided strings +func lcpSuffix(strs []string) string { + return lcp(strs, false) +} + +func lcp(strs []string, pre bool) string { + // short-circuit empty list + if len(strs) == 0 { + return "" + } + xfix := strs[0] + // short-circuit single-element list + if len(strs) == 1 { + return xfix + } + // compare first to rest + for _, str := range strs[1:] { + xfixl := len(xfix) + strl := len(str) + // short-circuit empty strings + if xfixl == 0 || strl == 0 { + return "" + } + // maximum possible length + maxl := xfixl + if strl < maxl { + maxl = strl + } + // compare letters + if pre { + // prefix, iterate left to right + for i := 0; i < maxl; i++ { + if xfix[i] != str[i] { + xfix = xfix[:i] + break + } + } + } else { + // suffix, iterate right to left + for i := 0; i < maxl; i++ { + xi := xfixl - i - 1 + si := strl - i - 1 + if xfix[xi] != str[si] { + xfix = xfix[xi+1:] + break + } + } + } + } + return xfix +} diff --git a/pkg/apis/minio.min.io/v2/helper_test.go b/pkg/apis/minio.min.io/v2/helper_test.go index 92f3a7f2811..ab7cf5812ed 100644 --- a/pkg/apis/minio.min.io/v2/helper_test.go +++ b/pkg/apis/minio.min.io/v2/helper_test.go @@ -317,3 +317,205 @@ export MINIO_SERVER_URL=http://localhost:9000 }) } } + +func TestTenant_GetDomainHosts(t1 *testing.T) { + type fields struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + Scheduler TenantScheduler + Spec TenantSpec + Status TenantStatus + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "List of domains to host list", + fields: fields{ + Spec: TenantSpec{ + Features: &Features{ + Domains: &TenantDomains{ + Minio: []string{ + "https://domain1.com:8080", + "http://domain2.com", + }, + }, + }, + }, + }, + want: []string{ + "domain1.com", + "domain2.com", + }, + }, + { + name: "Empty hosts", + fields: fields{ + Spec: TenantSpec{ + Features: &Features{}, + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t := &Tenant{ + TypeMeta: tt.fields.TypeMeta, + ObjectMeta: tt.fields.ObjectMeta, + Scheduler: tt.fields.Scheduler, + Spec: tt.fields.Spec, + Status: tt.fields.Status, + } + assert.Equalf(t1, tt.want, t.GetDomainHosts(), "GetDomainHosts()") + }) + } +} + +func TestTenant_HasEnv(t1 *testing.T) { + type fields struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + Scheduler TenantScheduler + Spec TenantSpec + Status TenantStatus + } + type args struct { + envName string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "Contains env", + fields: fields{ + Spec: TenantSpec{ + Env: []corev1.EnvVar{ + { + Name: "ENV1", + Value: "whatever", + }, + }, + }, + }, + args: args{ + envName: "ENV1", + }, + want: true, + }, + { + name: "Does not Contains env", + fields: fields{ + Spec: TenantSpec{ + Env: []corev1.EnvVar{ + { + Name: "ENV1", + Value: "whatever", + }, + }, + }, + }, + args: args{ + envName: "ENV2", + }, + want: false, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t := &Tenant{ + TypeMeta: tt.fields.TypeMeta, + ObjectMeta: tt.fields.ObjectMeta, + Scheduler: tt.fields.Scheduler, + Spec: tt.fields.Spec, + Status: tt.fields.Status, + } + assert.Equalf(t1, tt.want, t.HasEnv(tt.args.envName), "HasEnv(%v)", tt.args.envName) + }) + } +} + +func TestTenant_ValidateDomains(t1 *testing.T) { + type fields struct { + TypeMeta metav1.TypeMeta + ObjectMeta metav1.ObjectMeta + Scheduler TenantScheduler + Spec TenantSpec + Status TenantStatus + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Valid Domains", + fields: fields{ + Spec: TenantSpec{ + Features: &Features{ + Domains: &TenantDomains{ + Minio: []string{ + "https://domain1.com:8080", + "http://domain2.com", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Invalid Domains", + fields: fields{ + Spec: TenantSpec{ + Features: &Features{ + Domains: &TenantDomains{ + Minio: []string{ + "http s://domain1.com:8080", + "http://domain2.com", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "Duplicate Domains", + fields: fields{ + Spec: TenantSpec{ + Features: &Features{ + Domains: &TenantDomains{ + Minio: []string{ + "http://domain2.com", + "http://domain2.com", + }, + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t := &Tenant{ + TypeMeta: tt.fields.TypeMeta, + ObjectMeta: tt.fields.ObjectMeta, + Scheduler: tt.fields.Scheduler, + Spec: tt.fields.Spec, + Status: tt.fields.Status, + } + if tt.wantErr { + if err := t.ValidateDomains(); err == nil { + assert.Failf(t1, "Test %s did not return error", tt.name) + } + } + }) + } +} diff --git a/pkg/apis/minio.min.io/v2/types.go b/pkg/apis/minio.min.io/v2/types.go index c48b90b9e32..dbb10be5898 100644 --- a/pkg/apis/minio.min.io/v2/types.go +++ b/pkg/apis/minio.min.io/v2/types.go @@ -62,11 +62,13 @@ type Bucket struct { // TenantDomains (`domains`) - List of domains used to access the tenant from outside the kubernetes clusters. // this will only configure MinIO for the domains listed, but external DNS configuration is still needed. +// The listed domains should include schema and port if any is used, i.e. https://minio.domain.com:8123 type TenantDomains struct { // List of Domains used by MinIO. This will enable DNS style access to the object store where the bucket name is // inferred from a subdomain in the domain. Minio []string `json:"minio,omitempty"` // Domain used to expose the MinIO Console, this will configure the redirect on MinIO when visiting from the browser + // If Console is exposed via a subpath, the domain should include it, i.e. https://console.domain.com:8123/subpath/ Console string `json:"console,omitempty"` } diff --git a/pkg/resources/statefulsets/minio-statefulset.go b/pkg/resources/statefulsets/minio-statefulset.go index 6a383d53dcb..d52b54eef82 100644 --- a/pkg/resources/statefulsets/minio-statefulset.go +++ b/pkg/resources/statefulsets/minio-statefulset.go @@ -27,26 +27,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -func envVarsContains(vars []corev1.EnvVar, envName string) bool { - for _, v := range vars { - if v.Name == envName { - return true - } - } - return false -} - // Adds required Console environment variables func consoleEnvVars(t *miniov2.Tenant) []corev1.EnvVar { var envVars []corev1.EnvVar - if !envVarsContains(t.Spec.Env, "MINIO_SERVER_URL") { - envVars = append(envVars, corev1.EnvVar{ - Name: "MINIO_SERVER_URL", - Value: t.MinIOServerEndpoint(), - }) - } - if t.HasLogEnabled() { envVars = append(envVars, corev1.EnvVar{ Name: miniov2.LogQueryTokenKey, @@ -134,24 +118,41 @@ func minioEnvironmentVars(t *miniov2.Tenant, wsSecret *v1.Secret, hostsTemplate } // Check if any domains are configured if t.HasMinIODomains() { - domains = append(domains, t.Spec.Features.Domains.Minio...) + domains = append(domains, t.GetDomainHosts()...) } - // tell MinIO about all the domains meant to hit it - if len(domains) > 0 { + // tell MinIO about all the domains meant to hit it if they are not passed manually via .spec.env + if !t.HasEnv("MINIO_DOMAIN") && len(domains) > 0 { envVars = append(envVars, corev1.EnvVar{ Name: "MINIO_DOMAIN", Value: strings.Join(domains, ","), }) } + // If no specific server URL is specified we will specify the internal k8s url, but if a list of domains was + // provided we will use the first domain. + if !t.HasEnv("MINIO_SERVER_URL") { + serverURL := t.MinIOServerEndpoint() + if t.HasMinIODomains() { + serverURL = t.Spec.Features.Domains.Minio[0] + } + envVars = append(envVars, corev1.EnvVar{ + Name: "MINIO_SERVER_URL", + Value: serverURL, + }) + } + // Set the redirect url for console if t.HasConsoleDomains() { - schema := "http" - if t.TLS() { - schema = "https" + consoleDomain := t.Spec.Features.Domains.Console + if !strings.HasPrefix(consoleDomain, "http") { + useSchema := "http" + if t.TLS() { + useSchema = "https" + } + consoleDomain = fmt.Sprintf("%s://%s", useSchema, t.Spec.Features.Domains.Console) } envVars = append(envVars, corev1.EnvVar{ Name: "MINIO_BROWSER_REDIRECT_URL", - Value: fmt.Sprintf("%s://%s", schema, t.Spec.Features.Domains.Console), + Value: consoleDomain, }) }