|
| 1 | +// This file is part of MinIO Console Server |
| 2 | +// Copyright (c) 2021 MinIO, Inc. |
| 3 | +// |
| 4 | +// This program is free software: you can redistribute it and/or modify |
| 5 | +// it under the terms of the GNU Affero General Public License as published by |
| 6 | +// the Free Software Foundation, either version 3 of the License, or |
| 7 | +// (at your option) any later version. |
| 8 | +// |
| 9 | +// This program is distributed in the hope that it will be useful, |
| 10 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | +// GNU Affero General Public License for more details. |
| 13 | +// |
| 14 | +// You should have received a copy of the GNU Affero General Public License |
| 15 | +// along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + |
| 17 | +package main |
| 18 | + |
| 19 | +import ( |
| 20 | + "fmt" |
| 21 | + "io/ioutil" |
| 22 | + "path/filepath" |
| 23 | + "strconv" |
| 24 | + "time" |
| 25 | + |
| 26 | + "github.com/minio/console/restapi" |
| 27 | + |
| 28 | + "github.com/go-openapi/loads" |
| 29 | + "github.com/jessevdk/go-flags" |
| 30 | + "github.com/minio/cli" |
| 31 | + "github.com/minio/console/operatorapi" |
| 32 | + "github.com/minio/console/operatorapi/operations" |
| 33 | + "github.com/minio/console/pkg/certs" |
| 34 | +) |
| 35 | + |
| 36 | +// starts the server |
| 37 | +var operatorCmd = cli.Command{ |
| 38 | + Name: "operator", |
| 39 | + Aliases: []string{"opr"}, |
| 40 | + Usage: "Start MinIO Operator UI server", |
| 41 | + Action: startOperatorServer, |
| 42 | + Flags: []cli.Flag{ |
| 43 | + cli.StringFlag{ |
| 44 | + Name: "host", |
| 45 | + Value: restapi.GetHostname(), |
| 46 | + Usage: "bind to a specific HOST, HOST can be an IP or hostname", |
| 47 | + }, |
| 48 | + cli.IntFlag{ |
| 49 | + Name: "port", |
| 50 | + Value: restapi.GetPort(), |
| 51 | + Usage: "bind to specific HTTP port", |
| 52 | + }, |
| 53 | + // This is kept here for backward compatibility, |
| 54 | + // hostname's do not have HTTP or HTTPs |
| 55 | + // hostnames are opaque so using --host |
| 56 | + // works for both HTTP and HTTPS setup. |
| 57 | + cli.StringFlag{ |
| 58 | + Name: "tls-host", |
| 59 | + Value: restapi.GetHostname(), |
| 60 | + Hidden: true, |
| 61 | + }, |
| 62 | + cli.StringFlag{ |
| 63 | + Name: "certs-dir", |
| 64 | + Value: certs.GlobalCertsCADir.Get(), |
| 65 | + Usage: "path to certs directory", |
| 66 | + }, |
| 67 | + cli.IntFlag{ |
| 68 | + Name: "tls-port", |
| 69 | + Value: restapi.GetTLSPort(), |
| 70 | + Usage: "bind to specific HTTPS port", |
| 71 | + }, |
| 72 | + cli.StringFlag{ |
| 73 | + Name: "tls-redirect", |
| 74 | + Value: restapi.GetTLSRedirect(), |
| 75 | + Usage: "toggle HTTP->HTTPS redirect", |
| 76 | + }, |
| 77 | + cli.StringFlag{ |
| 78 | + Name: "tls-certificate", |
| 79 | + Value: "", |
| 80 | + Usage: "path to TLS public certificate", |
| 81 | + Hidden: true, |
| 82 | + }, |
| 83 | + cli.StringFlag{ |
| 84 | + Name: "tls-key", |
| 85 | + Value: "", |
| 86 | + Usage: "path to TLS private key", |
| 87 | + Hidden: true, |
| 88 | + }, |
| 89 | + cli.StringFlag{ |
| 90 | + Name: "tls-ca", |
| 91 | + Value: "", |
| 92 | + Usage: "path to TLS Certificate Authority", |
| 93 | + Hidden: true, |
| 94 | + }, |
| 95 | + }, |
| 96 | +} |
| 97 | + |
| 98 | +func buildOperatorServer() (*operatorapi.Server, error) { |
| 99 | + swaggerSpec, err := loads.Embedded(operatorapi.SwaggerJSON, operatorapi.FlatSwaggerJSON) |
| 100 | + if err != nil { |
| 101 | + return nil, err |
| 102 | + } |
| 103 | + |
| 104 | + api := operations.NewOperatorAPI(swaggerSpec) |
| 105 | + api.Logger = operatorapi.LogInfo |
| 106 | + server := operatorapi.NewServer(api) |
| 107 | + |
| 108 | + parser := flags.NewParser(server, flags.Default) |
| 109 | + parser.ShortDescription = "MinIO Console Server" |
| 110 | + parser.LongDescription = swaggerSpec.Spec().Info.Description |
| 111 | + |
| 112 | + server.ConfigureFlags() |
| 113 | + |
| 114 | + // register all APIs |
| 115 | + server.ConfigureAPI() |
| 116 | + |
| 117 | + for _, optsGroup := range api.CommandLineOptionsGroups { |
| 118 | + _, err := parser.AddGroup(optsGroup.ShortDescription, optsGroup.LongDescription, optsGroup.Options) |
| 119 | + if err != nil { |
| 120 | + return nil, err |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + if _, err := parser.Parse(); err != nil { |
| 125 | + return nil, err |
| 126 | + } |
| 127 | + |
| 128 | + return server, nil |
| 129 | +} |
| 130 | + |
| 131 | +func loadOperatorAllCerts(ctx *cli.Context) error { |
| 132 | + var err error |
| 133 | + // Set all certs and CAs directories path |
| 134 | + certs.GlobalCertsDir, _, err = certs.NewConfigDirFromCtx(ctx, "certs-dir", certs.DefaultCertsDir.Get) |
| 135 | + if err != nil { |
| 136 | + return err |
| 137 | + } |
| 138 | + |
| 139 | + certs.GlobalCertsCADir = &certs.ConfigDir{Path: filepath.Join(certs.GlobalCertsDir.Get(), certs.CertsCADir)} |
| 140 | + // check if certs and CAs directories exists or can be created |
| 141 | + if err = certs.MkdirAllIgnorePerm(certs.GlobalCertsCADir.Get()); err != nil { |
| 142 | + return fmt.Errorf("unable to create certs CA directory at %s: failed with %w", certs.GlobalCertsCADir.Get(), err) |
| 143 | + } |
| 144 | + |
| 145 | + // load the certificates and the CAs |
| 146 | + operatorapi.GlobalRootCAs, operatorapi.GlobalPublicCerts, operatorapi.GlobalTLSCertsManager, err = certs.GetAllCertificatesAndCAs() |
| 147 | + if err != nil { |
| 148 | + return fmt.Errorf("unable to load certificates at %s: failed with %w", certs.GlobalCertsDir.Get(), err) |
| 149 | + } |
| 150 | + |
| 151 | + { |
| 152 | + // TLS flags from swagger server, used to support VMware vsphere operator version. |
| 153 | + swaggerServerCertificate := ctx.String("tls-certificate") |
| 154 | + swaggerServerCertificateKey := ctx.String("tls-key") |
| 155 | + swaggerServerCACertificate := ctx.String("tls-ca") |
| 156 | + // load tls cert and key from swagger server tls-certificate and tls-key flags |
| 157 | + if swaggerServerCertificate != "" && swaggerServerCertificateKey != "" { |
| 158 | + if err = operatorapi.GlobalTLSCertsManager.AddCertificate(swaggerServerCertificate, swaggerServerCertificateKey); err != nil { |
| 159 | + return err |
| 160 | + } |
| 161 | + x509Certs, err := certs.ParsePublicCertFile(swaggerServerCertificate) |
| 162 | + if err == nil { |
| 163 | + operatorapi.GlobalPublicCerts = append(operatorapi.GlobalPublicCerts, x509Certs...) |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + // load ca cert from swagger server tls-ca flag |
| 168 | + if swaggerServerCACertificate != "" { |
| 169 | + caCert, caCertErr := ioutil.ReadFile(swaggerServerCACertificate) |
| 170 | + if caCertErr == nil { |
| 171 | + operatorapi.GlobalRootCAs.AppendCertsFromPEM(caCert) |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + return nil |
| 177 | +} |
| 178 | + |
| 179 | +// StartServer starts the console service |
| 180 | +func startOperatorServer(ctx *cli.Context) error { |
| 181 | + if err := loadOperatorAllCerts(ctx); err != nil { |
| 182 | + // Log this as a warning and continue running console without TLS certificates |
| 183 | + operatorapi.LogError("Unable to load certs: %v", err) |
| 184 | + } |
| 185 | + |
| 186 | + var rctx operatorapi.Context |
| 187 | + if err := rctx.Load(ctx); err != nil { |
| 188 | + operatorapi.LogError("argument validation failed: %v", err) |
| 189 | + return err |
| 190 | + } |
| 191 | + |
| 192 | + server, err := buildOperatorServer() |
| 193 | + if err != nil { |
| 194 | + operatorapi.LogError("Unable to initialize console server: %v", err) |
| 195 | + return err |
| 196 | + } |
| 197 | + |
| 198 | + server.Host = rctx.Host |
| 199 | + server.Port = rctx.HTTPPort |
| 200 | + // set conservative timesout for uploads |
| 201 | + server.ReadTimeout = 1 * time.Hour |
| 202 | + // no timeouts for response for downloads |
| 203 | + server.WriteTimeout = 0 |
| 204 | + operatorapi.Port = strconv.Itoa(server.Port) |
| 205 | + operatorapi.Hostname = server.Host |
| 206 | + |
| 207 | + if len(operatorapi.GlobalPublicCerts) > 0 { |
| 208 | + // If TLS certificates are provided enforce the HTTPS schema, meaning console will redirect |
| 209 | + // plain HTTP connections to HTTPS server |
| 210 | + server.EnabledListeners = []string{"http", "https"} |
| 211 | + server.TLSPort = rctx.HTTPSPort |
| 212 | + // Need to store tls-port, tls-host un config variables so secure.middleware can read from there |
| 213 | + operatorapi.TLSPort = strconv.Itoa(server.TLSPort) |
| 214 | + operatorapi.Hostname = rctx.Host |
| 215 | + operatorapi.TLSRedirect = rctx.TLSRedirect |
| 216 | + } |
| 217 | + |
| 218 | + defer server.Shutdown() |
| 219 | + |
| 220 | + // subnet license refresh process |
| 221 | + go func() { |
| 222 | + // start refreshing subnet license after 5 seconds.. |
| 223 | + time.Sleep(time.Second * 5) |
| 224 | + |
| 225 | + failedAttempts := 0 |
| 226 | + for { |
| 227 | + if err := operatorapi.RefreshLicense(); err != nil { |
| 228 | + operatorapi.LogError("Refreshing subnet license failed: %v", err) |
| 229 | + failedAttempts++ |
| 230 | + // end license refresh after 3 consecutive failed attempts |
| 231 | + if failedAttempts >= 3 { |
| 232 | + return |
| 233 | + } |
| 234 | + // wait 5 minutes and retry again |
| 235 | + time.Sleep(time.Minute * 5) |
| 236 | + continue |
| 237 | + } |
| 238 | + // if license refreshed successfully reset the counter |
| 239 | + failedAttempts = 0 |
| 240 | + // try to refresh license every 24 hrs |
| 241 | + time.Sleep(time.Hour * 24) |
| 242 | + } |
| 243 | + }() |
| 244 | + |
| 245 | + return server.Serve() |
| 246 | +} |
0 commit comments