From 77509ce391f42dd014422083769c6f8b305b7717 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Thu, 6 Aug 2020 18:03:16 -0700 Subject: [PATCH] Support looking up environment remotely (#10215) adds a feature where we can fetch the MinIO command-line remotely, this is primarily meant to add some stateless nature to the MinIO deployment in k8s environments, MinIO operator would run a webhook service endpoint which can be used to fetch any environment value in a generalized approach. --- cmd/config/constants.go | 7 +- cmd/gateway-main.go | 21 ++--- cmd/server-main.go | 46 +++++------ pkg/env/env.go | 7 +- pkg/env/web_env.go | 167 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 39 deletions(-) create mode 100644 pkg/env/web_env.go diff --git a/cmd/config/constants.go b/cmd/config/constants.go index 3a8bf5868..d25952c1e 100644 --- a/cmd/config/constants.go +++ b/cmd/config/constants.go @@ -31,11 +31,12 @@ const ( EnvDomain = "MINIO_DOMAIN" EnvRegionName = "MINIO_REGION_NAME" EnvPublicIPs = "MINIO_PUBLIC_IPS" - EnvEndpoints = "MINIO_ENDPOINTS" EnvFSOSync = "MINIO_FS_OSYNC" + EnvArgs = "MINIO_ARGS" EnvUpdate = "MINIO_UPDATE" - EnvWorm = "MINIO_WORM" // legacy - EnvRegion = "MINIO_REGION" // legacy + EnvEndpoints = "MINIO_ENDPOINTS" // legacy + EnvWorm = "MINIO_WORM" // legacy + EnvRegion = "MINIO_REGION" // legacy ) diff --git a/cmd/gateway-main.go b/cmd/gateway-main.go index 3f82c73a5..ce4c05753 100644 --- a/cmd/gateway-main.go +++ b/cmd/gateway-main.go @@ -172,6 +172,18 @@ func StartGateway(ctx *cli.Context, gw Gateway) { // Handle common command args. handleCommonCmdArgs(ctx) + // Check and load TLS certificates. + var err error + globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig() + logger.FatalIf(err, "Invalid TLS certificate file") + + // Check and load Root CAs. + globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get()) + logger.FatalIf(err, "Failed to read root CAs (%v)", err) + + // Register root CAs for remote ENVs + env.RegisterGlobalCAs(globalRootCAs) + // Initialize all help initHelp() @@ -184,15 +196,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) { // To avoid this error situation we check for port availability. logger.FatalIf(checkPortAvailability(globalMinioHost, globalMinioPort), "Unable to start the gateway") - // Check and load TLS certificates. - var err error - globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig() - logger.FatalIf(err, "Invalid TLS certificate file") - - // Check and load Root CAs. - globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get()) - logger.FatalIf(err, "Failed to read root CAs (%v)", err) - globalMinioEndpoint = func() string { host := globalMinioHost if host == "" { diff --git a/cmd/server-main.go b/cmd/server-main.go index c8d4efa4d..0e0a0d954 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -93,11 +93,16 @@ EXAMPLES: `, } -// Checks if endpoints are either available through environment -// or command line, returns false if both fails. -func endpointsPresent(ctx *cli.Context) bool { - endpoints := env.Get(config.EnvEndpoints, strings.Join(ctx.Args(), config.ValueSeparator)) - return len(endpoints) != 0 +func serverCmdArgs(ctx *cli.Context) []string { + v := env.Get(config.EnvArgs, "") + if v == "" { + // Fall back to older ENV MINIO_ENDPOINTS + v = env.Get(config.EnvEndpoints, "") + } + if v == "" { + return ctx.Args() + } + return strings.Fields(v) } func serverHandleCmdArgs(ctx *cli.Context) { @@ -106,18 +111,24 @@ func serverHandleCmdArgs(ctx *cli.Context) { logger.FatalIf(CheckLocalServerAddr(globalCLIContext.Addr), "Unable to validate passed arguments") - var setupType SetupType var err error + var setupType SetupType + + // Check and load TLS certificates. + globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig() + logger.FatalIf(err, "Unable to load the TLS configuration") + + // Check and load Root CAs. + globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get()) + logger.FatalIf(err, "Failed to read root CAs (%v)", err) + + // Register root CAs for remote ENVs + env.RegisterGlobalCAs(globalRootCAs) globalMinioAddr = globalCLIContext.Addr globalMinioHost, globalMinioPort = mustSplitHostPort(globalMinioAddr) - endpoints := strings.Fields(env.Get(config.EnvEndpoints, "")) - if len(endpoints) > 0 { - globalEndpoints, setupType, err = createServerEndpoints(globalCLIContext.Addr, endpoints...) - } else { - globalEndpoints, setupType, err = createServerEndpoints(globalCLIContext.Addr, ctx.Args()...) - } + globalEndpoints, setupType, err = createServerEndpoints(globalCLIContext.Addr, serverCmdArgs(ctx)...) logger.FatalIf(err, "Invalid command line arguments") // On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back @@ -370,9 +381,6 @@ func startBackgroundOps(ctx context.Context, objAPI ObjectLayer) { // serverMain handler called for 'minio server' command. func serverMain(ctx *cli.Context) { - if ctx.Args().First() == "help" || !endpointsPresent(ctx) { - cli.ShowCommandHelpAndExit(ctx, "server", 1) - } setDefaultProfilerRates() // Initialize globalConsoleSys system @@ -392,15 +400,7 @@ func serverMain(ctx *cli.Context) { // Initialize all help initHelp() - // Check and load TLS certificates. var err error - globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig() - logger.FatalIf(err, "Unable to load the TLS configuration") - - // Check and load Root CAs. - globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get()) - logger.FatalIf(err, "Failed to read root CAs (%v)", err) - globalProxyEndpoints, err = GetProxyEndpoints(globalEndpoints) logger.FatalIf(err, "Invalid command line arguments") diff --git a/pkg/env/env.go b/pkg/env/env.go index bdc406ebf..6a875bbc5 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -18,7 +18,6 @@ package env import ( - "os" "strings" "sync" ) @@ -46,7 +45,7 @@ func SetEnvOn() { // IsSet returns if the given env key is set. func IsSet(key string) bool { - _, ok := os.LookupEnv(key) + _, ok := LookupEnv(key) return ok } @@ -61,7 +60,7 @@ func Get(key, defaultValue string) string { if ok { return defaultValue } - if v, ok := os.LookupEnv(key); ok { + if v, ok := LookupEnv(key); ok { return v } return defaultValue @@ -69,7 +68,7 @@ func Get(key, defaultValue string) string { // List all envs with a given prefix. func List(prefix string) (envs []string) { - for _, env := range os.Environ() { + for _, env := range Environ() { if strings.HasPrefix(env, prefix) { values := strings.SplitN(env, "=", 2) if len(values) == 2 { diff --git a/pkg/env/web_env.go b/pkg/env/web_env.go new file mode 100644 index 000000000..1134d0d30 --- /dev/null +++ b/pkg/env/web_env.go @@ -0,0 +1,167 @@ +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package env + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "regexp" + "time" +) + +const ( + webEnvScheme = "env" + webEnvSchemeSecure = "env+tls" +) + +var ( + globalRootCAs *x509.CertPool +) + +// RegisterGlobalCAs register the global root CAs +func RegisterGlobalCAs(CAs *x509.CertPool) { + globalRootCAs = CAs +} + +func isValidEnvScheme(scheme string) bool { + switch scheme { + case webEnvScheme: + fallthrough + case webEnvSchemeSecure: + return true + } + return false +} + +var ( + hostKeys = regexp.MustCompile("^(https?://)(.*?):(.*?)@(.*?)$") +) + +func fetchEnvHTTP(envKey string, u *url.URL) (string, error) { + switch u.Scheme { + case webEnvScheme: + u.Scheme = "http" + case webEnvSchemeSecure: + u.Scheme = "https" + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + var ( + username, password string + ) + + envURL := u.String() + if hostKeys.MatchString(envURL) { + parts := hostKeys.FindStringSubmatch(envURL) + if len(parts) != 5 { + return "", errors.New("invalid arguments") + } + username = parts[2] + password = parts[3] + envURL = fmt.Sprintf("%s%s", parts[1], parts[4]) + } + + if username == "" && password == "" && u.User != nil { + username = u.User.Username() + password, _ = u.User.Password() + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, envURL+"?key="+envKey, nil) + if err != nil { + return "", err + } + + if username != "" && password != "" { + req.SetBasicAuth(username, password) + } + + clnt := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 3 * time.Second, + KeepAlive: 5 * time.Second, + }).DialContext, + ResponseHeaderTimeout: 3 * time.Second, + TLSHandshakeTimeout: 3 * time.Second, + ExpectContinueTimeout: 3 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: globalRootCAs, + }, + // Go net/http automatically unzip if content-type is + // gzip disable this feature, as we are always interested + // in raw stream. + DisableCompression: true, + }, + } + + resp, err := clnt.Do(req) + if err != nil { + return "", err + } + + envValueBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(envValueBytes), nil +} + +// Environ returns a copy of strings representing the +// environment, in the form "key=value". +func Environ() []string { + return os.Environ() +} + +// LookupEnv retrieves the value of the environment variable +// named by the key. If the variable is present in the +// environment the value (which may be empty) is returned +// and the boolean is true. Otherwise the returned value +// will be empty and the boolean will be false. +// +// Additionally if the input is env://username:password@remote:port/ +// to fetch ENV values for the env value from a remote server. +func LookupEnv(key string) (string, bool) { + v, ok := os.LookupEnv(key) + if ok { + u, err := url.Parse(v) + if err != nil { + return "", false + } + if !isValidEnvScheme(u.Scheme) { + return v, true + } + v, err = fetchEnvHTTP(key, u) + if err != nil { + return "", false + } + return v, true + } + return "", false +}