diff --git a/.goreleaser.yml b/.goreleaser.yml index d067326ad5d..c835fb25fe5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -28,6 +28,7 @@ builds: - s390x env: - CGO_ENABLED=0 + main: ./cmd/operator/ ldflags: - -s -w -X main.version={{.Tag}} flags: diff --git a/Dockerfile b/Dockerfile index 52b4b5cb5f5..cfc74b2e27d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ RUN \ COPY minio-operator /minio-operator COPY logsearchapi-bin /logsearchapi -CMD ["/minio-operator"] +ENTRYPOINT ["/minio-operator"] diff --git a/Makefile b/Makefile index 9e4f89cd761..f0bb72ace58 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ getdeps: verify: getdeps govet gotest lint operator: verify - @CGO_ENABLED=0 GOOS=linux go build -trimpath --ldflags $(LDFLAGS) -o minio-operator + @CGO_ENABLED=0 GOOS=linux go build -trimpath --ldflags $(LDFLAGS) -o minio-operator ./cmd/operator docker: operator logsearchapi @docker build --no-cache -t $(TAG) . diff --git a/cmd/operator/app_commands.go b/cmd/operator/app_commands.go new file mode 100644 index 00000000000..d5abf3ed9f0 --- /dev/null +++ b/cmd/operator/app_commands.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/minio/cli" +) + +var appCmds = []cli.Command{ + controllerCmd, + sidecarCmd, + validateCmd, +} diff --git a/cmd/operator/controller.go b/cmd/operator/controller.go new file mode 100644 index 00000000000..09befab3369 --- /dev/null +++ b/cmd/operator/controller.go @@ -0,0 +1,32 @@ +// Copyright (C) 2023, MinIO, Inc. +// +// This code is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License, version 3, +// as published by the Free Software Foundation. +// +// 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, version 3, +// along with this program. If not, see + +package main + +import ( + "github.com/minio/cli" + "github.com/minio/operator/pkg/controller" +) + +// starts the controller +var controllerCmd = cli.Command{ + Name: "controller", + Aliases: []string{"ctl"}, + Usage: "Start MinIO Operator Controller", + Action: startController, +} + +func startController(ctx *cli.Context) { + controller.StartOperator() +} diff --git a/cmd/operator/main.go b/cmd/operator/main.go new file mode 100644 index 00000000000..30ed36b089e --- /dev/null +++ b/cmd/operator/main.go @@ -0,0 +1,130 @@ +// Copyright (C) 2023, MinIO, Inc. +// +// This code is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License, version 3, +// as published by the Free Software Foundation. +// +// 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, version 3, +// along with this program. If not, see + +package main + +import ( + "os" + "path/filepath" + "sort" + "time" + + "github.com/minio/operator/pkg" + + "github.com/minio/cli" + "github.com/minio/pkg/console" + "github.com/minio/pkg/trie" + "github.com/minio/pkg/words" +) + +// Help template for Operator. +var operatorHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +DESCRIPTION: + {{.Description}} + +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}}{{end}} [ARGS...] + +COMMANDS: + {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{if .VisibleFlags}} +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +VERSION: + {{.Version}} +` + +func newApp(name string) *cli.App { + // Collection of console commands currently supported are. + var commands []cli.Command + + // Collection of console commands currently supported in a trie tree. + commandsTree := trie.NewTrie() + + // registerCommand registers a cli command. + registerCommand := func(command cli.Command) { + commands = append(commands, command) + commandsTree.Insert(command.Name) + } + + // register commands + for _, cmd := range appCmds { + registerCommand(cmd) + } + + findClosestCommands := func(command string) []string { + var closestCommands []string + closestCommands = append(closestCommands, commandsTree.PrefixMatch(command)...) + + sort.Strings(closestCommands) + // Suggest other close commands - allow missed, wrongly added and + // even transposed characters + for _, value := range commandsTree.Walk(commandsTree.Root()) { + if sort.SearchStrings(closestCommands, value) < len(closestCommands) { + continue + } + // 2 is arbitrary and represents the max + // allowed number of typed errors + if words.DamerauLevenshteinDistance(command, value) < 2 { + closestCommands = append(closestCommands, value) + } + } + + return closestCommands + } + + cli.HelpFlag = cli.BoolFlag{ + Name: "help, h", + Usage: "show help", + } + + app := cli.NewApp() + app.Name = name + app.Version = pkg.Version + " - " + pkg.ShortCommitID + app.Author = "MinIO, Inc." + app.Usage = "MinIO Operator" + app.Description = `MinIO Operator automates the orchestration of MinIO Tenants on Kubernetes.` + app.Copyright = "(c) 2023 MinIO, Inc." + app.Compiled, _ = time.Parse(time.RFC3339, pkg.ReleaseTime) + app.Commands = commands + app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`. + app.CustomAppHelpTemplate = operatorHelpTemplate + app.CommandNotFound = func(ctx *cli.Context, command string) { + console.Printf("‘%s’ is not a console sub-command. See ‘console --help’.\n", command) + closestCommands := findClosestCommands(command) + if len(closestCommands) > 0 { + console.Println() + console.Println("Did you mean one of these?") + for _, cmd := range closestCommands { + console.Printf("\t‘%s’\n", cmd) + } + } + os.Exit(1) + } + + return app +} + +func main() { + args := os.Args + // Set the orchestrator app name. + appName := filepath.Base(args[0]) + // Run the app - exit on error. + if err := newApp(appName).Run(args); err != nil { + os.Exit(1) + } +} diff --git a/cmd/operator/sidecar.go b/cmd/operator/sidecar.go new file mode 100644 index 00000000000..128c7e7a3ac --- /dev/null +++ b/cmd/operator/sidecar.go @@ -0,0 +1,57 @@ +// Copyright (C) 2023, MinIO, Inc. +// +// This code is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License, version 3, +// as published by the Free Software Foundation. +// +// 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, version 3, +// along with this program. If not, see + +package main + +import ( + "log" + "os" + + "github.com/minio/cli" + "github.com/minio/operator/pkg/sidecar" +) + +// starts the controller +var sidecarCmd = cli.Command{ + Name: "sidecar", + Aliases: []string{"s"}, + Usage: "Start MinIO Operator Sidecar", + Action: startSideCar, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "tenant", + Value: "", + Usage: "name of tenant being validated", + }, + cli.StringFlag{ + Name: "config-name", + Value: "", + Usage: "secret being watched", + }, + }, +} + +func startSideCar(ctx *cli.Context) { + tenantName := ctx.String("tenant") + if tenantName == "" { + log.Println("Must pass --tenant flag") + os.Exit(1) + } + secretName := ctx.String("config-name") + if tenantName == "" { + log.Println("Must pass --config-name flag") + os.Exit(1) + } + sidecar.StartSideCar(tenantName, secretName) +} diff --git a/cmd/operator/validate.go b/cmd/operator/validate.go new file mode 100644 index 00000000000..8c6395990ca --- /dev/null +++ b/cmd/operator/validate.go @@ -0,0 +1,42 @@ +// This file is part of MinIO Operator +// Copyright (c) 2023 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 . + +package main + +import ( + "github.com/minio/cli" + "github.com/minio/operator/pkg/validator" +) + +// starts the controller +var validateCmd = cli.Command{ + Name: "validate", + Aliases: []string{"v"}, + Usage: "Start MinIO Operator Config Validator", + Action: startValidator, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "tenant", + Value: "", + Usage: "name of tenant being validated", + }, + }, +} + +func startValidator(ctx *cli.Context) { + tenantName := ctx.String("tenant") + validator.Validate(tenantName) +} diff --git a/go.mod b/go.mod index 5a3f40f94b3..8b2851212f4 100644 --- a/go.mod +++ b/go.mod @@ -22,13 +22,11 @@ require ( golang.org/x/time v0.3.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.25.4 - k8s.io/apiextensions-apiserver v0.25.4 k8s.io/apimachinery v0.25.4 k8s.io/client-go v0.25.4 k8s.io/code-generator v0.25.4 k8s.io/klog/v2 v2.80.1 k8s.io/kubectl v0.25.4 - sigs.k8s.io/controller-runtime v0.13.1 ) require ( @@ -169,9 +167,11 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.25.4 // indirect k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 // indirect k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect + sigs.k8s.io/controller-runtime v0.13.1 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/pkg/apis/minio.min.io/v2/constants.go b/pkg/apis/minio.min.io/v2/constants.go index b84ccb54d07..360852b720a 100644 --- a/pkg/apis/minio.min.io/v2/constants.go +++ b/pkg/apis/minio.min.io/v2/constants.go @@ -48,6 +48,9 @@ const MinIOCertPath = "/tmp/certs" // TmpPath /tmp path inside the container file system const TmpPath = "/tmp" +// CfgPath is the location of the MinIO Configuration File +const CfgPath = "/etc/minio/config.env" + // TenantLabel is applied to all components of a Tenant cluster const TenantLabel = "v1.min.io/tenant" diff --git a/pkg/apis/minio.min.io/v2/helper.go b/pkg/apis/minio.min.io/v2/helper.go index 520c8f0cb5c..51e55f12774 100644 --- a/pkg/apis/minio.min.io/v2/helper.go +++ b/pkg/apis/minio.min.io/v2/helper.go @@ -411,6 +411,10 @@ func (t *Tenant) EnsureDefaults() *Tenant { } } } + // ServiceAccount + if t.Spec.ServiceAccountName == "" { + t.Spec.ServiceAccountName = fmt.Sprintf("%s-sa", t.Name) + } return t } @@ -1338,3 +1342,13 @@ func GetPgImage() string { }) return pgDefaultImage } + +// GetRoleName returns the role name we will use for the tenant +func (t *Tenant) GetRoleName() string { + return fmt.Sprintf("%s-role", t.Name) +} + +// GetBindingName returns the binding name we will use for the tenant +func (t *Tenant) GetBindingName() string { + return fmt.Sprintf("%s-binding", t.Name) +} diff --git a/pkg/build-constants.go b/pkg/build-constants.go new file mode 100644 index 00000000000..65ab34306e6 --- /dev/null +++ b/pkg/build-constants.go @@ -0,0 +1,30 @@ +// This file is part of MinIO Operator +// Copyright (c) 2023 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 . + +package pkg + +var ( + // Version - the version being released (v prefix stripped) + Version = "(dev)" + // ReleaseTag - the current git tag + ReleaseTag = "(no tag)" + // ReleaseTime - current UTC date in RFC3339 format. + ReleaseTime = "(no release)" + // CommitID - latest commit id. + CommitID = "(dev)" + // ShortCommitID - first 12 characters from CommitID. + ShortCommitID = "(dev)" +) diff --git a/pkg/controller/cluster/http_handlers.go b/pkg/controller/cluster/http_handlers.go index f66f894a781..c18f7bf53c6 100644 --- a/pkg/controller/cluster/http_handlers.go +++ b/pkg/controller/cluster/http_handlers.go @@ -17,14 +17,11 @@ package cluster import ( - "context" "fmt" "net/http" "strconv" "strings" - "github.com/minio/operator/pkg/resources/statefulsets" - "github.com/gorilla/mux" miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" "github.com/minio/operator/pkg/resources/services" @@ -119,78 +116,3 @@ func validateBucketName(bucket string) (bool, error) { } return true, nil } - -// GetenvHandler - GET /webhook/v1/getenv/{namespace}/{name}?key={env} -func (c *Controller) GetenvHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - namespace := vars["namespace"] - name := vars["name"] - key := vars["key"] - - secret, err := c.kubeClientSet.CoreV1().Secrets(namespace).Get(r.Context(), - miniov2.WebhookSecret, metav1.GetOptions{}) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - if err = c.validateRequest(r, secret); err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - // Get the Tenant resource with this namespace/name - tenant, err := c.minioClientSet.MinioV2().Tenants(namespace).Get(context.Background(), name, metav1.GetOptions{}) - if err != nil { - if k8serrors.IsNotFound(err) { - // The Tenant resource may no longer exist, in which case we stop processing. - http.Error(w, fmt.Sprintf("Tenant '%s' in work queue no longer exists", key), http.StatusNotFound) - return - } - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - tenant.EnsureDefaults() - - // Validate the MinIO Tenant - if err = tenant.Validate(); err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - // correct all statefulset names by loading them, this will fix their name on the tenant pool names - _, err = c.getAllSSForTenant(tenant) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - switch key { - case envMinIOArgs: - args := strings.Join(statefulsets.GetContainerArgs(tenant, c.hostsTemplate), " ") - klog.Infof("%s value is %s", key, args) - - _, _ = w.Write([]byte(args)) - w.(http.Flusher).Flush() - case envMinIOServiceTarget: - schema := "https" - if !isOperatorTLS() { - schema = "http" - } - target := fmt.Sprintf("%s://%s:%s%s/%s/%s", - schema, - fmt.Sprintf("operator.%s.svc.%s", - miniov2.GetNSFromFile(), - miniov2.GetClusterDomain()), - miniov2.WebhookDefaultPort, - miniov2.WebhookAPIBucketService, - tenant.Namespace, - tenant.Name) - klog.Infof("%s value is %s", key, target) - - _, _ = w.Write([]byte(target)) - default: - http.Error(w, fmt.Sprintf("%s env key is not supported yet", key), http.StatusBadRequest) - return - } -} diff --git a/pkg/controller/cluster/main-controller.go b/pkg/controller/cluster/main-controller.go index 65206c1ba97..1222aca7098 100644 --- a/pkg/controller/cluster/main-controller.go +++ b/pkg/controller/cluster/main-controller.go @@ -199,6 +199,8 @@ type Controller struct { // time, and makes it easy to ensure we are never processing the same item // simultaneously in two different workers. healthCheckQueue queue.RateLimitingInterface + // image being used in the operator deployment + operatorImage string } // NewController returns a new sample controller @@ -213,6 +215,19 @@ func NewController(podName string, namespacesToWatch set.StringSet, kubeClientSe eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClientSet.CoreV1().Events("")}) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) + // get operator deployment name + ns := miniov2.GetNSFromFile() + ctx := context.Background() + oprImg := DefaultOperatorImage + oprDep, err := kubeClientSet.AppsV1().Deployments(ns).Get(ctx, DefaultDeploymentName, metav1.GetOptions{}) + if err == nil && oprDep != nil { + for _, c := range oprDep.Spec.Template.Spec.Containers { + if c.Name == "minio-operator" { + oprImg = c.Image + } + } + } + controller := &Controller{ podName: podName, namespacesToWatch: namespacesToWatch, @@ -232,6 +247,7 @@ func NewController(podName string, namespacesToWatch set.StringSet, kubeClientSe recorder: recorder, hostsTemplate: hostsTemplate, operatorVersion: operatorVersion, + operatorImage: oprImg, } // Initialize operator webhook handlers @@ -670,6 +686,13 @@ func (c *Controller) syncHandler(key string) error { // get combined configurations (tenant.env, tenant.credsSecret and tenant.Configuration) for tenant tenantConfiguration, err := c.getTenantCredentials(ctx, tenant) if err != nil { + if errors.Is(err, ErrEmptyRootCredentials) { + if _, err2 := c.updateTenantStatus(ctx, tenant, err.Error(), 0); err2 != nil { + klog.V(2).Infof(err2.Error()) + } + c.RegisterEvent(ctx, tenant, corev1.EventTypeWarning, "MissingCreds", "Tenant is missing root credentials") + return nil + } return err } // get existing configuration from config.env @@ -794,6 +817,13 @@ func (c *Controller) syncHandler(key string) error { } } } + // Create Tenant Services Accoutns for Tenant + err = c.checkAndCreateServiceAccount(ctx, tenant) + if err != nil { + c.RegisterEvent(ctx, tenant, corev1.EventTypeWarning, "SAFailed", "Service Account creation failed") + return err + } + c.RegisterEvent(ctx, tenant, corev1.EventTypeNormal, "SACreated", "Service Account Created") adminClnt, err := tenant.NewMinIOAdmin(tenantConfiguration, c.getTransport()) if err != nil { @@ -898,7 +928,19 @@ func (c *Controller) syncHandler(key string) error { if tenant, err = c.updateTenantStatus(ctx, tenant, StatusProvisioningStatefulSet, 0); err != nil { return err } - ss = statefulsets.NewPool(tenant, secret, skipEnvVars, &pool, &tenant.Status.Pools[i], hlSvc.Name, c.hostsTemplate, c.operatorVersion, isOperatorTLS(), operatorCATLSExists) + ss = statefulsets.NewPool(&statefulsets.NewPoolArgs{ + Tenant: tenant, + WsSecret: secret, + SkipEnvVars: skipEnvVars, + Pool: &pool, + PoolStatus: &tenant.Status.Pools[i], + ServiceName: hlSvc.Name, + HostsTemplate: c.hostsTemplate, + OperatorVersion: c.operatorVersion, + OperatorTLS: isOperatorTLS(), + OperatorCATLS: operatorCATLSExists, + OperatorImage: c.operatorImage, + }) ss, err = c.kubeClientSet.AppsV1().StatefulSets(tenant.Namespace).Create(ctx, ss, cOpts) if err != nil { return err @@ -1113,7 +1155,19 @@ func (c *Controller) syncHandler(key string) error { for i, pool := range tenant.Spec.Pools { // Now proceed to make the yaml changes for the tenant statefulset. - ss := statefulsets.NewPool(tenant, secret, skipEnvVars, &pool, &tenant.Status.Pools[i], hlSvc.Name, c.hostsTemplate, c.operatorVersion, isOperatorTLS(), operatorCATLSExists) + ss := statefulsets.NewPool(&statefulsets.NewPoolArgs{ + Tenant: tenant, + WsSecret: secret, + SkipEnvVars: skipEnvVars, + Pool: &pool, + PoolStatus: &tenant.Status.Pools[i], + ServiceName: hlSvc.Name, + HostsTemplate: c.hostsTemplate, + OperatorVersion: c.operatorVersion, + OperatorTLS: isOperatorTLS(), + OperatorCATLS: operatorCATLSExists, + OperatorImage: c.operatorImage, + }) if _, err = c.kubeClientSet.AppsV1().StatefulSets(tenant.Namespace).Update(ctx, ss, uOpts); err != nil { return err } @@ -1153,7 +1207,19 @@ func (c *Controller) syncHandler(key string) error { } } // generated the expected StatefulSet based on the new tenant configuration - expectedStatefulSet := statefulsets.NewPool(tenant, secret, skipEnvVars, &pool, &tenant.Status.Pools[i], hlSvc.Name, c.hostsTemplate, c.operatorVersion, isOperatorTLS(), operatorCATLSExists) + expectedStatefulSet := statefulsets.NewPool(&statefulsets.NewPoolArgs{ + Tenant: tenant, + WsSecret: secret, + SkipEnvVars: skipEnvVars, + Pool: &pool, + PoolStatus: &tenant.Status.Pools[i], + ServiceName: hlSvc.Name, + HostsTemplate: c.hostsTemplate, + OperatorVersion: c.operatorVersion, + OperatorTLS: isOperatorTLS(), + OperatorCATLS: operatorCATLSExists, + OperatorImage: c.operatorImage, + }) // Verify if this pool matches the spec on the tenant (resources, affinity, sidecars, etc) poolMatchesSS, err := poolSSMatchesSpec(expectedStatefulSet, existingStatefulSet) if err != nil { diff --git a/pkg/controller/cluster/operator.go b/pkg/controller/cluster/operator.go index 47ca36283a3..5ea611a49cd 100644 --- a/pkg/controller/cluster/operator.go +++ b/pkg/controller/cluster/operator.go @@ -51,6 +51,8 @@ const ( OperatorTLSSecretName = "operator-tls" // DefaultDeploymentName is the default name of the operator deployment DefaultDeploymentName = "minio-operator" + // DefaultOperatorImage is the version fo the operator being used + DefaultOperatorImage = "minio/operator:v4.5.8" ) var serverCertsManager *xcerts.Manager diff --git a/pkg/controller/cluster/service-account.go b/pkg/controller/cluster/service-account.go new file mode 100644 index 00000000000..ab9bb0ea11b --- /dev/null +++ b/pkg/controller/cluster/service-account.go @@ -0,0 +1,133 @@ +// This file is part of MinIO Operator +// Copyright (c) 2023 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 . + +package cluster + +import ( + "context" + + miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (c *Controller) checkAndCreateServiceAccount(ctx context.Context, tenant *miniov2.Tenant) error { + // check if service account exits + sa, err := c.kubeClientSet.CoreV1().ServiceAccounts(tenant.Namespace).Get(ctx, tenant.Spec.ServiceAccountName, v1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + // create SA + sa, err = c.kubeClientSet.CoreV1().ServiceAccounts(tenant.Namespace).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: v1.ObjectMeta{ + Name: tenant.Spec.ServiceAccountName, + Namespace: tenant.Namespace, + }, + }, v1.CreateOptions{}) + if err != nil { + return err + } + } else { + return err + } + } + // check if role exist + role, err := c.kubeClientSet.RbacV1().Roles(tenant.Namespace).Get(ctx, tenant.GetRoleName(), v1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + role = getTenantRole(tenant) + role, err = c.kubeClientSet.RbacV1().Roles(tenant.Namespace).Create(ctx, role, v1.CreateOptions{}) + if err != nil { + return err + } + } else { + return err + } + } + // check rolebinding + _, err = c.kubeClientSet.RbacV1().RoleBindings(tenant.Namespace).Get(ctx, tenant.GetBindingName(), v1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + _, err = c.kubeClientSet.RbacV1().RoleBindings(tenant.Namespace).Create(ctx, getRoleBinding(tenant, sa, role), v1.CreateOptions{}) + if err != nil { + return err + } + } else { + return err + } + } + return nil +} + +func getRoleBinding(tenant *miniov2.Tenant, sa *corev1.ServiceAccount, role *rbacv1.Role) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: v1.ObjectMeta{ + Name: tenant.GetBindingName(), + Namespace: tenant.Namespace, + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: role.Name, + }, + } +} + +func getTenantRole(tenant *miniov2.Tenant) *rbacv1.Role { + role := rbacv1.Role{ + ObjectMeta: v1.ObjectMeta{ + Name: tenant.GetRoleName(), + Namespace: tenant.Namespace, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{ + "", + }, + Resources: []string{ + "secrets", + }, + Verbs: []string{ + "get", + "list", + "watch", + }, + }, + { + APIGroups: []string{ + "minio.min.io", + }, + Resources: []string{ + "tenants", + }, + Verbs: []string{ + "get", + "list", + "watch", + }, + }, + }, + } + return &role +} diff --git a/pkg/controller/cluster/tenants.go b/pkg/controller/cluster/tenants.go index 85e7ad0e8ad..920931a59d6 100644 --- a/pkg/controller/cluster/tenants.go +++ b/pkg/controller/cluster/tenants.go @@ -25,6 +25,9 @@ import ( miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" ) +// ErrEmptyRootCredentials is the error returned when we detect missing root credentials +var ErrEmptyRootCredentials = errors.New("empty tenant credentials") + func (c *Controller) getTenantConfiguration(ctx context.Context, tenant *miniov2.Tenant) (map[string][]byte, error) { tenantConfiguration := map[string][]byte{} // Load tenant configuration from file @@ -74,7 +77,7 @@ func (c *Controller) getTenantCredentials(ctx context.Context, tenant *miniov2.T } if accessKey == "" || secretKey == "" { - return tenantConfiguration, errors.New("empty tenant credentials") + return tenantConfiguration, ErrEmptyRootCredentials } return tenantConfiguration, nil diff --git a/pkg/controller/cluster/webhook.go b/pkg/controller/cluster/webhook.go index baa1e864826..6977c4ce708 100644 --- a/pkg/controller/cluster/webhook.go +++ b/pkg/controller/cluster/webhook.go @@ -66,11 +66,6 @@ func configureHTTPUpgradeServer(c *Controller) *http.Server { func configureWebhookServer(c *Controller) *http.Server { router := mux.NewRouter().SkipClean(true).UseEncodedPath() - router.Methods(http.MethodGet). - Path(miniov2.WebhookAPIGetenv + "/{namespace}/{name:.+}"). - HandlerFunc(c.GetenvHandler). - Queries(restQueries("key")...) - router.Methods(http.MethodPost). Path(miniov2.WebhookAPIBucketService + "/{namespace}/{name:.+}"). HandlerFunc(c.BucketSrvHandler). diff --git a/main.go b/pkg/controller/controller.go similarity index 98% rename from main.go rename to pkg/controller/controller.go index 46c9a333545..8999fad0b70 100644 --- a/main.go +++ b/pkg/controller/controller.go @@ -12,7 +12,7 @@ // You should have received a copy of the GNU Affero General Public License, version 3, // along with this program. If not, see -package main +package controller import ( "flag" @@ -23,10 +23,10 @@ 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" "k8s.io/klog/v2" @@ -67,7 +67,8 @@ func init() { flag.BoolVar(&checkVersion, "version", false, "print version") } -func main() { +// StartOperator starts the MinIO Operator controller +func StartOperator() { klog.Info("Starting MinIO Operator") // set up signals, so we handle the first shutdown signal gracefully stopCh := setupSignalHandler() diff --git a/pkg/resources/statefulsets/minio-statefulset.go b/pkg/resources/statefulsets/minio-statefulset.go index 454bb4c6a66..14b08d13de4 100644 --- a/pkg/resources/statefulsets/minio-statefulset.go +++ b/pkg/resources/statefulsets/minio-statefulset.go @@ -79,17 +79,6 @@ func minioEnvironmentVars(t *miniov2.Tenant, skipEnvVars map[string][]byte, opVe Name: "MINIO_UPDATE_MINISIGN_PUBKEY", Value: "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav", }, - miniov2.WebhookMinIOArgs: { - Name: miniov2.WebhookMinIOArgs, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: miniov2.WebhookSecret, - }, - Key: miniov2.WebhookMinIOArgs, - }, - }, - }, "MINIO_OPERATOR_VERSION": { Name: "MINIO_OPERATOR_VERSION", Value: opVersion, @@ -198,7 +187,7 @@ func minioEnvironmentVars(t *miniov2.Tenant, skipEnvVars map[string][]byte, opVe if t.HasConfigurationSecret() { envVarsMap["MINIO_CONFIG_ENV_FILE"] = corev1.EnvVar{ Name: "MINIO_CONFIG_ENV_FILE", - Value: miniov2.TmpPath + "/minio-config/config.env", + Value: miniov2.CfgPath, } } @@ -279,15 +268,29 @@ func ContainerMatchLabels(t *miniov2.Tenant, pool *miniov2.Pool) *metav1.LabelSe } } +// CfgVolumeMount is the volume mount used by `minio`, `sidecar` and `validate-arguments` containers +var CfgVolumeMount = corev1.VolumeMount{ + Name: CfgVol, + MountPath: "/etc/minio", +} + +// TmpCfgVolumeMount is the temporary location +var TmpCfgVolumeMount = corev1.VolumeMount{ + Name: "configuration", + MountPath: miniov2.TmpPath + "/minio-config", +} + // Builds the volume mounts for MinIO container. func volumeMounts(t *miniov2.Tenant, pool *miniov2.Pool, operatorTLS bool, certVolumeSources []v1.VolumeProjection) (mounts []v1.VolumeMount) { - // This is the case where user didn't provide a pool and we deploy a EmptyDir based - // single node single drive (FS) MinIO deployment + // Default volume name, unless another one was provided name := miniov2.MinIOVolumeName if pool.VolumeClaimTemplate != nil { name = pool.VolumeClaimTemplate.Name } + // shared configuration Volume + mounts = append(mounts, CfgVolumeMount) + if pool.VolumesPerServer == 1 { mounts = append(mounts, corev1.VolumeMount{ Name: name + strconv.Itoa(0), @@ -311,13 +314,6 @@ func volumeMounts(t *miniov2.Tenant, pool *miniov2.Pool, operatorTLS bool, certV }) } - if t.HasConfigurationSecret() { - mounts = append(mounts, corev1.VolumeMount{ - Name: "configuration", - MountPath: miniov2.TmpPath + "/minio-config", - }) - } - return mounts } @@ -452,8 +448,38 @@ func poolContainerSecurityContext(pool *miniov2.Pool) *v1.SecurityContext { return &containerSecurityContext } +// CfgVol is the name of the configuration volume we will use +const CfgVol = "cfg-vol" + +// NewPoolArgs arguments used to create a new pool +type NewPoolArgs struct { + Tenant *miniov2.Tenant + WsSecret *v1.Secret + SkipEnvVars map[string][]byte + Pool *miniov2.Pool + PoolStatus *miniov2.PoolStatus + ServiceName string + HostsTemplate string + OperatorVersion string + OperatorTLS bool + OperatorCATLS bool + OperatorImage string +} + // NewPool creates a new StatefulSet for the given Cluster. -func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]byte, pool *miniov2.Pool, poolStatus *miniov2.PoolStatus, serviceName, hostsTemplate, operatorVersion string, operatorTLS bool, operatorCATLS bool) *appsv1.StatefulSet { +func NewPool(args *NewPoolArgs) *appsv1.StatefulSet { + t := args.Tenant + wsSecret := args.WsSecret + skipEnvVars := args.SkipEnvVars + pool := args.Pool + poolStatus := args.PoolStatus + serviceName := args.ServiceName + hostsTemplate := args.HostsTemplate + operatorVersion := args.OperatorVersion + operatorTLS := args.OperatorTLS + operatorCATLS := args.OperatorCATLS + operatorImage := args.OperatorImage + var podVolumes []corev1.Volume replicas := pool.Servers var certVolumeSources []corev1.VolumeProjection @@ -468,6 +494,15 @@ func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]by {Key: "public.crt", Path: "CAs/kes.crt"}, } + // Create an empty dir volume to share the configuration between the main container and side-car + + podVolumes = append(podVolumes, corev1.Volume{ + Name: CfgVol, + VolumeSource: v1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + // Multiple certificates will be mounted using the following folder structure: // // certs @@ -791,6 +826,7 @@ func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]by containers := []corev1.Container{ poolMinioServerContainer(t, wsSecret, skipEnvVars, pool, hostsTemplate, operatorVersion, operatorTLS, certVolumeSources), + getSideCardContainer(t, operatorImage), } // attach any sidecar containers and volumes @@ -811,6 +847,8 @@ func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]by unavailable = intstr.FromInt(2) } + initContainer := getInitContainer(t, operatorImage) + ss := &appsv1.StatefulSet{ ObjectMeta: ssMeta, Spec: appsv1.StatefulSetSpec{ @@ -827,6 +865,9 @@ func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]by Template: corev1.PodTemplateSpec{ ObjectMeta: PodMetadata(t, pool), Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + initContainer, + }, Containers: containers, Volumes: podVolumes, RestartPolicy: corev1.RestartPolicyAlways, @@ -868,3 +909,43 @@ func NewPool(t *miniov2.Tenant, wsSecret *v1.Secret, skipEnvVars map[string][]by return ss } + +func getInitContainer(t *miniov2.Tenant, operatorImage string) v1.Container { + initContainer := corev1.Container{ + Name: "validate-arguments", + Image: operatorImage, + Args: []string{ + "validate", + "--tenant", + t.Name, + }, + VolumeMounts: []corev1.VolumeMount{ + CfgVolumeMount, + }, + } + if t.HasConfigurationSecret() { + initContainer.VolumeMounts = append(initContainer.VolumeMounts, TmpCfgVolumeMount) + } + return initContainer +} + +func getSideCardContainer(t *miniov2.Tenant, operatorImage string) v1.Container { + sidecarContainer := corev1.Container{ + Name: "sidecar", + Image: operatorImage, + Args: []string{ + "sidecar", + "--tenant", + t.Name, + "--config-name", + t.Spec.Configuration.Name, + }, + VolumeMounts: []corev1.VolumeMount{ + CfgVolumeMount, + }, + } + if t.HasConfigurationSecret() { + sidecarContainer.VolumeMounts = append(sidecarContainer.VolumeMounts, TmpCfgVolumeMount) + } + return sidecarContainer +} diff --git a/pkg/sidecar/sidecar.go b/pkg/sidecar/sidecar.go new file mode 100644 index 00000000000..ab6eecb1f4f --- /dev/null +++ b/pkg/sidecar/sidecar.go @@ -0,0 +1,193 @@ +// This file is part of MinIO Operator +// Copyright (c) 2023 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 . + +package sidecar + +import ( + "context" + "fmt" + "log" + "os" + "time" + + v2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" + clientset "github.com/minio/operator/pkg/client/clientset/versioned" + minioInformers "github.com/minio/operator/pkg/client/informers/externalversions" + v22 "github.com/minio/operator/pkg/client/informers/externalversions/minio.min.io/v2" + "github.com/minio/operator/pkg/validator" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/informers" + coreinformers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +// StartSideCar instantiates kube clients and starts the side car controller +func StartSideCar(tenantName string, secretName string) { + log.Println("Starting Sidecar") + cfg, err := rest.InClusterConfig() + if err != nil { + panic(err) + } + + if err != nil { + klog.Fatalf("Error building kubeconfig: %s", err.Error()) + } + + kubeClient, err := kubernetes.NewForConfig(cfg) + if err != nil { + klog.Fatalf("Error building Kubernetes clientset: %s", err.Error()) + } + + controllerClient, err := clientset.NewForConfig(cfg) + if err != nil { + klog.Fatalf("Error building MinIO clientset: %s", err.Error()) + } + + controller := NewSideCarController(kubeClient, controllerClient, tenantName, secretName) + + stop := make(chan struct{}) + defer close(stop) + err = controller.Run(stop) + if err != nil { + klog.Fatal(err) + } + select {} +} + +// Controller is the controller holding the informers used to monitor args and tenant structure +type Controller struct { + kubeClient *kubernetes.Clientset + controllerClient *clientset.Clientset + tenantName string + secretName string + minInformerFactory minioInformers.SharedInformerFactory + secretInformer coreinformers.SecretInformer + tenantInformer v22.TenantInformer + namespace string + informerFactory informers.SharedInformerFactory +} + +// NewSideCarController returns an instance of Controller with the provided clients +func NewSideCarController(kubeClient *kubernetes.Clientset, controllerClient *clientset.Clientset, tenantName string, secretName string) *Controller { + namespace := v2.GetNSFromFile() + + factory := informers.NewSharedInformerFactoryWithOptions(kubeClient, time.Hour*1, informers.WithNamespace(namespace)) + secretInformer := factory.Core().V1().Secrets() + + minioInformerFactory := minioInformers.NewSharedInformerFactoryWithOptions(controllerClient, time.Hour*1, minioInformers.WithNamespace(namespace)) + tenantInformer := minioInformerFactory.Minio().V2().Tenants() + + c := &Controller{ + kubeClient: kubeClient, + controllerClient: controllerClient, + tenantName: tenantName, + namespace: namespace, + secretName: secretName, + minInformerFactory: minioInformerFactory, + informerFactory: factory, + tenantInformer: tenantInformer, + secretInformer: secretInformer, + } + + tenantInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(old, new interface{}) { + oldTenant := old.(*v2.Tenant) + newTenant := new.(*v2.Tenant) + if newTenant.ResourceVersion == oldTenant.ResourceVersion { + // Periodic resync will send update events for all known Tenants. + // Two different versions of the same Tenant will always have different RVs. + return + } + c.regenCfg(tenantName, namespace) + }, + }) + + secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(old, new interface{}) { + oldSecret := old.(*corev1.Secret) + // ignore anything that is not what we want + if oldSecret.Name != secretName { + return + } + newSecret := new.(*corev1.Secret) + if newSecret.ResourceVersion == oldSecret.ResourceVersion { + // Periodic resync will send update events for all known Tenants. + // Two different versions of the same Tenant will always have different RVs. + return + } + data := newSecret.Data["config.env"] + c.regenCfgWithCfg(tenantName, namespace, string(data)) + }, + }) + + return c +} + +func (c Controller) regenCfg(tenantName string, namespace string) { + rootUserFound, rootPwdFound, fileContents, err := validator.ReadTmpConfig() + if err != nil { + log.Println(err) + return + } + if !rootUserFound || !rootPwdFound { + log.Println("Missing root credentials in the configuration.") + log.Println("MinIO won't start") + os.Exit(1) + } + c.regenCfgWithCfg(tenantName, namespace, fileContents) +} + +func (c Controller) regenCfgWithCfg(tenantName string, namespace string, fileContents string) { + ctx := context.Background() + + args, err := validator.GetTenantArgs(ctx, c.controllerClient, tenantName, namespace) + if err != nil { + log.Println(err) + return + } + + fileContents = fileContents + fmt.Sprintf("export MINIO_ARGS=\"%s\"\n", args) + + err = os.WriteFile("/etc/minio/config.env", []byte(fileContents), 0o644) + if err != nil { + log.Println(err) + } +} + +// Run starts the informers +func (c *Controller) Run(stopCh chan struct{}) error { + // Starts all the shared minioInformers that have been created by the factory so + // far. + c.minInformerFactory.Start(stopCh) + c.informerFactory.Start(stopCh) + + // wait for the initial synchronization of the local cache. + if !cache.WaitForCacheSync(stopCh, c.tenantInformer.Informer().HasSynced) { + return fmt.Errorf("Failed to sync") + } + // wait for the initial synchronization of the local cache. + if !cache.WaitForCacheSync(stopCh, c.secretInformer.Informer().HasSynced) { + return fmt.Errorf("Failed to sync") + } + return nil +} diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go new file mode 100644 index 00000000000..61e4fb4b490 --- /dev/null +++ b/pkg/validator/validator.go @@ -0,0 +1,140 @@ +// This file is part of MinIO Operator +// Copyright (c) 2023 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 . + +package validator + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strings" + + miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" + clientset "github.com/minio/operator/pkg/client/clientset/versioned" + "github.com/minio/operator/pkg/resources/statefulsets" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" +) + +// Validate checks the configuration on the seeded configuration and issues a valid one for MinIO to +// start, however if root credentials are missing, it will exit with error +func Validate(tenantName string) { + rootUserFound, rootPwdFound, fileContents, err := ReadTmpConfig() + if err != nil { + panic(err) + } + + namespace := miniov2.GetNSFromFile() + + cfg, err := rest.InClusterConfig() + // If config is passed as a flag use that instead + //if kubeconfig != "" { + // cfg, err = clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) + //} + if err != nil { + panic(err) + } + + controllerClient, err := clientset.NewForConfig(cfg) + if err != nil { + klog.Fatalf("Error building MinIO clientset: %s", err.Error()) + } + + ctx := context.Background() + + args, err := GetTenantArgs(ctx, controllerClient, tenantName, namespace) + if err != nil { + log.Println(err) + os.Exit(1) + } + + fileContents = fileContents + fmt.Sprintf("export MINIO_ARGS=\"%s\"\n", args) + + if !rootUserFound || !rootPwdFound { + log.Println("Missing root credentials in the configuration.") + log.Println("MinIO won't start") + os.Exit(1) + } + + err = os.WriteFile("/etc/minio/config.env", []byte(fileContents), 0o644) + if err != nil { + log.Println(err) + } +} + +// GetTenantArgs returns the arguments for the tenant based on the tenants they have +func GetTenantArgs(ctx context.Context, controllerClient *clientset.Clientset, tenantName string, namespace string) (string, error) { + // get the only tenant in this namespace + tenant, err := controllerClient.MinioV2().Tenants(namespace).Get(ctx, tenantName, metav1.GetOptions{}) + if err != nil { + log.Println(err) + return "", err + } + + tenant.EnsureDefaults() + + // Validate the MinIO Tenant + if err = tenant.Validate(); err != nil { + log.Println(err) + return "", err + } + + args := strings.Join(statefulsets.GetContainerArgs(tenant, ""), " ") + return args, err +} + +// ReadTmpConfig reads the seeded configuration from a tmp location +func ReadTmpConfig() (bool, bool, string, error) { + file, err := os.Open("/tmp/minio-config/config.env") + if err != nil { + log.Fatal(err) + } + defer file.Close() + + rootUserFound := false + rootPwdFound := false + + scanner := bufio.NewScanner(file) + newFile := "" + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "MINIO_ROOT_USER") { + rootUserFound = true + } + if strings.Contains(line, "MINIO_ACCESS_KEY") { + rootUserFound = true + } + if strings.Contains(line, "MINIO_ROOT_PASSWORD") { + rootPwdFound = true + } + if strings.Contains(line, "MINIO_SECRET_KEY") { + rootPwdFound = true + } + // We don't allow users to set MINIO_ARGS + if strings.Contains(line, "MINIO_ARGS") { + log.Println("MINIO_ARGS in config file found. It will be ignored.") + continue + } + newFile = newFile + line + "\n" + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + return rootUserFound, rootPwdFound, newFile, nil +} diff --git a/resources/base/cluster-role.yaml b/resources/base/cluster-role.yaml index f90749e7ea3..2ff0468aed7 100644 --- a/resources/base/cluster-role.yaml +++ b/resources/base/cluster-role.yaml @@ -55,6 +55,31 @@ rules: - list - delete - deletecollection + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - apps resources: diff --git a/resources/base/deployment.yaml b/resources/base/deployment.yaml index de5256b0bf8..a0364add8da 100644 --- a/resources/base/deployment.yaml +++ b/resources/base/deployment.yaml @@ -23,6 +23,8 @@ spec: - name: minio-operator image: minio/operator:v4.5.8 imagePullPolicy: IfNotPresent + args: + - controller resources: requests: cpu: 200m diff --git a/testing/deploy-tenant-upgrade.sh b/testing/deploy-tenant-upgrade.sh index 7d79f737efc..323f7f104ab 100755 --- a/testing/deploy-tenant-upgrade.sh +++ b/testing/deploy-tenant-upgrade.sh @@ -57,7 +57,7 @@ function port_forward() { echo 'Validating tenant pods are ready to serve' for pod in `kubectl --namespace $namespace --selector=v1.min.io/tenant=$tenant get pod -o json | jq '.items[] | select(.metadata.name|contains("'$tenant'"))| .metadata.name' | sed 's/"//g'`; do while true; do - if kubectl --namespace $namespace logs pod/$pod | grep --quiet 'All MinIO sub-systems initialized successfully'; then + if kubectl --namespace $namespace -c minio logs pod/$pod | grep --quiet 'All MinIO sub-systems initialized successfully'; then echo "$pod is ready to serve" && break fi sleep 5