From 19eceaf37fc8c479d84771dfb70471e68c4b5b22 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Thu, 4 May 2023 09:44:11 +1200 Subject: [PATCH] fix(restore/swarm): init primary endpoint after admin user is created (#8854) --- api/cmd/portainer/main.go | 155 +--------------- api/go.mod | 8 +- api/go.sum | 22 ++- api/http/handler/users/admin_init.go | 3 + api/http/handler/users/handler.go | 1 + api/http/server.go | 1 + api/internal/endpointutils/endpoint_setup.go | 184 +++++++++++++++++++ 7 files changed, 222 insertions(+), 152 deletions(-) create mode 100644 api/internal/endpointutils/endpoint_setup.go diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index c3479cac4..f2f83bae9 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -29,12 +29,12 @@ import ( "github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/hostmanagement/openamt" "github.com/portainer/portainer/api/http" - "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/http/proxy" kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/edge" "github.com/portainer/portainer/api/internal/edge/edgestacks" + "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/ssl" "github.com/portainer/portainer/api/internal/upgrade" @@ -346,147 +346,6 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D return generateAndStoreKeyPair(fileService, signatureService) } -func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { - tlsConfiguration := portainer.TLSConfiguration{ - TLS: *flags.TLS, - TLSSkipVerify: *flags.TLSSkipVerify, - } - - if *flags.TLS { - tlsConfiguration.TLSCACertPath = *flags.TLSCacert - tlsConfiguration.TLSCertPath = *flags.TLSCert - tlsConfiguration.TLSKeyPath = *flags.TLSKey - } else if !*flags.TLS && *flags.TLSSkipVerify { - tlsConfiguration.TLS = true - } - - endpointID := dataStore.Endpoint().GetNextIdentifier() - endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: *flags.EndpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: tlsConfiguration, - UserAccessPolicies: portainer.UserAccessPolicies{}, - TeamAccessPolicies: portainer.TeamAccessPolicies{}, - TagIDs: []portainer.TagID{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.DockerSnapshot{}, - Kubernetes: portainer.KubernetesDefault(), - - SecuritySettings: portainer.EndpointSecuritySettings{ - AllowVolumeBrowserForRegularUsers: false, - EnableHostManagementFeatures: false, - - AllowSysctlSettingForRegularUsers: true, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowHostNamespaceForRegularUsers: true, - AllowContainerCapabilitiesForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - }, - } - - if strings.HasPrefix(endpoint.URL, "tcp://") { - tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify) - if err != nil { - return err - } - - agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig) - if err != nil { - return err - } - - if agentOnDockerEnvironment { - endpoint.Type = portainer.AgentOnDockerEnvironment - } - } - - err := snapshotService.SnapshotEndpoint(endpoint) - if err != nil { - log.Error(). - Str("endpoint", endpoint.Name). - Str("URL", endpoint.URL). - Err(err). - Msg("environment snapshot error") - } - - return dataStore.Endpoint().Create(endpoint) -} - -func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { - if strings.HasPrefix(endpointURL, "tcp://") { - _, err := client.ExecutePingOperation(endpointURL, nil) - if err != nil { - return err - } - } - - endpointID := dataStore.Endpoint().GetNextIdentifier() - endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: endpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: portainer.TLSConfiguration{}, - UserAccessPolicies: portainer.UserAccessPolicies{}, - TeamAccessPolicies: portainer.TeamAccessPolicies{}, - TagIDs: []portainer.TagID{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.DockerSnapshot{}, - Kubernetes: portainer.KubernetesDefault(), - - SecuritySettings: portainer.EndpointSecuritySettings{ - AllowVolumeBrowserForRegularUsers: false, - EnableHostManagementFeatures: false, - - AllowSysctlSettingForRegularUsers: true, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowHostNamespaceForRegularUsers: true, - AllowContainerCapabilitiesForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - }, - } - - err := snapshotService.SnapshotEndpoint(endpoint) - if err != nil { - log.Error(). - Str("endpoint", endpoint.Name). - Str("URL", endpoint.URL).Err(err). - Msg("environment snapshot error") - } - - return dataStore.Endpoint().Create(endpoint) -} - -func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { - if *flags.EndpointURL == "" { - return nil - } - - endpoints, err := dataStore.Endpoint().Endpoints() - if err != nil { - return err - } - - if len(endpoints) > 0 { - log.Info().Msg("instance already has defined environments, skipping the environment defined via CLI") - - return nil - } - - if *flags.TLS || *flags.TLSSkipVerify { - return createTLSSecuredEndpoint(flags, dataStore, snapshotService) - } - return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService) -} - func loadEncryptionSecretKey(keyfilename string) []byte { content, err := os.ReadFile(path.Join("/run/secrets", keyfilename)) if err != nil { @@ -627,10 +486,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { } } - err = initEndpoint(flags, dataStore, snapshotService) - if err != nil { - log.Fatal().Err(err).Msg("failed initializing environment") - } + // channel to control when the admin user is created + adminCreationDone := make(chan struct{}, 1) + + go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService) adminPasswordHash := "" if *flags.AdminPasswordFile != "" { @@ -665,6 +524,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { if err != nil { log.Fatal().Err(err).Msg("failed creating admin user") } + + // notify the admin user is created, the endpoint initialization can start + adminCreationDone <- struct{}{} } else { log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.") } @@ -738,6 +600,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { StackDeployer: stackDeployer, DemoService: demoService, UpgradeService: upgradeService, + AdminCreationDone: adminCreationDone, } } diff --git a/api/go.mod b/api/go.mod index 79db8f2d5..281571718 100644 --- a/api/go.mod +++ b/api/go.mod @@ -45,11 +45,12 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.29.0 github.com/stretchr/testify v1.8.1 + github.com/swaggo/swag v1.16.1 github.com/viney-shih/go-lock v1.1.1 go.etcd.io/bbolt v1.3.6 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + golang.org/x/sync v0.1.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.26.1 @@ -60,6 +61,7 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect @@ -83,7 +85,8 @@ require ( github.com/go-logr/logr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -129,6 +132,7 @@ require ( golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/api/go.sum b/api/go.sum index 27fd18d1f..2badfd8da 100644 --- a/api/go.sum +++ b/api/go.sum @@ -37,12 +37,16 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzU github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= @@ -160,11 +164,14 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -396,6 +403,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= +github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/viney-shih/go-lock v1.1.1 h1:SwzDPPAiHpcwGCr5k8xD15d2gQSo8d4roRYd7TDV2eI= @@ -464,6 +473,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -494,6 +504,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= @@ -514,8 +525,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -551,6 +562,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -624,6 +636,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index c40c16394..9e56c7f46 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -76,5 +76,8 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe return httperror.InternalServerError("Unable to persist user inside the database", err) } + // After the admin user is created, we can notify the endpoint initialization process + handler.AdminCreationDone <- struct{}{} + return response.JSON(w, user) } diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index 03cf3fd9e..d6287ad10 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -37,6 +37,7 @@ type Handler struct { DataStore dataservices.DataStore CryptoService portainer.CryptoService passwordStrengthChecker security.PasswordStrengthChecker + AdminCreationDone chan<- struct{} } // NewHandler creates a handler to manage user operations. diff --git a/api/http/server.go b/api/http/server.go index 956ad1d13..f2bd79b83 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -106,6 +106,7 @@ type Server struct { StackDeployer deployments.StackDeployer DemoService *demo.Service UpgradeService upgrade.Service + AdminCreationDone chan struct{} } // Start starts the HTTP server diff --git a/api/internal/endpointutils/endpoint_setup.go b/api/internal/endpointutils/endpoint_setup.go new file mode 100644 index 000000000..5b0205cd5 --- /dev/null +++ b/api/internal/endpointutils/endpoint_setup.go @@ -0,0 +1,184 @@ +package endpointutils + +import ( + "context" + "strings" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/http/client" + "github.com/rs/zerolog/log" +) + +// InitEndpoint controls the workflow to initialize the primary endpoint. +// When installing Portainer in a Docker Cluster using the yaml file provided in the official +// documentation (https://docs.portainer.io/start/install/server/swarm/linux), the "primary" +// endpoint is initialized before the admin user is created. This triggers the creation of a +// snapshot of the environment in the background, which includes the agent saving the signature +// from the first request made by the server. However, if a user restores Portainer from a backup +// instead of creating a new admin user, the server will not be able to connect to the agent because +// the saved signature will not match. To solve this issue, this solution proposes to wait for +// the admin user to be created before initializing the primary endpoint. This way, the agent +// will save the signature from the first request after the admin user is created, ensuring that +// it matches in the event of a backup restoration. +func InitEndpoint(shutdownCtx context.Context, adminCreationDone <-chan struct{}, flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) { + + select { + case <-shutdownCtx.Done(): + log.Debug().Msg("shutdown endpoint initalization") + + case <-adminCreationDone: + // Wait for the admin user to be created before initializing the primary endpoint + // The admin user can be created in two ways: + // 1. Using the CLI with the --admin-password flag + // 2. Using the API with the /api/users/admin/init endpoint + log.Debug().Msg("init primary endpoint") + + err := initEndpoint(flags, dataStore, snapshotService) + if err != nil { + log.Fatal().Err(err).Msg("failed initializing environment") + } + } +} + +func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { + if *flags.EndpointURL == "" { + return nil + } + + endpoints, err := dataStore.Endpoint().Endpoints() + if err != nil { + return err + } + + if len(endpoints) > 0 { + log.Info().Msg("instance already has defined environments, skipping the environment defined via CLI") + + return nil + } + + if *flags.TLS || *flags.TLSSkipVerify { + return createTLSSecuredEndpoint(flags, dataStore, snapshotService) + } + return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService) +} + +func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { + tlsConfiguration := portainer.TLSConfiguration{ + TLS: *flags.TLS, + TLSSkipVerify: *flags.TLSSkipVerify, + } + + if *flags.TLS { + tlsConfiguration.TLSCACertPath = *flags.TLSCacert + tlsConfiguration.TLSCertPath = *flags.TLSCert + tlsConfiguration.TLSKeyPath = *flags.TLSKey + } else if !*flags.TLS && *flags.TLSSkipVerify { + tlsConfiguration.TLS = true + } + + endpointID := dataStore.Endpoint().GetNextIdentifier() + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: *flags.EndpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: tlsConfiguration, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + TagIDs: []portainer.TagID{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowSysctlSettingForRegularUsers: true, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, + } + + if strings.HasPrefix(endpoint.URL, "tcp://") { + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify) + if err != nil { + return err + } + + agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig) + if err != nil { + return err + } + + if agentOnDockerEnvironment { + endpoint.Type = portainer.AgentOnDockerEnvironment + } + } + + err := snapshotService.SnapshotEndpoint(endpoint) + if err != nil { + log.Error(). + Str("endpoint", endpoint.Name). + Str("URL", endpoint.URL). + Err(err). + Msg("environment snapshot error") + } + + return dataStore.Endpoint().Create(endpoint) +} + +func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error { + if strings.HasPrefix(endpointURL, "tcp://") { + _, err := client.ExecutePingOperation(endpointURL, nil) + if err != nil { + return err + } + } + + endpointID := dataStore.Endpoint().GetNextIdentifier() + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: endpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: portainer.TLSConfiguration{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + TagIDs: []portainer.TagID{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), + + SecuritySettings: portainer.EndpointSecuritySettings{ + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + + AllowSysctlSettingForRegularUsers: true, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowHostNamespaceForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + }, + } + + err := snapshotService.SnapshotEndpoint(endpoint) + if err != nil { + log.Error(). + Str("endpoint", endpoint.Name). + Str("URL", endpoint.URL).Err(err). + Msg("environment snapshot error") + } + + return dataStore.Endpoint().Create(endpoint) +}