mirror of https://github.com/portainer/portainer
feat(server): use https by default (#5315) [EE-332]
parent
3257cb1e28
commit
11d555bbd6
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/portainer/portainer/api/bolt/role"
|
||||
"github.com/portainer/portainer/api/bolt/schedule"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
"github.com/portainer/portainer/api/bolt/ssl"
|
||||
"github.com/portainer/portainer/api/bolt/stack"
|
||||
"github.com/portainer/portainer/api/bolt/tag"
|
||||
"github.com/portainer/portainer/api/bolt/team"
|
||||
|
@ -61,6 +62,7 @@ type Store struct {
|
|||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
|
@ -114,6 +116,7 @@ func (store *Store) Open() error {
|
|||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
// Safe to being called multiple times.
|
||||
func (store *Store) Close() error {
|
||||
if store.connection.DB != nil {
|
||||
return store.connection.Close()
|
||||
|
|
|
@ -55,6 +55,22 @@ func (store *Store) Init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = store.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
if err != errors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultSSLSettings := &portainer.SSLSettings{
|
||||
HTTPEnabled: true,
|
||||
}
|
||||
|
||||
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
groups, err := store.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/portainer/portainer/api/bolt/role"
|
||||
"github.com/portainer/portainer/api/bolt/schedule"
|
||||
"github.com/portainer/portainer/api/bolt/settings"
|
||||
"github.com/portainer/portainer/api/bolt/ssl"
|
||||
"github.com/portainer/portainer/api/bolt/stack"
|
||||
"github.com/portainer/portainer/api/bolt/tag"
|
||||
"github.com/portainer/portainer/api/bolt/team"
|
||||
|
@ -105,6 +106,12 @@ func (store *Store) initServices() error {
|
|||
}
|
||||
store.SettingsService = settingsService
|
||||
|
||||
sslSettingsService, err := ssl.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.SSLSettingsService = sslSettingsService
|
||||
|
||||
stackService, err := stack.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -217,6 +224,11 @@ func (store *Store) Settings() portainer.SettingsService {
|
|||
return store.SettingsService
|
||||
}
|
||||
|
||||
// SSLSettings gives access to the SSL Settings data management layer
|
||||
func (store *Store) SSLSettings() portainer.SSLSettingsService {
|
||||
return store.SSLSettingsService
|
||||
}
|
||||
|
||||
// Stack gives access to the Stack data management layer
|
||||
func (store *Store) Stack() portainer.StackService {
|
||||
return store.StackService
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package ssl
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "ssl"
|
||||
key = "SSL"
|
||||
)
|
||||
|
||||
// Service represents a service for managing ssl data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Settings retrieve the ssl settings object.
|
||||
func (service *Service) Settings() (*portainer.SSLSettings, error) {
|
||||
var settings portainer.SSLSettings
|
||||
|
||||
err := internal.GetObject(service.connection, BucketName, []byte(key), &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a SSLSettings object.
|
||||
func (service *Service) UpdateSettings(settings *portainer.SSLSettings) error {
|
||||
return internal.UpdateObject(service.connection, BucketName, []byte(key), settings)
|
||||
}
|
|
@ -30,6 +30,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
|
||||
flags := &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(),
|
||||
|
@ -42,9 +43,10 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
|
@ -92,6 +94,10 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
|||
if *flags.NoAnalytics {
|
||||
log.Println("Warning: 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.")
|
||||
}
|
||||
|
||||
if *flags.SSL {
|
||||
log.Println("Warning: SSL is enabled by default and there is no need for the --ssl flag. It has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
|
|
|
@ -4,6 +4,7 @@ package cli
|
|||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
|
@ -13,6 +14,7 @@ const (
|
|||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
|
|
|
@ -2,6 +2,7 @@ package cli
|
|||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
|
@ -11,6 +12,7 @@ const (
|
|||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
@ -54,7 +55,7 @@ func initFileService(dataStorePath string) portainer.FileService {
|
|||
return fileService
|
||||
}
|
||||
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
|
@ -74,9 +75,16 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
|
|||
if err != nil {
|
||||
log.Fatalf("failed migration: %v", err)
|
||||
}
|
||||
|
||||
go shutdownDatastore(shutdownCtx, store)
|
||||
return store
|
||||
}
|
||||
|
||||
func shutdownDatastore(shutdownCtx context.Context, datastore portainer.DataStore) {
|
||||
<-shutdownCtx.Done()
|
||||
datastore.Close()
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, dataStorePath, proxyManager)
|
||||
if err != nil {
|
||||
|
@ -136,6 +144,23 @@ func initGitService() portainer.GitService {
|
|||
return git.NewService()
|
||||
}
|
||||
|
||||
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore portainer.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
||||
slices := strings.Split(addr, ":")
|
||||
host := slices[0]
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
|
||||
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
|
||||
|
||||
err := sslService.Init(host, certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sslService, nil
|
||||
}
|
||||
|
||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
@ -182,7 +207,26 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
|
|||
settings.BlackListedLabels = *flags.Labels
|
||||
}
|
||||
|
||||
return dataStore.Settings().UpdateSettings(settings)
|
||||
err = dataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpEnabled := !*flags.HTTPDisabled
|
||||
|
||||
sslSettings, err := dataStore.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sslSettings.HTTPEnabled = httpEnabled
|
||||
|
||||
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
|
@ -354,7 +398,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, fileService)
|
||||
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -375,6 +419,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing key pai: %v", err)
|
||||
|
@ -467,7 +516,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed starting license service: %s", err)
|
||||
log.Fatalf("failed starting tunnel server: %s", err)
|
||||
}
|
||||
|
||||
sslSettings, err := dataStore.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to fetch ssl settings from DB")
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
|
@ -475,6 +529,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
BindAddressHTTPS: *flags.AddrHTTPS,
|
||||
HTTPEnabled: sslSettings.HTTPEnabled,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
|
@ -490,9 +546,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
SSLService: sslService,
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
ShutdownCtx: shutdownCtx,
|
||||
|
@ -507,8 +561,8 @@ func main() {
|
|||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
log.Printf("Starting Portainer %s on %s\n", portainer.APIVersion, *flags.Addr)
|
||||
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
||||
err := server.Start()
|
||||
log.Printf("Http server exited: %s\n", err)
|
||||
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,12 @@ const (
|
|||
CustomTemplateStorePath = "custom_templates"
|
||||
// TempPath represent the subfolder where temporary files are saved
|
||||
TempPath = "tmp"
|
||||
// SSLCertPath represents the default ssl certificates path
|
||||
SSLCertPath = "certs"
|
||||
// DefaultSSLCertFilename represents the default ssl certificate file name
|
||||
DefaultSSLCertFilename = "cert.pem"
|
||||
// DefaultSSLKeyFilename represents the default ssl key file name
|
||||
DefaultSSLKeyFilename = "key.pem"
|
||||
)
|
||||
|
||||
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
|
||||
|
@ -74,6 +80,11 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
err = service.createDirectoryInStore(SSLCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = service.createDirectoryInStore(TLSStorePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -108,6 +119,66 @@ func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
|||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
}
|
||||
|
||||
// Copy copies the file on fromFilePath to toFilePath
|
||||
// if toFilePath exists func will fail unless deleteIfExists is true
|
||||
func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error {
|
||||
exists, err := service.FileExists(fromFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.New("File doesn't exist")
|
||||
}
|
||||
|
||||
finput, err := os.Open(fromFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer finput.Close()
|
||||
|
||||
exists, err = service.FileExists(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
if !deleteIfExists {
|
||||
return errors.New("Destination file exists")
|
||||
}
|
||||
|
||||
err := os.Remove(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
foutput, err := os.Create(toFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer foutput.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := finput.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := foutput.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
|
@ -507,6 +578,58 @@ func (service *Service) GetDatastorePath() string {
|
|||
return service.dataStorePath
|
||||
}
|
||||
|
||||
func (service *Service) wrapFileStore(filepath string) string {
|
||||
return path.Join(service.fileStorePath, filepath)
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
// GetDefaultSSLCertsPath returns the ssl certs path
|
||||
func (service *Service) GetDefaultSSLCertsPath() (string, string) {
|
||||
certPath, keyPath := defaultCertPathUnderFileStore()
|
||||
return service.wrapFileStore(certPath), service.wrapFileStore(keyPath)
|
||||
}
|
||||
|
||||
// StoreSSLCertPair stores a ssl certificate pair
|
||||
func (service *Service) StoreSSLCertPair(cert, key []byte) (string, string, error) {
|
||||
certPath, keyPath := defaultCertPathUnderFileStore()
|
||||
|
||||
r := bytes.NewReader(cert)
|
||||
err := service.createFileInStore(certPath, r)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
r = bytes.NewReader(key)
|
||||
err = service.createFileInStore(keyPath, r)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(certPath), service.wrapFileStore(keyPath), nil
|
||||
}
|
||||
|
||||
// CopySSLCertPair copies a ssl certificate pair
|
||||
func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, string, error) {
|
||||
defCertPath, defKeyPath := service.GetDefaultSSLCertsPath()
|
||||
|
||||
err := service.Copy(certPath, defCertPath, false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
err = service.Copy(keyPath, defKeyPath, false)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return defCertPath, defKeyPath, nil
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
func FileExists(filePath string) (bool, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
|
|
|
@ -29,7 +29,7 @@ require (
|
|||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92
|
||||
github.com/portainer/libcompose v0.5.3
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
|
|
|
@ -245,8 +245,8 @@ github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92 h
|
|||
github.com/portainer/docker-compose-wrapper v0.0.0-20210527221011-0a1418224b92/go.mod h1:PF2O2O4UNYWdtPcp6n/mIKpKk+f1jhFTezS8txbf+XM=
|
||||
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
|
||||
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
"github.com/portainer/portainer/api/http/handler/settings"
|
||||
"github.com/portainer/portainer/api/http/handler/ssl"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
|
@ -54,6 +55,7 @@ type Handler struct {
|
|||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
SSLHandler *ssl.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
TagHandler *tags.Handler
|
||||
|
@ -130,6 +132,8 @@ type Handler struct {
|
|||
// @tag.description Manage App Templates
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage stacks
|
||||
// @tag.name ssl
|
||||
// @tag.description Manage ssl settings
|
||||
// @tag.name upload
|
||||
// @tag.description Upload files
|
||||
// @tag.name webhooks
|
||||
|
@ -199,6 +203,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ssl"):
|
||||
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package ssl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle MOTD operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
SSLService *ssl.Service
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/ssl",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.sslInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/ssl",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.sslUpdate))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ssl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// @id SSLInspect
|
||||
// @summary Inspect the ssl settings
|
||||
// @description Retrieve the ssl settings.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags ssl
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.SSLSettings "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /ssl [get]
|
||||
func (handler *Handler) sslInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SSLService.GetSSLSettings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to fetch certificate info", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, settings)
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package ssl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type sslUpdatePayload struct {
|
||||
Cert *string
|
||||
Key *string
|
||||
HTTPEnabled *bool
|
||||
}
|
||||
|
||||
func (payload *sslUpdatePayload) Validate(r *http.Request) error {
|
||||
if (payload.Cert == nil || payload.Key == nil) && payload.Cert != payload.Key {
|
||||
return errors.New("both certificate and key files should be provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id SSLUpdate
|
||||
// @summary Update the ssl settings
|
||||
// @description Update the ssl settings.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags ssl
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body sslUpdatePayload true "SSL Settings"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /ssl [put]
|
||||
func (handler *Handler) sslUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload sslUpdatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
if payload.Cert != nil {
|
||||
err = handler.SSLService.SetCertificates([]byte(*payload.Cert), []byte(*payload.Key))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to save certificate", err}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.HTTPEnabled != nil {
|
||||
err = handler.SSLService.SetHTTPEnabled(*payload.HTTPEnabled)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to force https", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -2,6 +2,7 @@ package http
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -31,6 +32,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
"github.com/portainer/portainer/api/http/handler/settings"
|
||||
sslhandler "github.com/portainer/portainer/api/http/handler/ssl"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
|
@ -46,13 +48,16 @@ import (
|
|||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
// Server implements the portainer.Server interface
|
||||
type Server struct {
|
||||
AuthorizationService *authorization.Service
|
||||
AuthorizationService *authorization.Service
|
||||
BindAddress string
|
||||
BindAddressHTTPS string
|
||||
HTTPEnabled bool
|
||||
AssetsPath string
|
||||
Status *portainer.Status
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
|
@ -70,9 +75,7 @@ type Server struct {
|
|||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
SSLService *ssl.Service
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
KubernetesDeployer portainer.KubernetesDeployer
|
||||
|
@ -175,6 +178,9 @@ func (server *Server) Start() error {
|
|||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.SnapshotService = server.SnapshotService
|
||||
|
||||
var sslHandler = sslhandler.NewHandler(requestBouncer)
|
||||
sslHandler.SSLService = server.SSLService
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.DataStore = server.DataStore
|
||||
stackHandler.DockerClientFactory = server.DockerClientFactory
|
||||
|
@ -236,6 +242,7 @@ func (server *Server) Start() error {
|
|||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
SSLHandler: sslHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
TagHandler: tagHandler,
|
||||
|
@ -248,31 +255,48 @@ func (server *Server) Start() error {
|
|||
WebhookHandler: webhookHandler,
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: server.BindAddress,
|
||||
Handler: server.Handler,
|
||||
}
|
||||
httpServer.Handler = offlineGate.WaitingMiddleware(time.Minute, httpServer.Handler)
|
||||
handler := offlineGate.WaitingMiddleware(time.Minute, server.Handler)
|
||||
|
||||
if server.SSL {
|
||||
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
|
||||
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
|
||||
if server.HTTPEnabled {
|
||||
go func() {
|
||||
log.Printf("[INFO] [http,server] [message: starting HTTP server on port %s]", server.BindAddress)
|
||||
httpServer := &http.Server{
|
||||
Addr: server.BindAddress,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
go shutdown(server.ShutdownCtx, httpServer)
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[ERROR] [message: http server failed] [error: %s]", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go server.shutdown(httpServer)
|
||||
log.Printf("[INFO] [http,server] [message: starting HTTPS server on port %s]", server.BindAddressHTTPS)
|
||||
httpsServer := &http.Server{
|
||||
Addr: server.BindAddressHTTPS,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
return httpServer.ListenAndServe()
|
||||
httpsServer.TLSConfig = crypto.CreateServerTLSConfiguration()
|
||||
httpsServer.TLSConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return server.SSLService.GetRawCertificate(), nil
|
||||
}
|
||||
|
||||
go shutdown(server.ShutdownCtx, httpsServer)
|
||||
return httpsServer.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
func (server *Server) shutdown(httpServer *http.Server) {
|
||||
<-server.ShutdownCtx.Done()
|
||||
func shutdown(shutdownCtx context.Context, httpServer *http.Server) {
|
||||
<-shutdownCtx.Done()
|
||||
|
||||
log.Println("[DEBUG] Shutting down http server")
|
||||
log.Println("[DEBUG] [http,server] [message: shutting down http server]")
|
||||
shutdownTimeout, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := httpServer.Shutdown(shutdownTimeout)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed shutdown http server: %s \n", err)
|
||||
fmt.Printf("[ERROR] [http,server] [message: failed shutdown http server] [error: %s]", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
package ssl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libcrypto"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Service represents a service to manage SSL certificates
|
||||
type Service struct {
|
||||
fileService portainer.FileService
|
||||
dataStore portainer.DataStore
|
||||
rawCert *tls.Certificate
|
||||
shutdownTrigger context.CancelFunc
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new Service
|
||||
func NewService(fileService portainer.FileService, dataStore portainer.DataStore, shutdownTrigger context.CancelFunc) *Service {
|
||||
return &Service{
|
||||
fileService: fileService,
|
||||
dataStore: dataStore,
|
||||
shutdownTrigger: shutdownTrigger,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the service
|
||||
func (service *Service) Init(host, certPath, keyPath string) error {
|
||||
settings, err := service.GetSSLSettings()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed fetching ssl settings")
|
||||
}
|
||||
|
||||
// certificates already exist
|
||||
if settings.CertPath != "" && settings.KeyPath != "" {
|
||||
err := service.cacheCertificate(settings.CertPath, settings.KeyPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// continue if certs don't exist
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
pathSupplied := certPath != "" && keyPath != ""
|
||||
if pathSupplied {
|
||||
newCertPath, newKeyPath, err := service.fileService.CopySSLCertPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed copying supplied certs")
|
||||
}
|
||||
|
||||
return service.cacheInfo(newCertPath, newKeyPath, false)
|
||||
}
|
||||
|
||||
// path not supplied and certificates doesn't exist - generate self signed
|
||||
certPath, keyPath = service.fileService.GetDefaultSSLCertsPath()
|
||||
|
||||
err = service.generateSelfSignedCertificates(host, certPath, keyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed generating self signed certs")
|
||||
}
|
||||
|
||||
return service.cacheInfo(certPath, keyPath, true)
|
||||
|
||||
}
|
||||
|
||||
// GetRawCertificate gets the raw certificate
|
||||
func (service *Service) GetRawCertificate() *tls.Certificate {
|
||||
return service.rawCert
|
||||
}
|
||||
|
||||
// GetSSLSettings gets the certificate info
|
||||
func (service *Service) GetSSLSettings() (*portainer.SSLSettings, error) {
|
||||
return service.dataStore.SSLSettings().Settings()
|
||||
}
|
||||
|
||||
// SetCertificates sets the certificates
|
||||
func (service *Service) SetCertificates(certData, keyData []byte) error {
|
||||
if len(certData) == 0 || len(keyData) == 0 {
|
||||
return errors.New("missing certificate files")
|
||||
}
|
||||
|
||||
_, err := tls.X509KeyPair(certData, keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certPath, keyPath, err := service.fileService.StoreSSLCertPair(certData, keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.cacheInfo(certPath, keyPath, false)
|
||||
|
||||
service.shutdownTrigger()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) SetHTTPEnabled(httpEnabled bool) error {
|
||||
settings, err := service.dataStore.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.HTTPEnabled == httpEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings.HTTPEnabled = httpEnabled
|
||||
|
||||
err = service.dataStore.SSLSettings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.shutdownTrigger()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) cacheCertificate(certPath, keyPath string) error {
|
||||
rawCert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.rawCert = &rawCert
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) cacheInfo(certPath, keyPath string, selfSigned bool) error {
|
||||
err := service.cacheCertificate(certPath, keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := service.dataStore.SSLSettings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.CertPath = certPath
|
||||
settings.KeyPath = keyPath
|
||||
settings.SelfSigned = selfSigned
|
||||
|
||||
err = service.dataStore.SSLSettings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) generateSelfSignedCertificates(ip, certPath, keyPath string) error {
|
||||
if ip == "" {
|
||||
return errors.New("host can't be empty")
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [internal,ssl] [message: no cert files found, generating self signed ssl certificates]")
|
||||
return libcrypto.GenerateCertsForHost("localhost", ip, certPath, keyPath, time.Now().AddDate(5, 0, 0))
|
||||
}
|
|
@ -17,6 +17,7 @@ type datastore struct {
|
|||
registry portainer.RegistryService
|
||||
resourceControl portainer.ResourceControlService
|
||||
role portainer.RoleService
|
||||
sslSettings portainer.SSLSettingsService
|
||||
settings portainer.SettingsService
|
||||
stack portainer.StackService
|
||||
tag portainer.TagService
|
||||
|
@ -47,6 +48,7 @@ func (d *datastore) Registry() portainer.RegistryService { retur
|
|||
func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl }
|
||||
func (d *datastore) Role() portainer.RoleService { return d.role }
|
||||
func (d *datastore) Settings() portainer.SettingsService { return d.settings }
|
||||
func (d *datastore) SSLSettings() portainer.SSLSettingsService { return d.sslSettings }
|
||||
func (d *datastore) Stack() portainer.StackService { return d.stack }
|
||||
func (d *datastore) Tag() portainer.TagService { return d.tag }
|
||||
func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership }
|
||||
|
|
|
@ -44,6 +44,7 @@ type (
|
|||
// CLIFlags represents the available flags on the CLI
|
||||
CLIFlags struct {
|
||||
Addr *string
|
||||
AddrHTTPS *string
|
||||
TunnelAddr *string
|
||||
TunnelPort *string
|
||||
AdminPassword *string
|
||||
|
@ -61,6 +62,7 @@ type (
|
|||
TLSCacert *string
|
||||
TLSCert *string
|
||||
TLSKey *string
|
||||
HTTPDisabled *bool
|
||||
SSL *bool
|
||||
SSLCert *string
|
||||
SSLKey *string
|
||||
|
@ -704,6 +706,14 @@ type (
|
|||
// SoftwareEdition represents an edition of Portainer
|
||||
SoftwareEdition int
|
||||
|
||||
// SSLSettings represents a pair of SSL certificate and key
|
||||
SSLSettings struct {
|
||||
CertPath string `json:"certPath"`
|
||||
KeyPath string `json:"keyPath"`
|
||||
SelfSigned bool `json:"selfSigned"`
|
||||
HTTPEnabled bool `json:"httpEnabled"`
|
||||
}
|
||||
|
||||
// Stack represents a Docker stack created via docker stack deploy
|
||||
Stack struct {
|
||||
// Stack Identifier
|
||||
|
@ -1056,6 +1066,7 @@ type (
|
|||
ResourceControl() ResourceControlService
|
||||
Role() RoleService
|
||||
Settings() SettingsService
|
||||
SSLSettings() SSLSettingsService
|
||||
Stack() StackService
|
||||
Tag() TagService
|
||||
TeamMembership() TeamMembershipService
|
||||
|
@ -1166,6 +1177,9 @@ type (
|
|||
GetCustomTemplateProjectPath(identifier string) string
|
||||
GetTemporaryPath() (string, error)
|
||||
GetDatastorePath() string
|
||||
GetDefaultSSLCertsPath() (string, string)
|
||||
StoreSSLCertPair(cert, key []byte) (string, string, error)
|
||||
CopySSLCertPair(certPath, keyPath string) (string, string, error)
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
|
@ -1271,6 +1285,12 @@ type (
|
|||
Start() error
|
||||
}
|
||||
|
||||
// SSLSettingsService represents a service for managing application settings
|
||||
SSLSettingsService interface {
|
||||
Settings() (*SSLSettings, error)
|
||||
UpdateSettings(settings *SSLSettings) error
|
||||
}
|
||||
|
||||
// StackService represents a service for managing stack data
|
||||
StackService interface {
|
||||
Stack(ID StackID) (*Stack, error)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
import componentsModule from './components';
|
||||
import settingsModule from './settings';
|
||||
|
||||
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
|
@ -17,7 +18,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
|
|||
return await Authentication.init();
|
||||
}
|
||||
|
||||
angular.module('portainer.app', ['portainer.oauth', componentsModule]).config([
|
||||
angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsModule]).config([
|
||||
'$stateRegistryProvider',
|
||||
function ($stateRegistryProvider) {
|
||||
'use strict';
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import angular from 'angular';
|
||||
|
||||
const API_ENDPOINT_SSL = 'api/ssl';
|
||||
|
||||
angular.module('portainer.app').factory('SSL', SSLFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function SSLFactory($resource) {
|
||||
return $resource(
|
||||
API_ENDPOINT_SSL,
|
||||
{},
|
||||
{
|
||||
get: { method: 'GET' },
|
||||
upload: { method: 'PUT' },
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.app').service('SSLService', SSLServiceFactory);
|
||||
|
||||
/* @ngInject */
|
||||
function SSLServiceFactory(SSL) {
|
||||
return {
|
||||
upload,
|
||||
get,
|
||||
};
|
||||
|
||||
function get() {
|
||||
return SSL.get().$promise;
|
||||
}
|
||||
|
||||
function upload(httpEnabled, cert, key) {
|
||||
return SSL.upload({ httpEnabled, cert, key }).$promise;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { sslCertificate } from './ssl-certificate';
|
||||
|
||||
export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).name;
|
|
@ -0,0 +1,6 @@
|
|||
import controller from './ssl-certificate.controller.js';
|
||||
|
||||
export const sslCertificate = {
|
||||
templateUrl: './ssl-certificate.html',
|
||||
controller,
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
class SslCertificateController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, SSLService, Notifications) {
|
||||
Object.assign(this, { $async, $state, SSLService, Notifications });
|
||||
|
||||
this.cert = null;
|
||||
this.originalValues = {
|
||||
forceHTTPS: false,
|
||||
certFile: null,
|
||||
keyFile: null,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
certFile: null,
|
||||
keyFile: null,
|
||||
forceHTTPS: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
reloadingPage: false,
|
||||
};
|
||||
|
||||
const pemPattern = '.pem';
|
||||
this.certFilePattern = `${pemPattern},.crt,.cer,.cert`;
|
||||
this.keyFilePattern = `${pemPattern},.key`;
|
||||
|
||||
this.save = this.save.bind(this);
|
||||
}
|
||||
|
||||
isFormChanged() {
|
||||
return Object.entries(this.originalValues).some(([key, value]) => value != this.formValues[key]);
|
||||
}
|
||||
|
||||
async save() {
|
||||
return this.$async(async () => {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
const cert = this.formValues.certFile ? await this.formValues.certFile.text() : null;
|
||||
const key = this.formValues.keyFile ? await this.formValues.keyFile.text() : null;
|
||||
const httpEnabled = !this.formValues.forceHTTPS;
|
||||
await this.SSLService.upload(httpEnabled, cert, key);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
location.reload();
|
||||
this.state.reloadingPage = true;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
this.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const certInfo = await this.SSLService.get();
|
||||
|
||||
this.formValues.forceHTTPS = !certInfo.httpEnabled;
|
||||
this.originalValues.forceHTTPS = this.formValues.forceHTTPS;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed loading certificate info');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SslCertificateController;
|
|
@ -0,0 +1,95 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-key" title-text="SSL certificate"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.sslForm">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Forcing HTTPs only will cause Portainer to stop listening on the HTTP port. Any edge agent endpoint that is using HTTP will no longer be available.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<por-switch-field ng-model="$ctrl.formValues.forceHTTPS" label="Force HTTPS only" on-change="($ctrl.onChangeForceHTTPS)"></por-switch-field>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12">
|
||||
Provide a new SSL Certificate to replace the existing one that is used for HTTPS connections.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12">
|
||||
Upload a X.509 certificate, commonly a crt, a cer, or a pem file.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.certFile" ngf-pattern="$ctrl.certFilePattern" name="certFile">
|
||||
Select file
|
||||
</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.formValues.certFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.certFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="$ctrl.sslForm.certFile.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="$ctrl.sslForm.certFile.$error">
|
||||
<p ng-message="pattern"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> File type is invalid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12">
|
||||
Upload a private key, commonly a key, or a pem file.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.keyFile" ngf-pattern="$ctrl.keyFilePattern" name="keyFile">
|
||||
Select file
|
||||
</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.formValues.keyFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.keyFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="$ctrl.sslForm.keyFile.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="$ctrl.sslForm.keyFile.$error">
|
||||
<p ng-message="pattern"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> File type is invalid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.isFormChanged()"
|
||||
ng-click="$ctrl.save()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.state.actionInProgress || $ctrl.state.reloadingPage">Apply Changes</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Saving in progress...</span>
|
||||
<span ng-show="$ctrl.state.reloadingPage">Reloading Page...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import generalModule from './general';
|
||||
|
||||
export default angular.module('portainer.settings', [generalModule]).name;
|
|
@ -44,20 +44,27 @@
|
|||
<label class="btn btn-primary" ng-model="state.platformType" uib-btn-radio="'windows'"><i class="fab fa-windows" style="margin-right: 2px;"></i> Windows</label>
|
||||
</div>
|
||||
</div>
|
||||
<por-switch-field
|
||||
label="Allow self-signed certs"
|
||||
ng-model="state.allowSelfSignedCerts"
|
||||
tooltip="When allowing self-signed certificates the edge agent will ignore the domain validation when connecting to Portainer via HTTPS"
|
||||
></por-switch-field>
|
||||
<div style="margin-top: 10px;">
|
||||
<uib-tabset active="state.deploymentTab">
|
||||
<uib-tab index="0" ng-if="state.platformType === 'linux'" heading="Kubernetes">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;"
|
||||
>curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }}</code
|
||||
>
|
||||
<uib-tab index="'kubernetes'" heading="Kubernetes" ng-if="state.platformType === 'linux'">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px;">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" heading="Docker Swarm">
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxSwarm }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsSwarm }}</code>
|
||||
<uib-tab index="'swarm'" heading="Docker Swarm">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px;">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" heading="Docker Standalone">
|
||||
<code ng-if="state.platformType === 'linux'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.linuxStandalone }}</code>
|
||||
<code ng-if="state.platformType === 'windows'" style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.windowsStandalone }}</code>
|
||||
<uib-tab index="'standalone'" heading="Docker Standalone">
|
||||
<code style="display: block; white-space: pre-wrap; padding: 16px 45px;">
|
||||
{{ dockerCommands[state.deploymentTab][state.platformType](randomEdgeID, endpoint.EdgeKey, state.allowSelfSignedCerts) }}
|
||||
</code>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<div style="margin-top: 10px;">
|
||||
|
|
|
@ -1,292 +1,290 @@
|
|||
import _ from 'lodash-es';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel';
|
||||
|
||||
angular
|
||||
.module('portainer.app')
|
||||
.controller('EndpointController', function EndpointController(
|
||||
$async,
|
||||
$q,
|
||||
$scope,
|
||||
$state,
|
||||
$transition$,
|
||||
$filter,
|
||||
clipboard,
|
||||
EndpointService,
|
||||
GroupService,
|
||||
TagService,
|
||||
EndpointProvider,
|
||||
Notifications,
|
||||
Authentication,
|
||||
SettingsService,
|
||||
ModalService
|
||||
) {
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false,
|
||||
deploymentTab: 0,
|
||||
azureEndpoint: false,
|
||||
kubernetesEndpoint: false,
|
||||
agentEndpoint: false,
|
||||
edgeEndpoint: false,
|
||||
platformType: 'linux',
|
||||
allowCreate: Authentication.isAdmin(),
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{ key: 'Use default interval', value: 0 },
|
||||
{
|
||||
key: '5 seconds',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
key: '10 seconds',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
key: '30 seconds',
|
||||
value: 30,
|
||||
},
|
||||
{ key: '5 minutes', value: 300 },
|
||||
{ key: '1 hour', value: 3600 },
|
||||
{ key: '1 day', value: 86400 },
|
||||
],
|
||||
};
|
||||
import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
||||
import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel';
|
||||
|
||||
$scope.formValues = {
|
||||
SecurityFormData: new EndpointSecurityFormData(),
|
||||
};
|
||||
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||
|
||||
$scope.copyEdgeAgentDeploymentCommand = function () {
|
||||
if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host -v portainer_agent_data:/data --restart always -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 2 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker run -d --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --name portainer_edge_agent portainer/agent'
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'linux') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
" -e CAP_HOST_MANAGEMENT=1 --mode global --constraint 'node.platform.os == linux' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=//,dst=/host --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent"
|
||||
);
|
||||
} else if ($scope.state.deploymentTab === 1 && $scope.state.platformType === 'windows') {
|
||||
clipboard.copyText(
|
||||
'docker network create --driver overlay portainer_edge_agent_network && docker service create --name portainer_edge_agent --network portainer_edge_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' +
|
||||
$scope.randomEdgeID +
|
||||
' -e EDGE_KEY=' +
|
||||
$scope.endpoint.EdgeKey +
|
||||
' -e CAP_HOST_MANAGEMENT=1 --mode global --constraint node.platform.os==windows --mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine --mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes --mount type=volume,src=portainer_agent_data,dst=C:\\data portainer/agent'
|
||||
);
|
||||
} else {
|
||||
clipboard.copyText('curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | bash -s -- ' + $scope.randomEdgeID + ' ' + $scope.endpoint.EdgeKey);
|
||||
}
|
||||
$('#copyNotificationDeploymentCommand').show().fadeOut(2500);
|
||||
};
|
||||
/* @ngInject */
|
||||
function EndpointController(
|
||||
$async,
|
||||
$q,
|
||||
$scope,
|
||||
$state,
|
||||
$transition$,
|
||||
$filter,
|
||||
clipboard,
|
||||
EndpointService,
|
||||
GroupService,
|
||||
TagService,
|
||||
EndpointProvider,
|
||||
Notifications,
|
||||
Authentication,
|
||||
SettingsService,
|
||||
ModalService
|
||||
) {
|
||||
const DEPLOYMENT_TABS = {
|
||||
SWARM: 'swarm',
|
||||
STANDALONE: 'standalone',
|
||||
KUBERNETES: 'kubernetes',
|
||||
};
|
||||
|
||||
$scope.copyEdgeAgentKey = function () {
|
||||
clipboard.copyText($scope.endpoint.EdgeKey);
|
||||
$('#copyNotificationEdgeKey').show().fadeOut(2500);
|
||||
};
|
||||
const PLATFORM_TYPES = {
|
||||
WINDOWS: 'windows',
|
||||
LINUX: 'linux',
|
||||
};
|
||||
|
||||
$scope.onCreateTag = function onCreateTag(tagName) {
|
||||
return $async(onCreateTagAsync, tagName);
|
||||
};
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false,
|
||||
deploymentTab: DEPLOYMENT_TABS.KUBERNETES,
|
||||
platformType: PLATFORM_TYPES.LINUX,
|
||||
azureEndpoint: false,
|
||||
kubernetesEndpoint: false,
|
||||
agentEndpoint: false,
|
||||
edgeEndpoint: false,
|
||||
allowCreate: Authentication.isAdmin(),
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{ key: 'Use default interval', value: 0 },
|
||||
{
|
||||
key: '5 seconds',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
key: '10 seconds',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
key: '30 seconds',
|
||||
value: 30,
|
||||
},
|
||||
{ key: '5 minutes', value: 300 },
|
||||
{ key: '1 hour', value: 3600 },
|
||||
{ key: '1 day', value: 86400 },
|
||||
],
|
||||
allowSelfSignedCerts: true,
|
||||
};
|
||||
|
||||
async function onCreateTagAsync(tagName) {
|
||||
try {
|
||||
const tag = await TagService.createTag(tagName);
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.endpoint.TagIds = $scope.endpoint.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failue', err, 'Unable to create tag');
|
||||
}
|
||||
$scope.dockerCommands = {
|
||||
[DEPLOYMENT_TABS.STANDALONE]: {
|
||||
[PLATFORM_TYPES.LINUX]: buildLinuxStandaloneCommand,
|
||||
[PLATFORM_TYPES.WINDOWS]: buildWindowsStandaloneCommand,
|
||||
},
|
||||
[DEPLOYMENT_TABS.SWARM]: {
|
||||
[PLATFORM_TYPES.LINUX]: buildLinuxSwarmCommand,
|
||||
[PLATFORM_TYPES.WINDOWS]: buildWindowsSwarmCommand,
|
||||
},
|
||||
[DEPLOYMENT_TABS.KUBERNETES]: {
|
||||
[PLATFORM_TYPES.LINUX]: buildKubernetesCommand,
|
||||
[PLATFORM_TYPES.WINDOWS]: () => '',
|
||||
},
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
SecurityFormData: new EndpointSecurityFormData(),
|
||||
};
|
||||
|
||||
$scope.copyEdgeAgentDeploymentCommand = copyEdgeAgentDeploymentCommand;
|
||||
function copyEdgeAgentDeploymentCommand() {
|
||||
const command = $scope.dockerCommands[$scope.state.deploymentTab][$scope.state.platformType]($scope.randomEdgeID, $scope.endpoint.EdgeKey, $scope.state.allowSelfSignedCerts);
|
||||
clipboard.copyText(command.trim());
|
||||
$('#copyNotificationDeploymentCommand').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
$scope.copyEdgeAgentKey = function () {
|
||||
clipboard.copyText($scope.endpoint.EdgeKey);
|
||||
$('#copyNotificationEdgeKey').show().fadeOut(2500);
|
||||
};
|
||||
|
||||
$scope.onCreateTag = function onCreateTag(tagName) {
|
||||
return $async(onCreateTagAsync, tagName);
|
||||
};
|
||||
|
||||
async function onCreateTagAsync(tagName) {
|
||||
try {
|
||||
const tag = await TagService.createTag(tagName);
|
||||
$scope.availableTags = $scope.availableTags.concat(tag);
|
||||
$scope.endpoint.TagIds = $scope.endpoint.TagIds.concat(tag.Id);
|
||||
} catch (err) {
|
||||
Notifications.error('Failue', err, 'Unable to create tag');
|
||||
}
|
||||
}
|
||||
|
||||
$scope.onDeassociateEndpoint = async function () {
|
||||
ModalService.confirmDeassociate((confirmed) => {
|
||||
if (confirmed) {
|
||||
deassociateEndpoint();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function deassociateEndpoint() {
|
||||
var endpoint = $scope.endpoint;
|
||||
|
||||
try {
|
||||
$scope.state.actionInProgress = true;
|
||||
await EndpointService.deassociateEndpoint(endpoint.Id);
|
||||
Notifications.success('Endpoint de-associated', $scope.endpoint.Name);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to de-associate endpoint');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
$scope.onDeassociateEndpoint = async function () {
|
||||
ModalService.confirmDeassociate((confirmed) => {
|
||||
if (confirmed) {
|
||||
deassociateEndpoint();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateEndpoint = function () {
|
||||
var endpoint = $scope.endpoint;
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
|
||||
var payload = {
|
||||
Name: endpoint.Name,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
GroupID: endpoint.GroupId,
|
||||
TagIds: endpoint.TagIds,
|
||||
EdgeCheckinInterval: endpoint.EdgeCheckinInterval,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
||||
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
||||
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
||||
AzureApplicationID: endpoint.AzureCredentials.ApplicationID,
|
||||
AzureTenantID: endpoint.AzureCredentials.TenantID,
|
||||
AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey,
|
||||
};
|
||||
|
||||
if (
|
||||
$scope.endpointType !== 'local' &&
|
||||
endpoint.Type !== PortainerEndpointTypes.AzureEnvironment &&
|
||||
endpoint.Type !== PortainerEndpointTypes.KubernetesLocalEnvironment &&
|
||||
endpoint.Type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment
|
||||
) {
|
||||
payload.URL = 'tcp://' + endpoint.URL;
|
||||
}
|
||||
|
||||
if (endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment) {
|
||||
payload.URL = endpoint.URL;
|
||||
}
|
||||
async function deassociateEndpoint() {
|
||||
var endpoint = $scope.endpoint;
|
||||
|
||||
try {
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.updateEndpoint(endpoint.Id, payload).then(
|
||||
function success() {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
$state.go('portainer.endpoints', {}, { reload: true });
|
||||
},
|
||||
function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
$scope.state.actionInProgress = false;
|
||||
},
|
||||
function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
}
|
||||
);
|
||||
await EndpointService.deassociateEndpoint(endpoint.Id);
|
||||
Notifications.success('Endpoint de-associated', $scope.endpoint.Name);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to de-associate endpoint');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.updateEndpoint = function () {
|
||||
var endpoint = $scope.endpoint;
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
var TLSMode = securityData.TLSMode;
|
||||
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||
|
||||
var payload = {
|
||||
Name: endpoint.Name,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
GroupID: endpoint.GroupId,
|
||||
TagIds: endpoint.TagIds,
|
||||
EdgeCheckinInterval: endpoint.EdgeCheckinInterval,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
||||
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
||||
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
||||
AzureApplicationID: endpoint.AzureCredentials.ApplicationID,
|
||||
AzureTenantID: endpoint.AzureCredentials.TenantID,
|
||||
AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey,
|
||||
};
|
||||
|
||||
function decodeEdgeKey(key) {
|
||||
let keyInformation = {};
|
||||
if (
|
||||
$scope.endpointType !== 'local' &&
|
||||
endpoint.Type !== PortainerEndpointTypes.AzureEnvironment &&
|
||||
endpoint.Type !== PortainerEndpointTypes.KubernetesLocalEnvironment &&
|
||||
endpoint.Type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment
|
||||
) {
|
||||
payload.URL = 'tcp://' + endpoint.URL;
|
||||
}
|
||||
|
||||
if (key === '') {
|
||||
return keyInformation;
|
||||
if (endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment) {
|
||||
payload.URL = endpoint.URL;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.updateEndpoint(endpoint.Id, payload).then(
|
||||
function success() {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
$state.go('portainer.endpoints', {}, { reload: true });
|
||||
},
|
||||
function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
$scope.state.actionInProgress = false;
|
||||
},
|
||||
function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let decodedKey = _.split(atob(key), '|');
|
||||
keyInformation.instanceURL = decodedKey[0];
|
||||
keyInformation.tunnelServerAddr = decodedKey[1];
|
||||
function decodeEdgeKey(key) {
|
||||
let keyInformation = {};
|
||||
|
||||
if (key === '') {
|
||||
return keyInformation;
|
||||
}
|
||||
|
||||
function configureState() {
|
||||
if (
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
|
||||
) {
|
||||
$scope.state.kubernetesEndpoint = true;
|
||||
}
|
||||
if ($scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
$scope.state.edgeEndpoint = true;
|
||||
}
|
||||
if ($scope.endpoint.Type === PortainerEndpointTypes.AzureEnvironment) {
|
||||
$scope.state.azureEndpoint = true;
|
||||
}
|
||||
if (
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnDockerEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
|
||||
) {
|
||||
$scope.state.agentEndpoint = true;
|
||||
}
|
||||
let decodedKey = _.split(atob(key), '|');
|
||||
keyInformation.instanceURL = decodedKey[0];
|
||||
keyInformation.tunnelServerAddr = decodedKey[1];
|
||||
|
||||
return keyInformation;
|
||||
}
|
||||
|
||||
function configureState() {
|
||||
if (
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
|
||||
) {
|
||||
$scope.state.kubernetesEndpoint = true;
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||
groups: GroupService.groups(),
|
||||
tags: TagService.tags(),
|
||||
settings: SettingsService.settings(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoint = data.endpoint;
|
||||
if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
$scope.endpointType = 'remote';
|
||||
}
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
$scope.dockerCommands = {
|
||||
linuxStandalone: buildLinuxStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsStandalone: buildWindowsStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
linuxSwarm: buildLinuxSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
windowsSwarm: buildWindowsSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey),
|
||||
};
|
||||
|
||||
const settings = data.settings;
|
||||
$scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`;
|
||||
}
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.groups = data.groups;
|
||||
$scope.availableTags = data.tags;
|
||||
configureState();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
});
|
||||
if ($scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
$scope.state.edgeEndpoint = true;
|
||||
}
|
||||
|
||||
function buildLinuxStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent`;
|
||||
if ($scope.endpoint.Type === PortainerEndpointTypes.AzureEnvironment) {
|
||||
$scope.state.azureEndpoint = true;
|
||||
}
|
||||
if (
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnDockerEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ||
|
||||
$scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment
|
||||
) {
|
||||
$scope.state.agentEndpoint = true;
|
||||
}
|
||||
}
|
||||
|
||||
function buildWindowsStandaloneCommand(edgeId, edgeKey) {
|
||||
return `docker run -d \\
|
||||
async function initView() {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const [endpoint, groups, tags, settings] = await Promise.all([
|
||||
EndpointService.endpoint($transition$.params().id),
|
||||
GroupService.groups(),
|
||||
TagService.tags(),
|
||||
SettingsService.settings(),
|
||||
]);
|
||||
|
||||
if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
$scope.endpointType = 'remote';
|
||||
}
|
||||
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) {
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
|
||||
$scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`;
|
||||
}
|
||||
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.groups = groups;
|
||||
$scope.availableTags = tags;
|
||||
|
||||
configureState();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildLinuxStandaloneCommand(edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \\
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \\
|
||||
-v /:/host \\
|
||||
-v portainer_agent_data:/data \\
|
||||
--restart always \\
|
||||
-e EDGE=1 \\
|
||||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent:2.4.0`;
|
||||
}
|
||||
|
||||
function buildWindowsStandaloneCommand(edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker run -d \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
|
@ -295,12 +293,14 @@ angular
|
|||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--name portainer_edge_agent \\
|
||||
portainer/agent`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinuxSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
function buildLinuxSwarmCommand(edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_agent_network;
|
||||
|
||||
|
@ -312,6 +312,7 @@ docker service create \\
|
|||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--mode global \\
|
||||
--constraint 'node.platform.os == linux' \\
|
||||
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
|
||||
|
@ -319,10 +320,11 @@ docker service create \\
|
|||
--mount type=bind,src=//,dst=/host \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=/data \\
|
||||
portainer/agent`;
|
||||
}
|
||||
}
|
||||
|
||||
function buildWindowsSwarmCommand(edgeId, edgeKey) {
|
||||
return `docker network create \\
|
||||
function buildWindowsSwarmCommand(edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
docker network create \\
|
||||
--driver overlay \\
|
||||
portainer_edge_agent_network && \\
|
||||
docker service create \\
|
||||
|
@ -333,13 +335,20 @@ docker service create \\
|
|||
-e EDGE_ID=${edgeId} \\
|
||||
-e EDGE_KEY=${edgeKey} \\
|
||||
-e CAP_HOST_MANAGEMENT=1 \\
|
||||
-e EDGE_INSECURE_POLL=${allowSelfSignedCerts ? 1 : 0} \\
|
||||
--mode global \\
|
||||
--constraint node.platform.os==windows \\
|
||||
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||
--mount type=volume,src=portainer_agent_data,dst=C:\\data \\
|
||||
portainer/agent`;
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
});
|
||||
function buildKubernetesCommand(edgeId, edgeKey, allowSelfSignedCerts) {
|
||||
return `
|
||||
curl https://downloads.portainer.io/portainer-ce29-edge-agent-setup.sh | bash -s -- ${edgeId} ${edgeKey} ${allowSelfSignedCerts ? '1' : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
||||
|
|
|
@ -131,6 +131,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ssl-certificate-settings></ssl-certificate-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
|
|
|
@ -189,10 +189,10 @@
|
|||
</li>
|
||||
<li class="sidebar-list" ng-if="isAdmin">
|
||||
<a ui-sref="portainer.settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.settings' || $state.current.name === 'portainer.settings.authentication') && isAdmin">
|
||||
<a ui-sref="portainer.settings.authentication" ui-sref-active="active">Authentication</a></div
|
||||
>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.settings' || $state.current.name === 'portainer.settings.authentication')">
|
||||
<div class="sidebar-sublist" ng-if="toggle && $state.current.name.startsWith('portainer.settings') && isAdmin">
|
||||
<a ui-sref="portainer.settings.authentication" ui-sref-active="active">Authentication</a>
|
||||
</div>
|
||||
<div class="sidebar-sublist" ng-if="toggle && $state.current.name.startsWith('portainer.settings')">
|
||||
<a href="http://www.portainer.io/help_about" target="_blank">Help / About</a>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -78,6 +78,7 @@ module.exports = {
|
|||
'/api': 'http://localhost:9000',
|
||||
},
|
||||
open: true,
|
||||
writeToDisk: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
|
|
Loading…
Reference in New Issue