mirror of https://github.com/portainer/portainer
207 lines
8.6 KiB
Go
207 lines
8.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// Service implements the CLIService interface
|
|
type Service struct{}
|
|
|
|
var (
|
|
ErrInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
|
|
ErrSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
|
|
ErrInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
|
|
ErrAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
|
|
)
|
|
|
|
func CLIFlags() *portainer.CLIFlags {
|
|
return &portainer.CLIFlags{
|
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
|
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
|
|
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
|
|
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
|
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
|
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
|
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
|
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
|
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
|
|
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
|
|
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
|
|
Rollback: kingpin.Flag("rollback", "Rollback the database to the previous backup").Bool(),
|
|
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
|
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
|
|
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
|
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
|
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
|
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
|
|
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
|
|
InitialMmapSize: kingpin.Flag("initial-mmap-size", "Initial mmap size of the database in bytes").Int(),
|
|
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
|
|
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
|
|
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
|
|
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
|
|
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
|
|
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
|
|
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
|
|
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
|
|
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
|
|
}
|
|
}
|
|
|
|
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
|
func (Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|
kingpin.Version(version)
|
|
|
|
var hasSSLFlag, hasSSLCertFlag, hasSSLKeyFlag bool
|
|
sslFlag := kingpin.Flag(
|
|
"ssl",
|
|
"Secure Portainer instance using SSL (deprecated)",
|
|
).Default(defaultSSL).IsSetByUser(&hasSSLFlag)
|
|
ssl := sslFlag.Bool()
|
|
sslCertFlag := kingpin.Flag(
|
|
"sslcert",
|
|
"Path to the SSL certificate used to secure the Portainer instance",
|
|
).IsSetByUser(&hasSSLCertFlag)
|
|
sslCert := sslCertFlag.String()
|
|
sslKeyFlag := kingpin.Flag(
|
|
"sslkey",
|
|
"Path to the SSL key used to secure the Portainer instance",
|
|
).IsSetByUser(&hasSSLKeyFlag)
|
|
sslKey := sslKeyFlag.String()
|
|
|
|
flags := CLIFlags()
|
|
|
|
var hasTLSFlag, hasTLSCertFlag, hasTLSKeyFlag bool
|
|
tlsFlag := kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).IsSetByUser(&hasTLSFlag)
|
|
flags.TLS = tlsFlag.Bool()
|
|
tlsCertFlag := kingpin.Flag(
|
|
"tlscert",
|
|
"Path to the TLS certificate file",
|
|
).Default(defaultTLSCertPath).IsSetByUser(&hasTLSCertFlag)
|
|
flags.TLSCert = tlsCertFlag.String()
|
|
tlsKeyFlag := kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).IsSetByUser(&hasTLSKeyFlag)
|
|
flags.TLSKey = tlsKeyFlag.String()
|
|
flags.TLSCacert = kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String()
|
|
|
|
kingpin.Parse()
|
|
|
|
if !filepath.IsAbs(*flags.Assets) {
|
|
ex, err := os.Executable()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
|
}
|
|
|
|
// If the user didn't provide a tls flag remove the defaults to match previous behaviour
|
|
if !hasTLSFlag {
|
|
if !hasTLSCertFlag {
|
|
*flags.TLSCert = ""
|
|
}
|
|
|
|
if !hasTLSKeyFlag {
|
|
*flags.TLSKey = ""
|
|
}
|
|
}
|
|
|
|
if hasSSLFlag {
|
|
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslFlag.Model().Name, tlsFlag.Model().Name)
|
|
|
|
if !hasTLSFlag {
|
|
flags.TLS = ssl
|
|
}
|
|
}
|
|
|
|
if hasSSLCertFlag {
|
|
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslCertFlag.Model().Name, tlsCertFlag.Model().Name)
|
|
|
|
if !hasTLSCertFlag {
|
|
flags.TLSCert = sslCert
|
|
}
|
|
}
|
|
|
|
if hasSSLKeyFlag {
|
|
log.Warn().Msgf("the %q flag is deprecated. use %q instead.", sslKeyFlag.Model().Name, tlsKeyFlag.Model().Name)
|
|
|
|
if !hasTLSKeyFlag {
|
|
flags.TLSKey = sslKey
|
|
}
|
|
}
|
|
|
|
return flags, nil
|
|
}
|
|
|
|
// ValidateFlags validates the values of the flags.
|
|
func (Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|
displayDeprecationWarnings(flags)
|
|
|
|
if err := validateEndpointURL(*flags.EndpointURL); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateSnapshotInterval(*flags.SnapshotInterval); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
|
|
return ErrAdminPassExcludeAdminPassFile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
|
if *flags.NoAnalytics {
|
|
log.Warn().Msg("the --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect")
|
|
}
|
|
}
|
|
|
|
func validateEndpointURL(endpointURL string) error {
|
|
if endpointURL == "" {
|
|
return nil
|
|
}
|
|
|
|
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
|
return ErrInvalidEndpointProtocol
|
|
}
|
|
|
|
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
|
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
|
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
|
|
|
if _, err := os.Stat(socketPath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return ErrSocketOrNamedPipeNotFound
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateSnapshotInterval(snapshotInterval string) error {
|
|
if snapshotInterval == "" {
|
|
return nil
|
|
}
|
|
|
|
if _, err := time.ParseDuration(snapshotInterval); err != nil {
|
|
return ErrInvalidSnapshotInterval
|
|
}
|
|
|
|
return nil
|
|
}
|