mirror of https://github.com/portainer/portainer
feat(system): path to upgrade standalone to BE [EE-4071] (#8095)
parent
756ac034ec
commit
5cbf52377d
|
@ -105,6 +105,11 @@ overrides:
|
|||
'no-await-in-loop': 'off'
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
||||
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
|
||||
overrides: # allow props spreading for hoc files
|
||||
- files:
|
||||
- app/**/with*.ts{,x}
|
||||
rules:
|
||||
'react/jsx-props-no-spreading': off
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
|
@ -35,6 +37,7 @@ import (
|
|||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
@ -147,8 +150,8 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
|||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
|
||||
func initComposeStackManager(composeDeployer libstack.Deployer, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating compose manager")
|
||||
}
|
||||
|
@ -629,7 +632,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing compose deployer")
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(composeDeployer, reverseTunnelService, proxyManager)
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
||||
if err != nil {
|
||||
|
@ -715,6 +723,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||
}
|
||||
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
// FIXME: In 2.16 we changed the way ingress controller permissions are
|
||||
// stored. Instead of being stored as annotation on an ingress rule, we keep
|
||||
// them in our database. However, in order to run the migration we need an
|
||||
|
@ -767,6 +780,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
UpgradeService: upgradeService,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"strings"
|
||||
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
|
@ -25,11 +24,7 @@ type ComposeStackManager struct {
|
|||
}
|
||||
|
||||
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeStackManager(binaryPath string, configPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
||||
deployer, err := compose.NewComposeDeployer(binaryPath, configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
||||
|
||||
return &ComposeStackManager{
|
||||
deployer: deployer,
|
||||
|
@ -43,7 +38,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
|||
}
|
||||
|
||||
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
|
||||
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
|
||||
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error {
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch environment proxy")
|
||||
|
@ -53,13 +48,21 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFile, err := createEnvFile(stack)
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
|
||||
err = manager.deployer.Deploy(ctx, filePaths, libstack.DeployOptions{
|
||||
Options: libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
},
|
||||
ForceRecreate: forceRecreate,
|
||||
})
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
|
||||
|
@ -73,14 +76,19 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFile, err := createEnvFile(stack)
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
err = manager.deployer.Remove(ctx, filePaths, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
})
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
|
@ -95,13 +103,18 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFile, err := createEnvFile(stack)
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
err = manager.deployer.Pull(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
err = manager.deployer.Pull(ctx, filePaths, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
})
|
||||
return errors.Wrap(err, "failed to pull images of the stack")
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
|
||||
|
@ -47,7 +48,12 @@ func Test_UpAndDown(t *testing.T) {
|
|||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w, err := NewComposeStackManager("", "", nil)
|
||||
deployer, err := compose.NewComposeDeployer("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := NewComposeStackManager(deployer, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating manager: %s", err)
|
||||
}
|
||||
|
|
|
@ -489,14 +489,7 @@ func (service *Service) createDirectoryInStore(name string) error {
|
|||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := service.wrapFileStore(filePath)
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
return CreateFile(path, r)
|
||||
}
|
||||
|
||||
// createBackupFileInStore makes a copy in the file store.
|
||||
|
@ -762,3 +755,15 @@ func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string
|
|||
|
||||
return service.wrapFileStore(filePath), nil
|
||||
}
|
||||
|
||||
func CreateFile(path string, r io.Reader) error {
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2 v1.11.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
|
@ -34,14 +35,14 @@ require (
|
|||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221122145319-915b021aea84
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a
|
||||
github.com/portainer/libhttp v0.0.0-20221121135534-76f46e09c9a9
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/stretchr/testify v1.8.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
|
||||
|
|
20
api/go.sum
20
api/go.sum
|
@ -75,6 +75,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.6.1/go.mod h1:/73aFBwUl60wKBKhdth2pE
|
|||
github.com/aws/aws-sdk-go-v2/service/sts v1.10.1/go.mod h1:+BmlPeQ1Y+PuIho93MMKDby12PoUnt1SZXQdEHCzSlw=
|
||||
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU=
|
||||
github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
|
@ -155,8 +157,8 @@ github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE
|
|||
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-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
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=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
|
@ -334,16 +336,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 h1:GFTn2e5AyIoBuK6hXbdVNkuV2m450DQnYmgQDZRU3x8=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221122145319-915b021aea84 h1:d1P8i0pCPvAfxH6nSLUFm6NYoi8tMrIpafaZXSV8Lac=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221122145319-915b021aea84/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a h1:BJ5V4EDNhg3ImYbmXnGS8vrMhq6rzsEneIXyJh0g4dc=
|
||||
github.com/portainer/libhttp v0.0.0-20220916153711-5d61e12f4b0a/go.mod h1:ckuHnoLA5kLuE5WkvPBXmrw63LUMdSH4aX71QRi9y10=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73 h1:7bPOnwucE0nor0so1BQJxQKCL5t+vCWO4nAz/S0lci0=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73/go.mod h1:E2w/A6qsKuG2VyiUubPdXpDyPykWfQqxuCs0YNS0MhM=
|
||||
github.com/portainer/libhttp v0.0.0-20221121135534-76f46e09c9a9 h1:L7o0L+1qq+LzKjzgRB6bDIh5ZrZ5A1oSS+WgWzDgJIo=
|
||||
github.com/portainer/libhttp v0.0.0-20221121135534-76f46e09c9a9/go.mod h1:H49JLiywwLt2rrJVroafEWy8fIs0i7mThAThK40sbb8=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724 h1:FZrRVMpxXdUV+p5VSCAy9Uz7RzAeEJr2ytlctvMrsHY=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724/go.mod h1:WUdwNVH9GMffP4qf4U2ea2qCYfti2V7S+IhGpO8Sxv0=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73 h1:7bPOnwucE0nor0so1BQJxQKCL5t+vCWO4nAz/S0lci0=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73/go.mod h1:E2w/A6qsKuG2VyiUubPdXpDyPykWfQqxuCs0YNS0MhM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
|
@ -368,6 +370,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
|
@ -375,8 +378,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
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/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=
|
||||
|
|
|
@ -29,8 +29,8 @@ import (
|
|||
"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/storybook"
|
||||
"github.com/portainer/portainer/api/http/handler/system"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
|
@ -69,8 +69,8 @@ type Handler struct {
|
|||
OpenAMTHandler *openamt.Handler
|
||||
FDOHandler *fdo.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
SystemHandler *system.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
|
@ -133,8 +133,6 @@ type Handler struct {
|
|||
// @tag.description Manage roles
|
||||
// @tag.name settings
|
||||
// @tag.description Manage Portainer settings
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name tags
|
||||
|
@ -155,6 +153,10 @@ type Handler struct {
|
|||
// @tag.description Manage webhooks
|
||||
// @tag.name websocket
|
||||
// @tag.description Create exec sessions using websockets
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name system
|
||||
// @tag.description Manage Portainer system
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -218,7 +220,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
|
||||
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
http.StripPrefix("/api", h.SystemHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/system"):
|
||||
http.StripPrefix("/api", h.SystemHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/tags"):
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates/helm"):
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle status operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
status *portainer.Status
|
||||
dataStore dataservices.DataStore
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demoService *demo.Service, dataStore dataservices.DataStore) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
demoService: demoService,
|
||||
status: status,
|
||||
}
|
||||
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
|
||||
h.Handle("/status/nodes",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.statusNodesCount))).Methods(http.MethodGet)
|
||||
h.Handle("/status/system",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.statusSystem))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
)
|
||||
|
||||
type status struct {
|
||||
*portainer.Status
|
||||
DemoEnvironment demo.EnvironmentDetails
|
||||
}
|
||||
|
||||
// @id StatusInspect
|
||||
// @summary Check Portainer status
|
||||
// @description Retrieve Portainer status
|
||||
// @description **Access policy**: public
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /status [get]
|
||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, &status{
|
||||
Status: handler.status,
|
||||
DemoEnvironment: handler.demoService.Details(),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle status operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
status *portainer.Status
|
||||
dataStore dataservices.DataStore
|
||||
demoService *demo.Service
|
||||
upgradeService upgrade.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer,
|
||||
status *portainer.Status,
|
||||
demoService *demo.Service,
|
||||
dataStore dataservices.DataStore,
|
||||
upgradeService upgrade.Service) *Handler {
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
demoService: demoService,
|
||||
status: status,
|
||||
upgradeService: upgradeService,
|
||||
}
|
||||
|
||||
router := h.PathPrefix("/system").Subrouter()
|
||||
|
||||
adminRouter := router.PathPrefix("/").Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
|
||||
adminRouter.Handle("/upgrade", httperror.LoggerHandler(h.systemUpgrade)).Methods(http.MethodPost)
|
||||
|
||||
authenticatedRouter := router.PathPrefix("/").Subrouter()
|
||||
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
authenticatedRouter.Handle("/version", http.HandlerFunc(h.version)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/nodes", httperror.LoggerHandler(h.systemNodesCount)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/info", httperror.LoggerHandler(h.systemInfo)).Methods(http.MethodGet)
|
||||
|
||||
publicRouter := router.PathPrefix("/").Subrouter()
|
||||
publicRouter.Use(bouncer.PublicAccess)
|
||||
|
||||
publicRouter.Handle("/status", httperror.LoggerHandler(h.systemStatus)).Methods(http.MethodGet)
|
||||
|
||||
// Deprecated /status endpoint, will be removed in the future.
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspectDeprecated))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.versionDeprecated))).Methods(http.MethodGet)
|
||||
h.Handle("/status/nodes",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.statusNodesCountDeprecated))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package status
|
||||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -7,23 +7,24 @@ import (
|
|||
"github.com/portainer/libhttp/response"
|
||||
statusutil "github.com/portainer/portainer/api/internal/nodes"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type nodesCountResponse struct {
|
||||
Nodes int `json:"nodes"`
|
||||
}
|
||||
|
||||
// @id statusNodesCount
|
||||
// @id systemNodesCount
|
||||
// @summary Retrieve the count of nodes
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} nodesCountResponse "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /status/nodes [get]
|
||||
func (handler *Handler) statusNodesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// @router /system/nodes [get]
|
||||
func (handler *Handler) systemNodesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to get environment list", err)
|
||||
|
@ -40,3 +41,21 @@ func (handler *Handler) statusNodesCount(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
return response.JSON(w, &nodesCountResponse{Nodes: nodes})
|
||||
}
|
||||
|
||||
// @id statusNodesCount
|
||||
// @summary Retrieve the count of nodes
|
||||
// @deprecated
|
||||
// @description Deprecated: use the `/system/nodes` endpoint instead.
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} nodesCountResponse "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /status/nodes [get]
|
||||
func (handler *Handler) statusNodesCountDeprecated(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
log.Warn().Msg("The /status/nodes endpoint is deprecated, please use the /system/nodes endpoint instead")
|
||||
|
||||
return handler.systemNodesCount(w, r)
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type status struct {
|
||||
*portainer.Status
|
||||
DemoEnvironment demo.EnvironmentDetails
|
||||
}
|
||||
|
||||
// @id systemStatus
|
||||
// @summary Check Portainer status
|
||||
// @description Retrieve Portainer status
|
||||
// @description **Access policy**: public
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /system/status [get]
|
||||
func (handler *Handler) systemStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, &status{
|
||||
Status: handler.status,
|
||||
DemoEnvironment: handler.demoService.Details(),
|
||||
})
|
||||
}
|
||||
|
||||
// swagger docs for deprecated route:
|
||||
// @id StatusInspect
|
||||
// @summary Check Portainer status
|
||||
// @deprecated
|
||||
// @description Deprecated: use the `/system/status` endpoint instead.
|
||||
// @description Retrieve Portainer status
|
||||
// @description **Access policy**: public
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /status [get]
|
||||
func (handler *Handler) statusInspectDeprecated(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
log.Warn().Msg("The /status endpoint is deprecated and will be removed in a future version of Portainer. Please use the /system/status endpoint instead.")
|
||||
|
||||
return handler.systemStatus(w, r)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package status
|
||||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
plf "github.com/portainer/portainer/api/platform"
|
||||
)
|
||||
|
||||
type systemInfoResponse struct {
|
||||
|
@ -16,17 +17,17 @@ type systemInfoResponse struct {
|
|||
Agents int `json:"agents"`
|
||||
}
|
||||
|
||||
// @id statusSystem
|
||||
// @id systemInfo
|
||||
// @summary Retrieve system info
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} systemInfoResponse "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /status/system [get]
|
||||
func (handler *Handler) statusSystem(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// @router /system/info [get]
|
||||
func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
environments, err := handler.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to get environment list", err)
|
||||
|
@ -48,13 +49,17 @@ func (handler *Handler) statusSystem(w http.ResponseWriter, r *http.Request) *ht
|
|||
if environment.IsEdgeDevice {
|
||||
edgeDevices++
|
||||
}
|
||||
}
|
||||
|
||||
platform, err := plf.DetermineContainerPlatform()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to determine container platform", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, &systemInfoResponse{
|
||||
EdgeAgents: edgeAgents,
|
||||
EdgeDevices: edgeDevices,
|
||||
Agents: agents,
|
||||
Platform: platform.DetermineContainerPlatform(),
|
||||
Platform: platform,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type systemUpgradePayload struct {
|
||||
License string
|
||||
}
|
||||
|
||||
var re = regexp.MustCompile(`^\d-.+`)
|
||||
|
||||
func (payload *systemUpgradePayload) Validate(r *http.Request) error {
|
||||
if payload.License == "" {
|
||||
return errors.New("license is missing")
|
||||
}
|
||||
|
||||
if !re.MatchString(payload.License) {
|
||||
return errors.New("license is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id systemUpgrade
|
||||
// @summary Upgrade Portainer to BE
|
||||
// @description Upgrade Portainer to BE
|
||||
// @description **Access policy**: administrator
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /system/upgrade [post]
|
||||
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload, err := request.GetPayload[systemUpgradePayload](r)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = handler.upgradeService.Upgrade(payload.License)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to upgrade Portainer")
|
||||
}
|
||||
}()
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package status
|
||||
package system
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -33,16 +33,16 @@ type BuildInfo struct {
|
|||
GoVersion string
|
||||
}
|
||||
|
||||
// @id Version
|
||||
// @id systemVersion
|
||||
// @summary Check for portainer updates
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} versionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
// @router /system/version [get]
|
||||
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result := &versionResponse{
|
||||
|
@ -106,3 +106,21 @@ func HasNewerVersion(currentVersion, latestVersion string) bool {
|
|||
|
||||
return currentVersionSemver.LessThan(*latestVersionSemver)
|
||||
}
|
||||
|
||||
// @id Version
|
||||
// @summary Check for portainer updates
|
||||
// @deprecated
|
||||
// @description Deprecated: use the `/system/version` endpoint instead.
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} versionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
func (handler *Handler) versionDeprecated(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warn().Msg("The /status/version endpoint is deprecated, please use the /system/version endpoint instead")
|
||||
|
||||
handler.version(w, r)
|
||||
}
|
|
@ -40,8 +40,8 @@ import (
|
|||
"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/storybook"
|
||||
"github.com/portainer/portainer/api/http/handler/system"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
|
@ -57,6 +57,7 @@ import (
|
|||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
edgestackservice "github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
k8s "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
|
@ -103,6 +104,7 @@ type Server struct {
|
|||
ShutdownTrigger context.CancelFunc
|
||||
StackDeployer deployments.StackDeployer
|
||||
DemoService *demo.Service
|
||||
UpgradeService upgrade.Service
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -251,7 +253,11 @@ func (server *Server) Start() error {
|
|||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status, server.DemoService, server.DataStore)
|
||||
var systemHandler = system.NewHandler(requestBouncer,
|
||||
server.Status,
|
||||
server.DemoService,
|
||||
server.DataStore,
|
||||
server.UpgradeService)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.DataStore = server.DataStore
|
||||
|
@ -301,9 +307,9 @@ func (server *Server) Start() error {
|
|||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
SSLHandler: sslHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
StorybookHandler: storybookHandler,
|
||||
SystemHandler: systemHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cbroglie/mustache"
|
||||
"github.com/pkg/errors"
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// mustacheUpgradeStandaloneTemplateFile represents the name of the template file for the standalone upgrade
|
||||
mustacheUpgradeStandaloneTemplateFile = "upgrade-standalone.yml.mustache"
|
||||
|
||||
// portainerImagePrefixEnvVar represents the name of the environment variable used to define the image prefix for portainer-updater
|
||||
// useful if there's a need to test PR images
|
||||
portainerImagePrefixEnvVar = "UPGRADE_PORTAINER_IMAGE_PREFIX"
|
||||
// skipPullImageEnvVar represents the name of the environment variable used to define if the image pull should be skipped
|
||||
// useful if there's a need to test local images
|
||||
skipPullImageEnvVar = "UPGRADE_SKIP_PULL_PORTAINER_IMAGE"
|
||||
// updaterImageEnvVar represents the name of the environment variable used to define the updater image
|
||||
// useful if there's a need to test a different updater
|
||||
updaterImageEnvVar = "UPGRADE_UPDATER_IMAGE"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Upgrade(licenseKey string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
composeDeployer libstack.Deployer
|
||||
isUpdating bool
|
||||
platform platform.ContainerPlatform
|
||||
assetsPath string
|
||||
}
|
||||
|
||||
func NewService(assetsPath string, composeDeployer libstack.Deployer) (Service, error) {
|
||||
platform, err := platform.DetermineContainerPlatform()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to determine container platform")
|
||||
}
|
||||
|
||||
return &service{
|
||||
assetsPath: assetsPath,
|
||||
composeDeployer: composeDeployer,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *service) Upgrade(licenseKey string) error {
|
||||
service.isUpdating = true
|
||||
|
||||
switch service.platform {
|
||||
case platform.PlatformDockerStandalone:
|
||||
return service.UpgradeDockerStandalone(licenseKey, portainer.APIVersion)
|
||||
// case platform.PlatformDockerSwarm:
|
||||
// case platform.PlatformKubernetes:
|
||||
// case platform.PlatformPodman:
|
||||
// case platform.PlatformNomad:
|
||||
// default:
|
||||
}
|
||||
|
||||
return errors.New("unsupported platform")
|
||||
}
|
||||
|
||||
func (service *service) UpgradeDockerStandalone(licenseKey, version string) error {
|
||||
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeStandaloneTemplateFile)
|
||||
|
||||
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||
if portainerImagePrefix == "" {
|
||||
portainerImagePrefix = "portainer/portainer-ee"
|
||||
}
|
||||
|
||||
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||
|
||||
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
||||
|
||||
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
||||
"image": image,
|
||||
"skip_pull_image": skipPullImage,
|
||||
"updater_image": os.Getenv(updaterImageEnvVar),
|
||||
"license": licenseKey,
|
||||
})
|
||||
|
||||
log.Debug().
|
||||
Str("composeFile", composeFile).
|
||||
Msg("Compose file for upgrade")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to render upgrade template")
|
||||
}
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", time.Now().Unix()))
|
||||
|
||||
r := bytes.NewReader([]byte(composeFile))
|
||||
|
||||
err = filesystem.CreateFile(filePath, r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create upgrade compose file")
|
||||
}
|
||||
|
||||
err = service.composeDeployer.Deploy(
|
||||
context.Background(),
|
||||
[]string{filePath},
|
||||
libstack.DeployOptions{
|
||||
ForceRecreate: true,
|
||||
AbortOnContainerExit: true,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to deploy upgrade stack")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,6 +1,12 @@
|
|||
package platform
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
PodmanMode = "PODMAN"
|
||||
|
@ -12,8 +18,10 @@ const (
|
|||
type ContainerPlatform string
|
||||
|
||||
const (
|
||||
// PlatformDocker represent the Docker platform (Standalone/Swarm)
|
||||
PlatformDocker = ContainerPlatform("Docker")
|
||||
// PlatformDockerStandalone represent the Docker platform (Standalone)
|
||||
PlatformDockerStandalone = ContainerPlatform("Docker Standalone")
|
||||
// PlatformDockerSwarm represent the Docker platform (Swarm)
|
||||
PlatformDockerSwarm = ContainerPlatform("Docker Swarm")
|
||||
// PlatformKubernetes represent the Kubernetes platform
|
||||
PlatformKubernetes = ContainerPlatform("Kubernetes")
|
||||
// PlatformPodman represent the Podman platform (Standalone)
|
||||
|
@ -26,19 +34,45 @@ const (
|
|||
// or KUBERNETES_SERVICE_HOST environment variable to determine if
|
||||
// the container is running on Podman or inside the Kubernetes platform.
|
||||
// Defaults to Docker otherwise.
|
||||
func DetermineContainerPlatform() ContainerPlatform {
|
||||
func DetermineContainerPlatform() (ContainerPlatform, error) {
|
||||
podmanModeEnvVar := os.Getenv(PodmanMode)
|
||||
if podmanModeEnvVar == "1" {
|
||||
return PlatformPodman
|
||||
return PlatformPodman, nil
|
||||
}
|
||||
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
|
||||
if serviceHostKubernetesEnvVar != "" {
|
||||
return PlatformKubernetes
|
||||
return PlatformKubernetes, nil
|
||||
}
|
||||
nomadJobName := os.Getenv(NomadJobName)
|
||||
if nomadJobName != "" {
|
||||
return PlatformNomad
|
||||
return PlatformNomad, nil
|
||||
}
|
||||
|
||||
return PlatformDocker
|
||||
dockerCli, err := client.NewClientWithOpts()
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to create docker client")
|
||||
}
|
||||
defer dockerCli.Close()
|
||||
|
||||
if !isRunningInContainer() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
info, err := dockerCli.Info(context.Background())
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to retrieve docker info")
|
||||
}
|
||||
|
||||
if info.Swarm.NodeID == "" {
|
||||
return PlatformDockerStandalone, nil
|
||||
}
|
||||
|
||||
return PlatformDockerSwarm, nil
|
||||
}
|
||||
|
||||
// isRunningInContainer returns true if the process is running inside a container
|
||||
// this code is taken from https://github.com/moby/libnetwork/blob/master/drivers/bridge/setup_bridgenetfiltering.go
|
||||
func isRunningInContainer() bool {
|
||||
_, err := os.Stat("/.dockerenv")
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
|
|
@ -1501,12 +1501,8 @@ const (
|
|||
WebSocketKeepAlive = 1 * time.Hour
|
||||
)
|
||||
|
||||
const FeatureFlagBEUpgrade = "beUpgrade"
|
||||
|
||||
// List of supported features
|
||||
var SupportedFeatureFlags = []Feature{
|
||||
FeatureFlagBEUpgrade,
|
||||
}
|
||||
var SupportedFeatureFlags = []Feature{}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
|
|
|
@ -15,7 +15,6 @@ export const API_ENDPOINT_REGISTRIES = 'api/registries';
|
|||
export const API_ENDPOINT_RESOURCE_CONTROLS = 'api/resource_controls';
|
||||
export const API_ENDPOINT_SETTINGS = 'api/settings';
|
||||
export const API_ENDPOINT_STACKS = 'api/stacks';
|
||||
export const API_ENDPOINT_STATUS = 'api/status';
|
||||
export const API_ENDPOINT_SUPPORT = 'api/support';
|
||||
export const API_ENDPOINT_USERS = 'api/users';
|
||||
export const API_ENDPOINT_TAGS = 'api/tags';
|
||||
|
@ -53,7 +52,6 @@ angular
|
|||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', API_ENDPOINT_RESOURCE_CONTROLS)
|
||||
.constant('API_ENDPOINT_SETTINGS', API_ENDPOINT_SETTINGS)
|
||||
.constant('API_ENDPOINT_STACKS', API_ENDPOINT_STACKS)
|
||||
.constant('API_ENDPOINT_STATUS', API_ENDPOINT_STATUS)
|
||||
.constant('API_ENDPOINT_SUPPORT', API_ENDPOINT_SUPPORT)
|
||||
.constant('API_ENDPOINT_USERS', API_ENDPOINT_USERS)
|
||||
.constant('API_ENDPOINT_TAGS', API_ENDPOINT_TAGS)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { error as notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
import { getNodesCount } from '../services/api/status.service';
|
||||
import { useNodesCount } from '@/react/portainer/system/useNodesCount';
|
||||
|
||||
import { getLicenseInfo } from './license.service';
|
||||
import { LicenseInfo, LicenseType } from './types';
|
||||
|
@ -21,22 +20,8 @@ export function useLicenseInfo() {
|
|||
return { isLoading, info };
|
||||
}
|
||||
|
||||
function useNodesCounts() {
|
||||
const { isLoading, data } = useQuery(
|
||||
['status', 'nodes'],
|
||||
() => getNodesCount(),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failed to get nodes count');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { nodesCount: data || 0, isLoading };
|
||||
}
|
||||
|
||||
export function useIntegratedLicenseInfo() {
|
||||
const { isLoading: isLoadingNodes, nodesCount } = useNodesCounts();
|
||||
const { isLoading: isLoadingNodes, data: nodesCount = 0 } = useNodesCount();
|
||||
|
||||
const { isLoading: isLoadingLicense, info } = useLicenseInfo();
|
||||
if (
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
angular.module('portainer.app').factory('Status', [
|
||||
'$resource',
|
||||
'API_ENDPOINT_STATUS',
|
||||
function StatusFactory($resource, API_ENDPOINT_STATUS) {
|
||||
'use strict';
|
||||
return $resource(
|
||||
API_ENDPOINT_STATUS + '/:action',
|
||||
{},
|
||||
{
|
||||
get: { method: 'GET' },
|
||||
version: { method: 'GET', params: { action: 'version' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -1,80 +0,0 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '../axios';
|
||||
|
||||
export interface NodesCountResponse {
|
||||
nodes: number;
|
||||
}
|
||||
|
||||
export async function getNodesCount() {
|
||||
try {
|
||||
const { data } = await axios.get<NodesCountResponse>(buildUrl('nodes'));
|
||||
return data.nodes;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
Edition: string;
|
||||
Version: string;
|
||||
InstanceID: string;
|
||||
}
|
||||
|
||||
export async function getStatus() {
|
||||
try {
|
||||
const { data } = await axios.get<StatusResponse>(buildUrl());
|
||||
|
||||
data.Edition = 'Community Edition';
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export function useStatus<T = StatusResponse>(
|
||||
select?: (status: StatusResponse) => T
|
||||
) {
|
||||
return useQuery(['status'], () => getStatus(), { select });
|
||||
}
|
||||
|
||||
export interface VersionResponse {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable: boolean;
|
||||
// The latest version available
|
||||
LatestVersion: string;
|
||||
ServerVersion: string;
|
||||
DatabaseVersion: string;
|
||||
Build: {
|
||||
BuildNumber: string;
|
||||
ImageTag: string;
|
||||
NodejsVersion: string;
|
||||
YarnVersion: string;
|
||||
WebpackVersion: string;
|
||||
GoVersion: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getVersionStatus() {
|
||||
try {
|
||||
const { data } = await axios.get<VersionResponse>(buildUrl('version'));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export function useVersionStatus() {
|
||||
return useQuery(['version'], () => getVersionStatus());
|
||||
}
|
||||
|
||||
function buildUrl(action?: string) {
|
||||
let url = '/status';
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -1,42 +1,27 @@
|
|||
import { StatusVersionViewModel, StatusViewModel } from '../../models/status';
|
||||
import { getSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||
import { StatusViewModel } from '../../models/status';
|
||||
|
||||
angular.module('portainer.app').factory('StatusService', [
|
||||
'$q',
|
||||
'Status',
|
||||
function StatusServiceFactory($q, Status) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
angular.module('portainer.app').factory('StatusService', StatusServiceFactory);
|
||||
|
||||
service.status = function () {
|
||||
var deferred = $q.defer();
|
||||
/* @ngInject */
|
||||
function StatusServiceFactory($q) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
Status.get()
|
||||
.$promise.then(function success(data) {
|
||||
var status = new StatusViewModel(data);
|
||||
deferred.resolve(status);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve application status', err: err });
|
||||
});
|
||||
service.status = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
getSystemStatus()
|
||||
.then(function success(data) {
|
||||
var status = new StatusViewModel(data);
|
||||
deferred.resolve(status);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve application status', err: err });
|
||||
});
|
||||
|
||||
service.version = function () {
|
||||
var deferred = $q.defer();
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
Status.version()
|
||||
.$promise.then(function success(data) {
|
||||
var status = new StatusVersionViewModel(data);
|
||||
deferred.resolve(status);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve application version info', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
||||
return service;
|
||||
}
|
||||
|
|
|
@ -12,15 +12,31 @@ export default {
|
|||
interface TextFieldProps {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
vertical?: boolean;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export { TextField, SelectField };
|
||||
|
||||
function TextField({ label, tooltip = '' }: TextFieldProps) {
|
||||
function TextField({
|
||||
label,
|
||||
tooltip = '',
|
||||
required,
|
||||
error,
|
||||
vertical,
|
||||
}: TextFieldProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const inputId = 'input';
|
||||
return (
|
||||
<FormControl inputId={inputId} label={label} tooltip={tooltip}>
|
||||
<FormControl
|
||||
inputId={inputId}
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
required={required}
|
||||
errors={error}
|
||||
size={vertical ? 'vertical' : undefined}
|
||||
>
|
||||
<Input
|
||||
id={inputId}
|
||||
type="text"
|
||||
|
@ -34,9 +50,18 @@ function TextField({ label, tooltip = '' }: TextFieldProps) {
|
|||
TextField.args = {
|
||||
label: 'label',
|
||||
tooltip: '',
|
||||
vertical: false,
|
||||
required: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
function SelectField({ label, tooltip = '' }: TextFieldProps) {
|
||||
function SelectField({
|
||||
label,
|
||||
tooltip = '',
|
||||
vertical,
|
||||
required,
|
||||
error,
|
||||
}: TextFieldProps) {
|
||||
const options = [
|
||||
{ value: 1, label: 'one' },
|
||||
{ value: 2, label: 'two' },
|
||||
|
@ -44,7 +69,14 @@ function SelectField({ label, tooltip = '' }: TextFieldProps) {
|
|||
const [value, setValue] = useState(0);
|
||||
const inputId = 'input';
|
||||
return (
|
||||
<FormControl inputId={inputId} label={label} tooltip={tooltip}>
|
||||
<FormControl
|
||||
inputId={inputId}
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
size={vertical ? 'vertical' : undefined}
|
||||
required={required}
|
||||
errors={error}
|
||||
>
|
||||
<Select
|
||||
className="form-control"
|
||||
value={value}
|
||||
|
@ -58,4 +90,7 @@ function SelectField({ label, tooltip = '' }: TextFieldProps) {
|
|||
SelectField.args = {
|
||||
label: 'select',
|
||||
tooltip: '',
|
||||
vertical: false,
|
||||
required: false,
|
||||
error: '',
|
||||
};
|
||||
|
|
|
@ -5,9 +5,7 @@ import { Tooltip } from '@@/Tip/Tooltip';
|
|||
|
||||
import { FormError } from '../FormError';
|
||||
|
||||
import styles from './FormControl.module.css';
|
||||
|
||||
export type Size = 'xsmall' | 'small' | 'medium' | 'large';
|
||||
export type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'vertical';
|
||||
|
||||
export interface Props {
|
||||
inputId?: string;
|
||||
|
@ -29,7 +27,12 @@ export function FormControl({
|
|||
required,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className={clsx('form-group', styles.container)}>
|
||||
<div
|
||||
className={clsx(
|
||||
'form-group',
|
||||
'after:content-[""] after:clear-both after:table' // to fix issues with float
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className={clsx(sizeClassLabel(size), 'control-label', 'text-left')}
|
||||
|
@ -62,6 +65,8 @@ function sizeClassLabel(size?: Size) {
|
|||
return 'col-sm-4 col-lg-3';
|
||||
case 'xsmall':
|
||||
return 'col-sm-2';
|
||||
case 'vertical':
|
||||
return '';
|
||||
default:
|
||||
return 'col-sm-3 col-lg-2';
|
||||
}
|
||||
|
@ -75,6 +80,8 @@ function sizeClassChildren(size?: Size) {
|
|||
return 'col-sm-8 col-lg-9';
|
||||
case 'xsmall':
|
||||
return 'col-sm-8';
|
||||
case 'vertical':
|
||||
return '';
|
||||
default:
|
||||
return 'col-sm-9 col-lg-10';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
.close {
|
||||
color: var(--button-close-color);
|
||||
opacity: var(--button-opacity);
|
||||
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
|
||||
font-size: 21px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
filter: alpha(opacity=20);
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: var(--button-close-color);
|
||||
opacity: var(--button-opacity-hover);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import styles from './CloseButton.module.css';
|
||||
|
||||
export function CloseButton({
|
||||
onClose,
|
||||
className,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(styles.close, className, 'absolute top-2 right-2')}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.modal-dialog {
|
||||
width: 450px;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bg-modal-content-color);
|
||||
padding: 20px;
|
||||
|
||||
position: relative;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
|
||||
outline: 0;
|
||||
|
||||
box-shadow: 0 5px 15px rgb(0 0 0 / 50%);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { DialogContent, DialogOverlay } from '@reach/dialog';
|
||||
import clsx from 'clsx';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
import { CloseButton } from './CloseButton';
|
||||
import styles from './Modal.module.css';
|
||||
|
||||
const Context = createContext<boolean | null>(null);
|
||||
Context.displayName = 'ModalContext';
|
||||
|
||||
export function useModalContext() {
|
||||
const context = useContext(Context);
|
||||
if (!context) {
|
||||
throw new Error('should be nested under Modal');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onDismiss?(): void;
|
||||
'aria-label'?: string;
|
||||
'aria-labelledby'?: string;
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
children,
|
||||
onDismiss,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<Context.Provider value>
|
||||
<DialogOverlay
|
||||
isOpen
|
||||
className="flex items-center justify-center z-50"
|
||||
onDismiss={onDismiss}
|
||||
role="dialog"
|
||||
>
|
||||
<DialogContent
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
className={clsx(styles.modalDialog, 'p-0 bg-transparent')}
|
||||
>
|
||||
<div className={clsx(styles.modalContent, 'relative')}>
|
||||
{children}
|
||||
{onDismiss && <CloseButton onClose={onDismiss} />}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.modal-body {
|
||||
padding: 10px 0px;
|
||||
border-bottom: none;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { useModalContext } from './Modal';
|
||||
import styles from './ModalBody.module.css';
|
||||
|
||||
export function ModalBody({ children }: PropsWithChildren<unknown>) {
|
||||
useModalContext();
|
||||
return <div className={styles.modalBody}>{children}</div>;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.modal-footer {
|
||||
padding: 10px 0px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { useModalContext } from './Modal';
|
||||
import styles from './ModalFooter.module.css';
|
||||
|
||||
export function ModalFooter({ children }: PropsWithChildren<unknown>) {
|
||||
useModalContext();
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.modalFooter, 'flex justify-end')}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
.modal-header {
|
||||
margin-bottom: 10px;
|
||||
padding: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.background-error {
|
||||
padding-top: 55px;
|
||||
background-image: url(~assets/images/icon-error.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
.background-warning {
|
||||
padding-top: 55px;
|
||||
background-image: url(~assets/images/icon-warning.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ModalType } from './types';
|
||||
import { useModalContext } from './Modal';
|
||||
import styles from './ModalHeader.module.css';
|
||||
|
||||
interface Props {
|
||||
title: ReactNode;
|
||||
modalType?: ModalType;
|
||||
}
|
||||
|
||||
export function ModalHeader({ title, modalType }: Props) {
|
||||
useModalContext();
|
||||
|
||||
return (
|
||||
<div className={styles.modalHeader}>
|
||||
{modalType && (
|
||||
<div
|
||||
className={clsx({
|
||||
[styles.backgroundError]: modalType === ModalType.Destructive,
|
||||
[styles.backgroundWarning]: modalType === ModalType.Warn,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{typeof title === 'string' ? (
|
||||
<h5 className="font-bold">{title}</h5>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { Modal as MainComponent } from './Modal';
|
||||
import { ModalHeader } from './ModalHeader';
|
||||
import { ModalBody } from './ModalBody';
|
||||
import { ModalFooter } from './ModalFooter';
|
||||
|
||||
interface WithSubComponents {
|
||||
Header: typeof ModalHeader;
|
||||
Body: typeof ModalBody;
|
||||
Footer: typeof ModalFooter;
|
||||
}
|
||||
|
||||
const Modal = MainComponent as typeof MainComponent & WithSubComponents;
|
||||
|
||||
Modal.Header = ModalHeader;
|
||||
Modal.Body = ModalBody;
|
||||
Modal.Footer = ModalFooter;
|
||||
|
||||
export { Modal };
|
|
@ -0,0 +1,6 @@
|
|||
export type OnSubmit<TResult> = (result?: TResult) => void;
|
||||
|
||||
export enum ModalType {
|
||||
Warn = 'warning',
|
||||
Destructive = 'error',
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { ComponentType } from 'react';
|
||||
|
||||
/**
|
||||
* Hides the wrapped component if portainer is running as a docker extension.
|
||||
*/
|
||||
export function withHideOnExtension<T>(
|
||||
WrappedComponent: ComponentType<T>
|
||||
): ComponentType<T> {
|
||||
// Try to create a nice displayName for React Dev Tools.
|
||||
const displayName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
|
||||
function WrapperComponent(props: T) {
|
||||
if (window.ddExtension) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = `withHideOnExtension(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
|
@ -11,7 +11,7 @@ test('when user is using more nodes then allowed he should see message', async (
|
|||
rest.get('/api/licenses/info', (req, res, ctx) =>
|
||||
res(ctx.json({ nodes: allowed, type: LicenseType.Subscription }))
|
||||
),
|
||||
rest.get('/api/status/nodes', (req, res, ctx) =>
|
||||
rest.get('/api/system/nodes', (req, res, ctx) =>
|
||||
res(ctx.json({ nodes: used }))
|
||||
)
|
||||
);
|
||||
|
@ -32,7 +32,7 @@ test("when user is using less nodes then allowed he shouldn't see message", asyn
|
|||
rest.get('/api/licenses/info', (req, res, ctx) =>
|
||||
res(ctx.json({ nodes: allowed, type: LicenseType.Subscription }))
|
||||
),
|
||||
rest.get('/api/status/nodes', (req, res, ctx) =>
|
||||
rest.get('/api/system/nodes', (req, res, ctx) =>
|
||||
res(ctx.json({ nodes: used }))
|
||||
)
|
||||
);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { error as notifyError } from '@/portainer/services/notifications';
|
||||
import { LicenseType } from '@/portainer/license-management/types';
|
||||
import { useLicenseInfo } from '@/portainer/license-management/use-license.service';
|
||||
import { getNodesCount } from '@/portainer/services/api/status.service';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { InformationPanel } from '@@/InformationPanel';
|
||||
|
||||
import { useNodesCount } from '../system/useNodesCount';
|
||||
|
||||
export function LicenseNodePanel() {
|
||||
const nodesValid = useNodesValid();
|
||||
|
||||
|
@ -26,7 +24,7 @@ export function LicenseNodePanel() {
|
|||
}
|
||||
|
||||
function useNodesValid() {
|
||||
const { isLoading: isLoadingNodes, nodesCount } = useNodesCounts();
|
||||
const { isLoading: isLoadingNodes, data: nodesCount = 0 } = useNodesCount();
|
||||
|
||||
const { isLoading: isLoadingLicense, info } = useLicenseInfo();
|
||||
if (
|
||||
|
@ -40,17 +38,3 @@ function useNodesValid() {
|
|||
|
||||
return nodesCount <= info.nodes;
|
||||
}
|
||||
|
||||
function useNodesCounts() {
|
||||
const { isLoading, data } = useQuery(
|
||||
['status', 'nodes'],
|
||||
() => getNodesCount(),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failed to get nodes count');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { nodesCount: data || 0, isLoading };
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useStatus } from '@/portainer/services/api/status.service';
|
||||
import { useSettings } from '@/react/portainer/settings/queries';
|
||||
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||
|
||||
export function useAgentDetails() {
|
||||
const settingsQuery = useSettings();
|
||||
|
||||
const versionQuery = useStatus((status) => status.Version);
|
||||
const versionQuery = useSystemStatus({ select: (status) => status.Version });
|
||||
|
||||
if (!versionQuery.isSuccess || !settingsQuery.isSuccess) {
|
||||
return null;
|
||||
|
|
|
@ -2,9 +2,7 @@ import { useRouter } from '@uirouter/react';
|
|||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
export enum FeatureFlag {
|
||||
BEUpgrade = 'beUpgrade',
|
||||
}
|
||||
export enum FeatureFlag {}
|
||||
|
||||
export function useFeatureFlag(
|
||||
flag: FeatureFlag,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { ComponentType } from 'react';
|
||||
|
||||
export function withEdition<T>(
|
||||
WrappedComponent: ComponentType<T>,
|
||||
edition: 'BE' | 'CE'
|
||||
): ComponentType<T> {
|
||||
// Try to create a nice displayName for React Dev Tools.
|
||||
const displayName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
|
||||
function WrapperComponent(props: T) {
|
||||
if (process.env.PORTAINER_EDITION !== edition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = `with${edition}Edition(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { ComponentType } from 'react';
|
||||
|
||||
import { FeatureFlag, useFeatureFlag } from './useRedirectFeatureFlag';
|
||||
|
||||
export function withFeatureFlag<T>(
|
||||
WrappedComponent: ComponentType<T>,
|
||||
flag: FeatureFlag
|
||||
): ComponentType<T> {
|
||||
// Try to create a nice displayName for React Dev Tools.
|
||||
const displayName =
|
||||
WrappedComponent.displayName || WrappedComponent.name || 'Component';
|
||||
|
||||
function WrapperComponent(props: T) {
|
||||
const featureFlagQuery = useFeatureFlag(flag);
|
||||
|
||||
if (!featureFlagQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
}
|
||||
|
||||
WrapperComponent.displayName = `with${flag}FeatureFlag(${displayName})`;
|
||||
|
||||
return WrapperComponent;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export function buildUrl(action?: string) {
|
||||
let url = '/status';
|
||||
let url = '/system';
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
|
@ -0,0 +1,3 @@
|
|||
export const queryKeys = {
|
||||
base: () => ['system'] as const,
|
||||
};
|
|
@ -4,6 +4,9 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export const queryKey = [...queryKeys.base(), 'nodes'] as const;
|
||||
|
||||
export interface NodesCountResponse {
|
||||
nodes: number;
|
||||
|
@ -19,7 +22,7 @@ async function getNodesCount() {
|
|||
}
|
||||
|
||||
export function useNodesCount() {
|
||||
return useQuery(['status', 'nodes'], getNodesCount, {
|
||||
return useQuery(queryKey, getNodesCount, {
|
||||
...withError('Unable to retrieve nodes count'),
|
||||
});
|
||||
}
|
|
@ -4,9 +4,19 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export const queryKey = [...queryKeys.base(), 'info'] as const;
|
||||
|
||||
export type ContainerPlatform =
|
||||
| 'Docker Standalone'
|
||||
| 'Docker Swarm'
|
||||
| 'Kubernetes'
|
||||
| 'Podman'
|
||||
| 'Nomad';
|
||||
|
||||
export interface SystemInfoResponse {
|
||||
platform: string;
|
||||
platform: ContainerPlatform;
|
||||
agents: number;
|
||||
edgeAgents: number;
|
||||
edgeDevices: number;
|
||||
|
@ -14,7 +24,7 @@ export interface SystemInfoResponse {
|
|||
|
||||
async function getSystemInfo() {
|
||||
try {
|
||||
const { data } = await axios.get<SystemInfoResponse>(buildUrl('system'));
|
||||
const { data } = await axios.get<SystemInfoResponse>(buildUrl('info'));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
|
@ -22,7 +32,7 @@ async function getSystemInfo() {
|
|||
}
|
||||
|
||||
export function useSystemInfo() {
|
||||
return useQuery(['status', 'system'], getSystemInfo, {
|
||||
return useQuery(queryKey, getSystemInfo, {
|
||||
...withError('Unable to retrieve system info'),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { RetryValue } from 'react-query/types/core/retryer';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export const queryKey = [...queryKeys.base(), 'status'] as const;
|
||||
|
||||
export interface StatusResponse {
|
||||
Edition: string;
|
||||
Version: string;
|
||||
InstanceID: string;
|
||||
}
|
||||
|
||||
export async function getSystemStatus() {
|
||||
try {
|
||||
const { data } = await axios.get<StatusResponse>(buildUrl('status'));
|
||||
|
||||
data.Edition = 'Community Edition';
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export function useSystemStatus<T = StatusResponse>({
|
||||
select,
|
||||
enabled,
|
||||
retry,
|
||||
onSuccess,
|
||||
}: {
|
||||
select?: (status: StatusResponse) => T;
|
||||
enabled?: boolean;
|
||||
retry?: RetryValue<unknown>;
|
||||
onSuccess?: (data: T) => void;
|
||||
} = {}) {
|
||||
return useQuery(queryKey, () => getSystemStatus(), {
|
||||
select,
|
||||
enabled,
|
||||
retry,
|
||||
onSuccess,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export const queryKey = [...queryKeys.base(), 'version'] as const;
|
||||
|
||||
export interface VersionResponse {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable: boolean;
|
||||
// The latest version available
|
||||
LatestVersion: string;
|
||||
ServerVersion: string;
|
||||
DatabaseVersion: string;
|
||||
Build: {
|
||||
BuildNumber: string;
|
||||
ImageTag: string;
|
||||
NodejsVersion: string;
|
||||
YarnVersion: string;
|
||||
WebpackVersion: string;
|
||||
GoVersion: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSystemVersion() {
|
||||
try {
|
||||
const { data } = await axios.get<VersionResponse>(buildUrl('version'));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export function useSystemVersion() {
|
||||
return useQuery(queryKey, () => getSystemVersion());
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { useMutation } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
|
||||
export function useUpgradeEditionMutation() {
|
||||
return useMutation(upgradeEdition, {
|
||||
...withError('Unable to upgrade edition'),
|
||||
});
|
||||
}
|
||||
|
||||
async function upgradeEdition({ license }: { license: string }) {
|
||||
try {
|
||||
await axios.post(buildUrl('upgrade'), { license });
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
|
@ -2,10 +2,8 @@ import { useState } from 'react';
|
|||
import { Database, Hash, Server, Tag, Wrench } from 'lucide-react';
|
||||
import { DialogOverlay } from '@reach/dialog';
|
||||
|
||||
import {
|
||||
useStatus,
|
||||
useVersionStatus,
|
||||
} from '@/portainer/services/api/status.service';
|
||||
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
|
@ -13,7 +11,7 @@ import styles from './Footer.module.css';
|
|||
|
||||
export function BuildInfoModalButton() {
|
||||
const [isBuildInfoVisible, setIsBuildInfoVisible] = useState(false);
|
||||
const statusQuery = useStatus();
|
||||
const statusQuery = useSystemStatus();
|
||||
|
||||
if (!statusQuery.data) {
|
||||
return null;
|
||||
|
@ -39,8 +37,8 @@ export function BuildInfoModalButton() {
|
|||
}
|
||||
|
||||
function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
|
||||
const versionQuery = useVersionStatus();
|
||||
const statusQuery = useStatus();
|
||||
const versionQuery = useSystemVersion();
|
||||
const statusQuery = useSystemStatus();
|
||||
|
||||
if (!statusQuery.data || !versionQuery.data) {
|
||||
return null;
|
||||
|
|
|
@ -23,15 +23,6 @@ function CEFooter() {
|
|||
<span>Community Edition</span>
|
||||
|
||||
<BuildInfoModalButton />
|
||||
|
||||
<a
|
||||
href="https://www.portainer.io/install-BE-now"
|
||||
className="text-blue-6 font-medium"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Upgrade
|
||||
</a>
|
||||
</FooterContent>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import clsx from 'clsx';
|
||||
import { DownloadCloud } from 'lucide-react';
|
||||
|
||||
import { getVersionStatus } from '@/portainer/services/api/status.service';
|
||||
import { useUIState } from '@/react/hooks/useUIState';
|
||||
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
|
@ -11,7 +10,7 @@ import styles from './UpdateNotifications.module.css';
|
|||
|
||||
export function UpdateNotification() {
|
||||
const uiStateStore = useUIState();
|
||||
const query = useUpdateNotification();
|
||||
const query = useSystemVersion();
|
||||
|
||||
if (!query.data || !query.data.UpdateAvailable) {
|
||||
return null;
|
||||
|
@ -67,7 +66,3 @@ export function UpdateNotification() {
|
|||
uiStateStore.dismissUpdateVersion(version);
|
||||
}
|
||||
}
|
||||
|
||||
function useUpdateNotification() {
|
||||
return useQuery(['status', 'version'], () => getVersionStatus());
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { SidebarItem } from './SidebarItem';
|
|||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
import { SidebarProvider } from './useSidebarState';
|
||||
import { UpgradeBEBanner } from './UpgradeBEBanner';
|
||||
import { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
|
||||
|
||||
export function Sidebar() {
|
||||
const { isAdmin, user } = useUser();
|
||||
|
@ -31,7 +31,7 @@ export function Sidebar() {
|
|||
/* in the future (when we remove r2a) this should wrap the whole app - to change root styles */
|
||||
<SidebarProvider>
|
||||
<div className={clsx(styles.root, 'sidebar flex flex-col')}>
|
||||
<UpgradeBEBanner />
|
||||
<UpgradeBEBannerWrapper />
|
||||
<nav
|
||||
className={clsx(
|
||||
styles.nav,
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import {
|
||||
useFeatureFlag,
|
||||
FeatureFlag,
|
||||
} from '@/react/portainer/feature-flags/useRedirectFeatureFlag';
|
||||
import { useNodesCount } from '@/react/portainer/status/useNodesCount';
|
||||
import { useSystemInfo } from '@/react/portainer/status/useSystemInfo';
|
||||
|
||||
import { useSidebarState } from './useSidebarState';
|
||||
|
||||
export function UpgradeBEBanner() {
|
||||
const { data } = useFeatureFlag(FeatureFlag.BEUpgrade, { enabled: !isBE });
|
||||
|
||||
if (isBE || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Inner />;
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const { trackEvent } = useAnalytics();
|
||||
const { isOpen } = useSidebarState();
|
||||
const nodesCountQuery = useNodesCount();
|
||||
const systemInfoQuery = useSystemInfo();
|
||||
|
||||
if (!nodesCountQuery.data || !systemInfoQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodesCount = nodesCountQuery.data;
|
||||
const systemInfo = systemInfoQuery.data;
|
||||
|
||||
const metadata = {
|
||||
upgrade: false,
|
||||
nodeCount: nodesCount,
|
||||
platform: systemInfo.platform,
|
||||
edgeAgents: systemInfo.edgeAgents,
|
||||
edgeDevices: systemInfo.edgeDevices,
|
||||
agents: systemInfo.agents,
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="border-0 bg-warning-5 text-warning-9 w-full min-h-[48px] h-12 font-semibold flex justify-center items-center gap-3"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isOpen && <>Upgrade to Business Edition</>}
|
||||
<ArrowRight className="text-lg lucide" />
|
||||
</button>
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
trackEvent('portainer-upgrade-admin', {
|
||||
category: 'portainer',
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { Loader2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||
|
||||
import { Modal } from '@@/modals/Modal';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
export function LoadingDialog() {
|
||||
useWaitForServerStatus();
|
||||
|
||||
return (
|
||||
<Modal aria-label="Upgrade Portainer to Business Edition">
|
||||
<Modal.Body>
|
||||
<div className="flex flex-col items-center justify-center w-full">
|
||||
<Icon
|
||||
icon={Loader2}
|
||||
className="animate-spin-slow !text-8xl !text-blue-8"
|
||||
aria-label="loading"
|
||||
/>
|
||||
|
||||
<h1 className="!text-2xl">Upgrading Portainer...</h1>
|
||||
|
||||
<p className="text-center text-gray-6 text-xl">
|
||||
Please wait while we upgrade your Portainer to Business Edition.
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function useWaitForServerStatus() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
useSystemStatus({
|
||||
enabled,
|
||||
retry: true,
|
||||
onSuccess() {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setEnabled(true);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Modal } from '@@/modals/Modal';
|
||||
import { ModalType } from '@@/modals/Modal/types';
|
||||
|
||||
export function NonAdminUpgradeDialog({
|
||||
onDismiss,
|
||||
}: {
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Modal aria-label="Upgrade Portainer to Business Edition">
|
||||
<Modal.Header
|
||||
title="Contact your administrator"
|
||||
modalType={ModalType.Warn}
|
||||
/>
|
||||
<Modal.Body>
|
||||
You need to be logged in as an admin to upgrade Portainer to Business
|
||||
Edition.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<div className="flex gap-2 w-full">
|
||||
<Button
|
||||
color="default"
|
||||
size="medium"
|
||||
className="w-1/3"
|
||||
onClick={() => onDismiss()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<a
|
||||
href="https://www.portainer.io/take-5"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="no-link w-2/3"
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
size="medium"
|
||||
className="w-full"
|
||||
icon={ExternalLink}
|
||||
>
|
||||
Learn about Business Edition
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { ArrowRight } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
|
||||
import { useNodesCount } from '@/react/portainer/system/useNodesCount';
|
||||
import {
|
||||
ContainerPlatform,
|
||||
useSystemInfo,
|
||||
} from '@/react/portainer/system/useSystemInfo';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
import { withEdition } from '@/react/portainer/feature-flags/withEdition';
|
||||
import { withHideOnExtension } from '@/react/hooks/withHideOnExtension';
|
||||
|
||||
import { useSidebarState } from '../useSidebarState';
|
||||
|
||||
import { UpgradeDialog } from './UpgradeDialog';
|
||||
|
||||
export const UpgradeBEBannerWrapper = withHideOnExtension(
|
||||
withEdition(UpgradeBEBanner, 'CE')
|
||||
);
|
||||
|
||||
const enabledPlatforms: Array<ContainerPlatform> = ['Docker Standalone'];
|
||||
|
||||
function UpgradeBEBanner() {
|
||||
const { isAdmin } = useUser();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const { isOpen: isSidebarOpen } = useSidebarState();
|
||||
const nodesCountQuery = useNodesCount();
|
||||
const systemInfoQuery = useSystemInfo();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (!nodesCountQuery.isSuccess || !systemInfoQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodesCount = nodesCountQuery.data;
|
||||
const systemInfo = systemInfoQuery.data;
|
||||
|
||||
const metadata = {
|
||||
upgrade: false,
|
||||
nodeCount: nodesCount,
|
||||
platform: systemInfo.platform,
|
||||
edgeAgents: systemInfo.edgeAgents,
|
||||
edgeDevices: systemInfo.edgeDevices,
|
||||
agents: systemInfo.agents,
|
||||
};
|
||||
|
||||
if (!enabledPlatforms.includes(systemInfo.platform)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="border-0 bg-warning-5 text-warning-9 w-full min-h-[48px] h-12 font-semibold flex justify-center items-center gap-3"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isSidebarOpen && <>Upgrade to Business Edition</>}
|
||||
<ArrowRight className="text-lg lucide" />
|
||||
</button>
|
||||
|
||||
{isOpen && <UpgradeDialog onDismiss={() => setIsOpen(false)} />}
|
||||
</>
|
||||
);
|
||||
|
||||
function handleClick() {
|
||||
trackEvent(
|
||||
isAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
|
||||
{
|
||||
category: 'portainer',
|
||||
metadata,
|
||||
}
|
||||
);
|
||||
setIsOpen(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { UploadLicenseDialog } from './UploadLicenseDialog';
|
||||
import { LoadingDialog } from './LoadingDialog';
|
||||
import { NonAdminUpgradeDialog } from './NonAdminUpgradeDialog';
|
||||
|
||||
type Step = 'uploadLicense' | 'loading' | 'getLicense';
|
||||
|
||||
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
|
||||
const { isAdmin } = useUser();
|
||||
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
|
||||
|
||||
const component = getDialog();
|
||||
|
||||
return component;
|
||||
|
||||
function getDialog() {
|
||||
if (!isAdmin) {
|
||||
return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
|
||||
}
|
||||
|
||||
switch (currentStep) {
|
||||
case 'getLicense':
|
||||
throw new Error('Not implemented');
|
||||
// return <GetLicense setCurrentStep={setCurrentStep} />;
|
||||
case 'uploadLicense':
|
||||
return (
|
||||
<UploadLicenseDialog
|
||||
goToLoading={() => setCurrentStep('loading')}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
);
|
||||
case 'loading':
|
||||
return <LoadingDialog />;
|
||||
default:
|
||||
throw new Error('step type not found');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import { Field, Form, Formik } from 'formik';
|
||||
import { object, SchemaOf, string } from 'yup';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
import { useUpgradeEditionMutation } from '@/react/portainer/system/useUpgradeEditionMutation';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { Button, LoadingButton } from '@@/buttons';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { Modal } from '@@/modals/Modal';
|
||||
|
||||
interface FormValues {
|
||||
license: string;
|
||||
}
|
||||
|
||||
const initialValues: FormValues = {
|
||||
license: '',
|
||||
};
|
||||
|
||||
export function UploadLicenseDialog({
|
||||
onDismiss,
|
||||
goToLoading,
|
||||
}: {
|
||||
onDismiss: () => void;
|
||||
goToLoading: () => void;
|
||||
}) {
|
||||
const upgradeMutation = useUpgradeEditionMutation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onDismiss={onDismiss}
|
||||
aria-label="Upgrade Portainer to Business Edition"
|
||||
>
|
||||
<Modal.Header
|
||||
title={<h4 className="font-medium text-xl">Upgrade Portainer</h4>}
|
||||
/>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
>
|
||||
{({ errors }) => (
|
||||
<Form noValidate>
|
||||
<Modal.Body>
|
||||
<p className="font-semibold text-gray-7">
|
||||
Please enter your Portainer License Below
|
||||
</p>
|
||||
<FormControl
|
||||
label="License"
|
||||
errors={errors.license}
|
||||
required
|
||||
size="vertical"
|
||||
>
|
||||
<Field name="license" as={Input} required />
|
||||
</FormControl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<div className="flex gap-2 [&>*]:w-1/2 w-full">
|
||||
<a
|
||||
href="https://www.portainer.io/take-5"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="no-link"
|
||||
>
|
||||
<Button
|
||||
color="default"
|
||||
size="medium"
|
||||
className="w-full"
|
||||
icon={ExternalLink}
|
||||
>
|
||||
Get a license
|
||||
</Button>
|
||||
</a>
|
||||
<LoadingButton
|
||||
color="primary"
|
||||
size="medium"
|
||||
loadingText="Validating License"
|
||||
isLoading={upgradeMutation.isLoading}
|
||||
>
|
||||
Start upgrade
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
upgradeMutation.mutate(values, {
|
||||
onSuccess() {
|
||||
notifySuccess('Starting upgrade', 'License validated successfully');
|
||||
goToLoading();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validation(): SchemaOf<FormValues> {
|
||||
return object().shape({
|
||||
license: string()
|
||||
.required('License is required')
|
||||
.matches(/^\d-.+/, 'License is invalid'),
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from '@/portainer/license-management/types';
|
||||
import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
|
||||
import { Tag } from '@/portainer/tags/types';
|
||||
import { StatusResponse } from '@/portainer/services/api/status.service';
|
||||
import { StatusResponse } from '@/react/portainer/system/useSystemStatus';
|
||||
import { createMockTeams } from '@/react-tools/test-mocks';
|
||||
import { PublicSettingsResponse } from '@/react/portainer/settings/types';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
|
|
@ -11,6 +11,10 @@ $project_path = $((Get-Location).Path)
|
|||
New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null
|
||||
Set-Location -Path "$project_path\api\cmd\portainer"
|
||||
|
||||
# copy templates
|
||||
Copy-Item -Path "./mustache-templates" -Destination "./dist" -Recurse
|
||||
|
||||
|
||||
C:\go\bin\go.exe get -t -d -v ./...
|
||||
C:\go\bin\go.exe build -v
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ YARN_VERSION="0"
|
|||
WEBPACK_VERSION="0"
|
||||
GO_VERSION="0"
|
||||
|
||||
# copy templates
|
||||
cp -r "./mustache-templates" "./dist"
|
||||
|
||||
|
||||
cd api
|
||||
# the go get adds 8 seconds
|
||||
go get -t -d -v ./...
|
||||
|
|
|
@ -13,6 +13,8 @@ mkdir -p ${GOPATH}/src/github.com/portainer/portainer
|
|||
|
||||
cp -R api ${GOPATH}/src/github.com/portainer/portainer/api
|
||||
|
||||
cp -r "./mustache-templates" "./dist"
|
||||
|
||||
cd 'api/cmd/portainer'
|
||||
|
||||
go get -t -d -v ./...
|
||||
|
@ -29,3 +31,4 @@ if [ "${PLATFORM}" == 'windows' ]; then
|
|||
else
|
||||
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
|
||||
fi
|
||||
|
||||
|
|
|
@ -20,4 +20,6 @@ EXPOSE 9000
|
|||
EXPOSE 9443
|
||||
EXPOSE 8000
|
||||
|
||||
LABEL io.portainer.server true
|
||||
|
||||
ENTRYPOINT ["/portainer"]
|
||||
|
|
|
@ -9,4 +9,6 @@ EXPOSE 9000
|
|||
EXPOSE 9443
|
||||
EXPOSE 8000
|
||||
|
||||
LABEL io.portainer.server true
|
||||
|
||||
ENTRYPOINT ["/portainer"]
|
||||
|
|
|
@ -22,4 +22,6 @@ EXPOSE 9000
|
|||
EXPOSE 9443
|
||||
EXPOSE 8000
|
||||
|
||||
LABEL io.portainer.server true
|
||||
|
||||
ENTRYPOINT ["/portainer.exe"]
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
updater:
|
||||
image: {{updater_image}}{{^updater_image}}portainer/portainer-updater:latest{{/updater_image}}
|
||||
command: ["portainer",
|
||||
"--image", "{{image}}{{^image}}portainer/portainer-ee:latest{{/image}}",
|
||||
"--env-type", "standalone",
|
||||
"--license", "{{license}}"
|
||||
]
|
||||
{{#skip_pull_image}}
|
||||
environment:
|
||||
- SKIP_PULL=1
|
||||
{{/skip_pull_image}}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
{{! - \\.\pipe\docker_engine:\\.\pipe\docker_engine }}
|
Loading…
Reference in New Issue