Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b46bff06c6 | |||
| 5d311031e3 | |||
| 99de11894c | |||
| 02c006be8a | |||
| 60a2696a8d | |||
| 64b5d1df2d | |||
| 025a409ab5 | |||
| 9b65f01748 | |||
| e112ddfbeb | |||
| 707fc91a32 | |||
| b2eb4388fd | |||
| 5a451b2035 | |||
| 370d224d76 | |||
| b4c36b0e48 |
@@ -49,7 +49,6 @@ import (
|
|||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
"github.com/portainer/portainer/pkg/libhelm"
|
"github.com/portainer/portainer/pkg/libhelm"
|
||||||
"github.com/portainer/portainer/pkg/libstack"
|
|
||||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
@@ -166,26 +165,6 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
|
|||||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||||
}
|
}
|
||||||
|
|
||||||
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
|
||||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed creating compose manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
return composeWrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSwarmStackManager(
|
|
||||||
assetsPath string,
|
|
||||||
configPath string,
|
|
||||||
signatureService portainer.DigitalSignatureService,
|
|
||||||
fileService portainer.FileService,
|
|
||||||
reverseTunnelService portainer.ReverseTunnelService,
|
|
||||||
dataStore dataservices.DataStore,
|
|
||||||
) (portainer.SwarmStackManager, error) {
|
|
||||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||||
}
|
}
|
||||||
@@ -435,9 +414,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
|
|
||||||
composeDeployer := compose.NewComposeDeployer()
|
composeDeployer := compose.NewComposeDeployer()
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
|
swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -610,7 +610,7 @@
|
|||||||
"RequiredPasswordLength": 12
|
"RequiredPasswordLength": 12
|
||||||
},
|
},
|
||||||
"KubeconfigExpiry": "0",
|
"KubeconfigExpiry": "0",
|
||||||
"KubectlShellImage": "portainer/kubectl-shell:2.24.0",
|
"KubectlShellImage": "portainer/kubectl-shell:2.24.1",
|
||||||
"LDAPSettings": {
|
"LDAPSettings": {
|
||||||
"AnonymousMode": true,
|
"AnonymousMode": true,
|
||||||
"AutoCreateUsers": true,
|
"AutoCreateUsers": true,
|
||||||
@@ -942,7 +942,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": {
|
"version": {
|
||||||
"VERSION": "{\"SchemaVersion\":\"2.24.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
"VERSION": "{\"SchemaVersion\":\"2.24.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||||
},
|
},
|
||||||
"webhooks": null
|
"webhooks": null
|
||||||
}
|
}
|
||||||
+6
-3
@@ -31,15 +31,18 @@ type (
|
|||||||
// RegistryCredentials holds the credentials for a Docker registry.
|
// RegistryCredentials holds the credentials for a Docker registry.
|
||||||
// Used only for EE
|
// Used only for EE
|
||||||
RegistryCredentials []RegistryCredentials
|
RegistryCredentials []RegistryCredentials
|
||||||
// PrePullImage is a flag indicating if the agent should pull the image before deploying the stack.
|
// PrePullImage is a flag indicating if the agent must pull the image before deploying the stack.
|
||||||
// Used only for EE
|
// Used only for EE
|
||||||
PrePullImage bool
|
PrePullImage bool
|
||||||
// RePullImage is a flag indicating if the agent should pull the image if it is already present on the node.
|
// RePullImage is a flag indicating if the agent must pull the image if it is already present on the node.
|
||||||
// Used only for EE
|
// Used only for EE
|
||||||
RePullImage bool
|
RePullImage bool
|
||||||
// RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails.
|
// RetryDeploy is a flag indicating if the agent must retry to deploy the stack if it fails.
|
||||||
// Used only for EE
|
// Used only for EE
|
||||||
RetryDeploy bool
|
RetryDeploy bool
|
||||||
|
// RetryPeriod specifies the duration, in seconds, for which the agent should continue attempting to deploy the stack after a failure
|
||||||
|
// Used only for EE
|
||||||
|
RetryPeriod int
|
||||||
// EdgeUpdateID is the ID of the edge update related to this stack.
|
// EdgeUpdateID is the ID of the edge update related to this stack.
|
||||||
// Used only for EE
|
// Used only for EE
|
||||||
EdgeUpdateID int
|
EdgeUpdateID int
|
||||||
|
|||||||
+63
-11
@@ -9,27 +9,32 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||||
|
"github.com/portainer/portainer/api/internal/registryutils"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
"github.com/portainer/portainer/pkg/libstack"
|
"github.com/portainer/portainer/pkg/libstack"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ComposeStackManager is a wrapper for docker-compose binary
|
// ComposeStackManager is a wrapper for docker-compose binary
|
||||||
type ComposeStackManager struct {
|
type ComposeStackManager struct {
|
||||||
deployer libstack.Deployer
|
deployer libstack.Deployer
|
||||||
proxyManager *proxy.Manager
|
proxyManager *proxy.Manager
|
||||||
|
dataStore dataservices.DataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
// NewComposeStackManager returns a Compose stack manager
|
||||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
|
||||||
|
|
||||||
return &ComposeStackManager{
|
return &ComposeStackManager{
|
||||||
deployer: deployer,
|
deployer: deployer,
|
||||||
proxyManager: proxyManager,
|
proxyManager: proxyManager,
|
||||||
}, nil
|
dataStore: dataStore,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||||
@@ -60,6 +65,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
|||||||
EnvFilePath: envFilePath,
|
EnvFilePath: envFilePath,
|
||||||
Host: url,
|
Host: url,
|
||||||
ProjectName: stack.Name,
|
ProjectName: stack.Name,
|
||||||
|
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||||
},
|
},
|
||||||
ForceRecreate: options.ForceRecreate,
|
ForceRecreate: options.ForceRecreate,
|
||||||
AbortOnContainerExit: options.AbortOnContainerExit,
|
AbortOnContainerExit: options.AbortOnContainerExit,
|
||||||
@@ -90,6 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
|
|||||||
EnvFilePath: envFilePath,
|
EnvFilePath: envFilePath,
|
||||||
Host: url,
|
Host: url,
|
||||||
ProjectName: stack.Name,
|
ProjectName: stack.Name,
|
||||||
|
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||||
},
|
},
|
||||||
Remove: options.Remove,
|
Remove: options.Remove,
|
||||||
Args: options.Args,
|
Args: options.Args,
|
||||||
@@ -103,8 +110,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
} else if proxy != nil {
|
||||||
if proxy != nil {
|
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +126,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||||||
|
|
||||||
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
|
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
|
||||||
// but does not start containers based on those images.
|
// but does not start containers based on those images.
|
||||||
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
|
||||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
} else if proxy != nil {
|
||||||
if proxy != nil {
|
|
||||||
defer proxy.Close()
|
defer proxy.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
|||||||
EnvFilePath: envFilePath,
|
EnvFilePath: envFilePath,
|
||||||
Host: url,
|
Host: url,
|
||||||
ProjectName: stack.Name,
|
ProjectName: stack.Name,
|
||||||
|
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||||
})
|
})
|
||||||
return errors.Wrap(err, "failed to pull images of the stack")
|
return errors.Wrap(err, "failed to pull images of the stack")
|
||||||
}
|
}
|
||||||
@@ -178,12 +184,12 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
|||||||
|
|
||||||
// Copy from default .env file
|
// Copy from default .env file
|
||||||
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
|
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
|
||||||
if err = copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
|
if err := copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy from stack env vars
|
// Copy from stack env vars
|
||||||
if err = copyConfigEnvVars(envfile, stack.Env); err != nil {
|
if err := copyConfigEnvVars(envfile, stack.Env); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,3 +225,49 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
|
||||||
|
var authConfigs []types.AuthConfig
|
||||||
|
|
||||||
|
for _, r := range registries {
|
||||||
|
ac := types.AuthConfig{
|
||||||
|
Username: r.Username,
|
||||||
|
Password: r.Password,
|
||||||
|
ServerAddress: r.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Authentication {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfigs = append(authConfigs, ac)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authConfigs
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
|
||||||
|
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Err(err).
|
||||||
|
Str("RegistryName", registry.Name).
|
||||||
|
Msg("Failed to validate registry token. Skip logging with this registry.")
|
||||||
|
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := registryutils.GetRegEffectiveCredential(registry)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Err(err).
|
||||||
|
Str("RegistryName", registry.Name).
|
||||||
|
Msg("Failed to get effective credential. Skip logging with this registry.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return username, password, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ func Test_UpAndDown(t *testing.T) {
|
|||||||
|
|
||||||
deployer := compose.NewComposeDeployer()
|
deployer := compose.NewComposeDeployer()
|
||||||
|
|
||||||
w, err := NewComposeStackManager(deployer, nil)
|
w := NewComposeStackManager(deployer, nil, nil)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed creating manager: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
|||||||
+9
-29
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/registryutils"
|
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -46,8 +45,7 @@ func NewSwarmStackManager(
|
|||||||
dataStore: datastore,
|
dataStore: datastore,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
if err := manager.updateDockerCLIConfiguration(manager.configPath); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,33 +61,14 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
|||||||
|
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, ®istry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.
|
|
||||||
Warn().
|
|
||||||
Err(err).
|
|
||||||
Str("RegistryName", registry.Name).
|
|
||||||
Msg("Failed to validate registry token. Skip logging with this registry.")
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
username, password, err := registryutils.GetRegEffectiveCredential(®istry)
|
|
||||||
if err != nil {
|
|
||||||
log.
|
|
||||||
Warn().
|
|
||||||
Err(err).
|
|
||||||
Str("RegistryName", registry.Name).
|
|
||||||
Msg("Failed to get effective credential. Skip logging with this registry.")
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
||||||
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
if err := runCommandAndCaptureStdErr(command, registryArgs, nil, ""); err != nil {
|
||||||
if err != nil {
|
log.Warn().
|
||||||
log.
|
|
||||||
Warn().
|
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("RegistryName", registry.Name).
|
Str("RegistryName", registry.Name).
|
||||||
Msg("Failed to login.")
|
Msg("Failed to login.")
|
||||||
@@ -155,6 +134,7 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
|||||||
|
|
||||||
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
|
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
@@ -167,8 +147,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
|||||||
cmd.Env = append(cmd.Env, env...)
|
cmd.Env = append(cmd.Env, env...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmd.Run()
|
if err := cmd.Run(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.New(stderr.String())
|
return errors.New(stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +171,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointURL = "tcp://" + tunnelAddr
|
endpointURL = "tcp://" + tunnelAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +196,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
|||||||
|
|
||||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||||
configFilePath := path.Join(configPath, "config.json")
|
configFilePath := path.Join(configPath, "config.json")
|
||||||
|
|
||||||
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -246,8 +227,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
|||||||
return make(map[string]any), nil
|
return make(map[string]any), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(raw, &config)
|
if err := json.Unmarshal(raw, &config); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var edgeStack *portainer.EdgeStack
|
var edgeStack *portainer.EdgeStack
|
||||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
edgeStack, err = handler.createSwarmStack(tx, method, dryrun, tokenData.ID, r)
|
edgeStack, err = handler.createSwarmStack(tx, method, dryrun, tokenData.ID, r)
|
||||||
return err
|
return err
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
switch {
|
switch {
|
||||||
case httperrors.IsInvalidPayloadError(err):
|
case httperrors.IsInvalidPayloadError(err):
|
||||||
return httperror.BadRequest("Invalid payload", err)
|
return httperror.BadRequest("Invalid payload", err)
|
||||||
|
|||||||
@@ -57,17 +57,15 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var payload updateEdgeStackPayload
|
var payload updateEdgeStackPayload
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||||
if err != nil {
|
|
||||||
return httperror.BadRequest("Invalid request payload", err)
|
return httperror.BadRequest("Invalid request payload", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stack *portainer.EdgeStack
|
var stack *portainer.EdgeStack
|
||||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
stack, err = handler.updateEdgeStack(tx, portainer.EdgeStackID(stackID), payload)
|
stack, err = handler.updateEdgeStack(tx, portainer.EdgeStackID(stackID), payload)
|
||||||
return err
|
return err
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
var httpErr *httperror.HandlerError
|
var httpErr *httperror.HandlerError
|
||||||
if errors.As(err, &httpErr) {
|
if errors.As(err, &httpErr) {
|
||||||
return httpErr
|
return httpErr
|
||||||
@@ -122,14 +120,12 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
|||||||
stack.EdgeGroups = groupsIds
|
stack.EdgeGroups = groupsIds
|
||||||
|
|
||||||
if payload.UpdateVersion {
|
if payload.UpdateVersion {
|
||||||
err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds)
|
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +156,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
|||||||
|
|
||||||
delete(relation.EdgeStacks, edgeStackID)
|
delete(relation.EdgeStacks, edgeStackID)
|
||||||
|
|
||||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +176,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
|||||||
|
|
||||||
relation.EdgeStacks[edgeStackID] = true
|
relation.EdgeStacks[edgeStackID] = true
|
||||||
|
|
||||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @title PortainerCE API
|
// @title PortainerCE API
|
||||||
// @version 2.24.0
|
// @version 2.24.1
|
||||||
// @description.markdown api-description.md
|
// @description.markdown api-description.md
|
||||||
// @termsOfService
|
// @termsOfService
|
||||||
|
|
||||||
|
|||||||
@@ -14,47 +14,51 @@ func isRegTokenValid(registry *portainer.Registry) (valid bool) {
|
|||||||
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix()
|
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
func doGetRegToken(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
|
func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||||
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.AccessToken = *accessToken
|
registry.AccessToken = *accessToken
|
||||||
registry.AccessTokenExpiry = expiryAt.Unix()
|
registry.AccessTokenExpiry = expiryAt.Unix()
|
||||||
|
|
||||||
err = dataStore.Registry().Update(registry.ID, registry)
|
return tx.Registry().Update(registry.ID, registry)
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
||||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
return ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
|
||||||
return ecrClient.ParseAuthorizationToken(registry.AccessToken)
|
ParseAuthorizationToken(registry.AccessToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
|
func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||||
if registry.Type == portainer.EcrRegistry {
|
if registry.Type != portainer.EcrRegistry {
|
||||||
if isRegTokenValid(registry) {
|
return nil
|
||||||
log.Debug().Msg("current ECR token is still valid")
|
|
||||||
} else {
|
|
||||||
err = doGetRegToken(dataStore, registry)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug().Msg("refresh ECR token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if isRegTokenValid(registry) {
|
||||||
|
log.Debug().Msg("current ECR token is still valid")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := doGetRegToken(tx, registry); err != nil {
|
||||||
|
log.Debug().Msg("refresh ECR token")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRegEffectiveCredential(registry *portainer.Registry) (username, password string, err error) {
|
func GetRegEffectiveCredential(registry *portainer.Registry) (username, password string, err error) {
|
||||||
|
username = registry.Username
|
||||||
|
password = registry.Password
|
||||||
|
|
||||||
if registry.Type == portainer.EcrRegistry {
|
if registry.Type == portainer.EcrRegistry {
|
||||||
username, password, err = parseRegToken(registry)
|
username, password, err = parseRegToken(registry)
|
||||||
} else {
|
|
||||||
username = registry.Username
|
|
||||||
password = registry.Password
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ portainer.ComposeStackManager = &composeStackManager{}
|
||||||
|
|
||||||
type composeStackManager struct{}
|
type composeStackManager struct{}
|
||||||
|
|
||||||
func NewComposeStackManager() *composeStackManager {
|
func NewComposeStackManager() *composeStackManager {
|
||||||
@@ -31,6 +33,6 @@ func (manager *composeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *composeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *composeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -1367,7 +1367,13 @@ type (
|
|||||||
ValidateFlags(flags *CLIFlags) error
|
ValidateFlags(flags *CLIFlags) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComposeOptions struct {
|
||||||
|
Registries []Registry
|
||||||
|
}
|
||||||
|
|
||||||
ComposeUpOptions struct {
|
ComposeUpOptions struct {
|
||||||
|
ComposeOptions
|
||||||
|
|
||||||
// ForceRecreate forces to recreate containers
|
// ForceRecreate forces to recreate containers
|
||||||
ForceRecreate bool
|
ForceRecreate bool
|
||||||
// AbortOnContainerExit will stop the deployment if a container exits.
|
// AbortOnContainerExit will stop the deployment if a container exits.
|
||||||
@@ -1379,6 +1385,8 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComposeRunOptions struct {
|
ComposeRunOptions struct {
|
||||||
|
ComposeOptions
|
||||||
|
|
||||||
// Remove will remove the container after it has stopped
|
// Remove will remove the container after it has stopped
|
||||||
Remove bool
|
Remove bool
|
||||||
// Args are the arguments to pass to the container
|
// Args are the arguments to pass to the container
|
||||||
@@ -1394,7 +1402,7 @@ type (
|
|||||||
Run(ctx context.Context, stack *Stack, endpoint *Endpoint, serviceName string, options ComposeRunOptions) error
|
Run(ctx context.Context, stack *Stack, endpoint *Endpoint, serviceName string, options ComposeRunOptions) error
|
||||||
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeUpOptions) error
|
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeUpOptions) error
|
||||||
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
||||||
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CryptoService represents a service for encrypting/hashing data
|
// CryptoService represents a service for encrypting/hashing data
|
||||||
@@ -1609,7 +1617,7 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "2.24.0"
|
APIVersion = "2.24.1"
|
||||||
// Edition is what this edition of Portainer is called
|
// Edition is what this edition of Portainer is called
|
||||||
Edition = PortainerCE
|
Edition = PortainerCE
|
||||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||||
|
|||||||
@@ -58,23 +58,25 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
|
|||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
d.swarmStackManager.Login(registries, endpoint)
|
options := portainer.ComposeOptions{Registries: registries}
|
||||||
defer d.swarmStackManager.Logout(endpoint)
|
|
||||||
|
|
||||||
// --force-recreate doesn't pull updated images
|
// --force-recreate doesn't pull updated images
|
||||||
if forcePullImage {
|
if forcePullImage {
|
||||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint); err != nil {
|
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
if err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
||||||
ForceRecreate: forceRecreate,
|
ComposeOptions: options,
|
||||||
})
|
ForceRecreate: forceRecreate,
|
||||||
if err != nil {
|
}); err != nil {
|
||||||
d.composeStackManager.Down(context.TODO(), stack, endpoint)
|
d.composeStackManager.Down(context.TODO(), stack, endpoint)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
|||||||
|
|
||||||
// --force-recreate doesn't pull updated images
|
// --force-recreate doesn't pull updated images
|
||||||
if forcePullImage {
|
if forcePullImage {
|
||||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, portainer.ComposeOptions{}); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,82 +5,110 @@
|
|||||||
library css for buttons is overriden by `.widget .widget-body button`
|
library css for buttons is overriden by `.widget .widget-body button`
|
||||||
so we have to force margin: 0
|
so we have to force margin: 0
|
||||||
*/
|
*/
|
||||||
.react-datetime-picker .react-calendar button {
|
.react-daterange-picker__calendar .react-calendar button {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Extending Calendar.css from react-datetime-picker
|
Extending Calendar.css from react-daterange-picker__calendar
|
||||||
*/
|
*/
|
||||||
.react-datetime-picker .react-calendar {
|
.react-daterange-picker__calendar .react-calendar {
|
||||||
background: var(--bg-calendar-color);
|
background: var(--bg-calendar-color);
|
||||||
color: var(--text-main-color);
|
color: var(--text-main-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* calendar nav buttons */
|
/* calendar nav buttons */
|
||||||
.react-datetime-picker .react-calendar__navigation button:disabled {
|
.react-daterange-picker__calendar .react-calendar__navigation button:disabled {
|
||||||
background-color: var(--bg-calendar-color);
|
background: var(--bg-calendar-color);
|
||||||
@apply opacity-60;
|
@apply opacity-60;
|
||||||
@apply brightness-95 th-dark:brightness-110;
|
@apply brightness-95 th-dark:brightness-110;
|
||||||
}
|
}
|
||||||
.react-datetime-picker .react-calendar__navigation button:enabled:hover,
|
.react-daterange-picker__calendar .react-calendar__navigation button:enabled:hover,
|
||||||
.react-datetime-picker .react-calendar__navigation button:enabled:focus {
|
.react-daterange-picker__calendar .react-calendar__navigation button:enabled:focus {
|
||||||
background-color: var(--bg-daterangepicker-color);
|
background: var(--bg-daterangepicker-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* date tile */
|
/* date tile */
|
||||||
.react-datetime-picker .react-calendar__tile:disabled {
|
.react-daterange-picker__calendar .react-calendar__tile:disabled {
|
||||||
background-color: var(--bg-calendar-color);
|
background: var(--bg-calendar-color);
|
||||||
@apply opacity-60;
|
@apply opacity-60;
|
||||||
@apply brightness-95 th-dark:brightness-110;
|
@apply brightness-95 th-dark:brightness-110;
|
||||||
}
|
}
|
||||||
.react-datetime-picker .react-calendar__tile:enabled:hover,
|
.react-daterange-picker__calendar .react-calendar__tile:enabled:hover,
|
||||||
.react-datetime-picker .react-calendar__tile:enabled:focus {
|
.react-daterange-picker__calendar .react-calendar__tile:enabled:focus {
|
||||||
background-color: var(--bg-daterangepicker-hover);
|
background: var(--bg-daterangepicker-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* today's date tile */
|
/* today's date tile */
|
||||||
.react-datetime-picker .react-calendar__tile--now {
|
.react-daterange-picker__calendar .react-calendar__tile--now {
|
||||||
/* use background color to avoid white on yellow in dark/high contrast modes */
|
|
||||||
@apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)];
|
@apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)];
|
||||||
|
border-radius: 0.25rem !important;
|
||||||
}
|
}
|
||||||
.react-datetime-picker .react-calendar__tile--now:enabled:hover,
|
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:hover,
|
||||||
.react-datetime-picker .react-calendar__tile--now:enabled:focus {
|
.react-daterange-picker__calendar .react-calendar__tile--now:enabled:focus {
|
||||||
background: var(--bg-daterangepicker-hover);
|
background: var(--bg-daterangepicker-hover);
|
||||||
color: var(--text-daterangepicker-hover);
|
color: var(--text-daterangepicker-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* probably date tile in range */
|
/* probably date tile in range */
|
||||||
.react-datetime-picker .react-calendar__tile--hasActive {
|
.react-daterange-picker__calendar .react-calendar__tile--hasActive {
|
||||||
background: var(--bg-daterangepicker-end-date);
|
background: var(--bg-daterangepicker-end-date);
|
||||||
color: var(--text-daterangepicker-end-date);
|
color: var(--text-daterangepicker-end-date);
|
||||||
}
|
}
|
||||||
.react-datetime-picker .react-calendar__tile--hasActive:enabled:hover,
|
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:hover,
|
||||||
.react-datetime-picker .react-calendar__tile--hasActive:enabled:focus {
|
.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:focus {
|
||||||
background: var(--bg-daterangepicker-hover);
|
background: var(--bg-daterangepicker-hover);
|
||||||
color: var(--text-daterangepicker-hover);
|
color: var(--text-daterangepicker-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* selected date tile */
|
.react-daterange-picker__calendar .react-calendar__tile--active:enabled:hover,
|
||||||
.react-datetime-picker .react-calendar__tile--active {
|
.react-daterange-picker__calendar .react-calendar__tile--active:enabled:focus {
|
||||||
background: var(--bg-daterangepicker-active);
|
|
||||||
color: var(--text-daterangepicker-active);
|
|
||||||
}
|
|
||||||
.react-datetime-picker .react-calendar__tile--active:enabled:hover,
|
|
||||||
.react-datetime-picker .react-calendar__tile--active:enabled:focus {
|
|
||||||
background: var(--bg-daterangepicker-hover);
|
background: var(--bg-daterangepicker-hover);
|
||||||
color: var(--text-daterangepicker-hover);
|
color: var(--text-daterangepicker-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-daterange-picker__calendar
|
||||||
|
.react-calendar__month-view__days__day:hover:not(.react-daterange-picker__calendar .react-calendar__tile--hoverEnd):not(
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--hoverStart
|
||||||
|
):not(.react-calendar__tile--active) {
|
||||||
|
border-radius: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* on range select hover */
|
/* on range select hover */
|
||||||
.react-datetime-picker .react-calendar--selectRange .react-calendar__tile--hover {
|
.react-daterange-picker__calendar .react-calendar--selectRange .react-calendar__tile--hover {
|
||||||
background-color: var(--bg-daterangepicker-in-range);
|
background: var(--bg-daterangepicker-in-range);
|
||||||
color: var(--text-daterangepicker-in-range);
|
color: var(--text-daterangepicker-in-range);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Extending DateTimePicker.css from react-datetime-picker
|
Extending DateTimePicker.css from react-daterange-picker__calendar
|
||||||
*/
|
*/
|
||||||
.react-datetime-picker .react-datetime-picker--disabled {
|
.react-daterange-picker__calendar .react-daterange-picker__calendar--disabled {
|
||||||
@apply opacity-40;
|
@apply opacity-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* selected date tile */
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--active {
|
||||||
|
background: var(--bg-daterangepicker-active) !important;
|
||||||
|
color: var(--text-daterangepicker-active) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--rangeStart:not(.react-calendar__tile--rangeEnd),
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--hoverStart {
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--rangeEnd:not(.react-calendar__tile--rangeStart),
|
||||||
|
.react-daterange-picker__calendar .react-calendar__tile--hoverEnd {
|
||||||
|
border-top-right-radius: 0.25rem;
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-daterange-picker__calendar .react-calendar__month-view__days__day--weekend {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-calendar__tile--active.react-calendar__month-view__days__day--weekend {
|
||||||
|
color: var(--text-daterangepicker-active);
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export async function createSwarmStackFromGit({
|
|||||||
}: SwarmGitRepositoryPayload) {
|
}: SwarmGitRepositoryPayload) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post<Stack>(
|
const { data } = await axios.post<Stack>(
|
||||||
buildCreateUrl('standalone', 'repository'),
|
buildCreateUrl('swarm', 'repository'),
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
params: { endpointId: environmentId },
|
params: { endpointId: environmentId },
|
||||||
|
|||||||
@@ -14,13 +14,8 @@ type StringPortBinding = {
|
|||||||
containerPort: number;
|
containerPort: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NumericPortBinding = {
|
|
||||||
hostPort: number;
|
|
||||||
protocol: Protocol;
|
|
||||||
containerPort: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RangePortBinding = {
|
type RangePortBinding = {
|
||||||
|
hostIp: string;
|
||||||
hostPort: Range;
|
hostPort: Range;
|
||||||
protocol: Protocol;
|
protocol: Protocol;
|
||||||
containerPort: Range;
|
containerPort: Range;
|
||||||
@@ -42,9 +37,7 @@ export function toViewModel(portBindings: PortMap): Values {
|
|||||||
return value === 'tcp' || value === 'udp';
|
return value === 'tcp' || value === 'udp';
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePorts(
|
function parsePorts(portBindings: PortMap): Array<StringPortBinding> {
|
||||||
portBindings: PortMap
|
|
||||||
): Array<StringPortBinding | NumericPortBinding> {
|
|
||||||
return Object.entries(portBindings).flatMap(([key, bindings]) => {
|
return Object.entries(portBindings).flatMap(([key, bindings]) => {
|
||||||
const [containerPort, protocol] = key.split('/');
|
const [containerPort, protocol] = key.split('/');
|
||||||
|
|
||||||
@@ -63,15 +56,24 @@ export function toViewModel(portBindings: PortMap): Values {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return bindings.map((binding) => {
|
return bindings.map((binding) => {
|
||||||
|
let port = '';
|
||||||
|
if (binding.HostPort) {
|
||||||
|
port = binding.HostPort;
|
||||||
|
}
|
||||||
|
if (binding.HostIp) {
|
||||||
|
port = `${binding.HostIp}:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (binding.HostPort?.includes('-')) {
|
if (binding.HostPort?.includes('-')) {
|
||||||
|
// Range port
|
||||||
return {
|
return {
|
||||||
hostPort: binding.HostPort,
|
hostPort: port,
|
||||||
protocol,
|
protocol,
|
||||||
containerPort: containerPortNumber,
|
containerPort: containerPortNumber,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
hostPort: parseInt(binding.HostPort || '0', 10),
|
hostPort: port,
|
||||||
protocol,
|
protocol,
|
||||||
containerPort: containerPortNumber,
|
containerPort: containerPortNumber,
|
||||||
};
|
};
|
||||||
@@ -79,9 +81,9 @@ export function toViewModel(portBindings: PortMap): Values {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortPorts(ports: Array<StringPortBinding | NumericPortBinding>) {
|
function sortPorts(ports: Array<StringPortBinding>) {
|
||||||
const rangePorts = ports.filter(isStringPortBinding);
|
const rangePorts = ports.filter(isRangePortBinding);
|
||||||
const nonRangePorts = ports.filter(isNumericPortBinding);
|
const nonRangePorts = ports.filter((port) => !isRangePortBinding(port));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rangePorts,
|
rangePorts,
|
||||||
@@ -93,27 +95,40 @@ export function toViewModel(portBindings: PortMap): Values {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function combinePorts(ports: Array<NumericPortBinding>) {
|
function combinePorts(ports: Array<StringPortBinding>) {
|
||||||
return ports
|
return ports
|
||||||
.reduce((acc, port) => {
|
.reduce((acc, port) => {
|
||||||
|
let hostIp = '';
|
||||||
|
let hostPort = 0;
|
||||||
|
if (port.hostPort.includes(':')) {
|
||||||
|
const [ipStr, portStr] = port.hostPort.split(':');
|
||||||
|
hostIp = ipStr;
|
||||||
|
hostPort = parseInt(portStr || '0', 10);
|
||||||
|
} else {
|
||||||
|
hostPort = parseInt(port.hostPort || '0', 10);
|
||||||
|
}
|
||||||
|
|
||||||
const lastPort = acc[acc.length - 1];
|
const lastPort = acc[acc.length - 1];
|
||||||
if (
|
if (
|
||||||
lastPort &&
|
lastPort &&
|
||||||
|
lastPort.hostIp === hostIp &&
|
||||||
lastPort.containerPort.end === port.containerPort - 1 &&
|
lastPort.containerPort.end === port.containerPort - 1 &&
|
||||||
lastPort.hostPort.end === port.hostPort - 1 &&
|
lastPort.hostPort.end === hostPort - 1 &&
|
||||||
lastPort.protocol === port.protocol
|
lastPort.protocol === port.protocol
|
||||||
) {
|
) {
|
||||||
|
lastPort.hostIp = hostIp;
|
||||||
lastPort.containerPort.end = port.containerPort;
|
lastPort.containerPort.end = port.containerPort;
|
||||||
lastPort.hostPort.end = port.hostPort;
|
lastPort.hostPort.end = hostPort;
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...acc,
|
...acc,
|
||||||
{
|
{
|
||||||
|
hostIp,
|
||||||
hostPort: {
|
hostPort: {
|
||||||
start: port.hostPort,
|
start: hostPort,
|
||||||
end: port.hostPort,
|
end: hostPort,
|
||||||
},
|
},
|
||||||
containerPort: {
|
containerPort: {
|
||||||
start: port.containerPort,
|
start: port.containerPort,
|
||||||
@@ -123,34 +138,32 @@ export function toViewModel(portBindings: PortMap): Values {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [] as Array<RangePortBinding>)
|
}, [] as Array<RangePortBinding>)
|
||||||
.map(({ protocol, containerPort, hostPort }) => ({
|
.map(({ protocol, containerPort, hostPort, hostIp }) => ({
|
||||||
hostPort: getRange(hostPort.start, hostPort.end),
|
hostPort: getRange(hostPort.start, hostPort.end, hostIp),
|
||||||
containerPort: getRange(containerPort.start, containerPort.end),
|
containerPort: getRange(containerPort.start, containerPort.end),
|
||||||
protocol,
|
protocol,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getRange(start: number, end: number): string {
|
function getRange(start: number, end: number, hostIp?: string): string {
|
||||||
if (start === end) {
|
if (start === end) {
|
||||||
if (start === 0) {
|
if (start === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hostIp) {
|
||||||
|
return `${hostIp}:${start}`;
|
||||||
|
}
|
||||||
return start.toString();
|
return start.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hostIp) {
|
||||||
|
return `${hostIp}:${start}-${end}`;
|
||||||
|
}
|
||||||
return `${start}-${end}`;
|
return `${start}-${end}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNumericPortBinding(
|
function isRangePortBinding(port: StringPortBinding): boolean {
|
||||||
port: StringPortBinding | NumericPortBinding
|
return port.hostPort.includes('-');
|
||||||
): port is NumericPortBinding {
|
|
||||||
return port.hostPort !== 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStringPortBinding(
|
|
||||||
port: StringPortBinding | NumericPortBinding
|
|
||||||
): port is StringPortBinding {
|
|
||||||
return port.hostPort === 'string';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,15 @@ export async function buildImageFromDockerfileContentAndFiles(
|
|||||||
const dockerfile = new Blob([content], { type: 'text/plain' });
|
const dockerfile = new Blob([content], { type: 'text/plain' });
|
||||||
const uploadFiles = [dockerfile, ...files];
|
const uploadFiles = [dockerfile, ...files];
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
uploadFiles.forEach((file, index) => {
|
||||||
|
formData.append(`file${index}`, file);
|
||||||
|
});
|
||||||
|
|
||||||
return buildImage(
|
return buildImage(
|
||||||
environmentId,
|
environmentId,
|
||||||
{ t: names },
|
{ t: names },
|
||||||
{ file: uploadFiles },
|
formData,
|
||||||
'multipart/form-data'
|
'multipart/form-data'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,27 @@ export function useUpdateK8sConfigMapMutation(
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
data,
|
configMap,
|
||||||
configMapName,
|
configMapName,
|
||||||
}: {
|
}: {
|
||||||
data: ConfigMap;
|
configMap: ConfigMap;
|
||||||
configMapName: string;
|
configMapName: string;
|
||||||
}) => updateConfigMap(environmentId, namespace, configMapName, data),
|
}) => {
|
||||||
|
if (!configMap.metadata?.uid) {
|
||||||
|
return createConfigMap(
|
||||||
|
environmentId,
|
||||||
|
namespace,
|
||||||
|
configMapName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return updateConfigMap(
|
||||||
|
environmentId,
|
||||||
|
namespace,
|
||||||
|
configMapName,
|
||||||
|
configMap
|
||||||
|
);
|
||||||
|
},
|
||||||
...withInvalidate(queryClient, [
|
...withInvalidate(queryClient, [
|
||||||
configMapQueryKeys.configMaps(environmentId, namespace),
|
configMapQueryKeys.configMaps(environmentId, namespace),
|
||||||
]),
|
]),
|
||||||
@@ -50,3 +65,22 @@ async function updateConfigMap(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createConfigMap(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
namespace: string,
|
||||||
|
configMap: string,
|
||||||
|
data: ConfigMap
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return axios.post(
|
||||||
|
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/configmaps`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseKubernetesAxiosError(
|
||||||
|
e,
|
||||||
|
`Unable to create ConfigMap '${configMap}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function AccessDatatable() {
|
|||||||
configMap
|
configMap
|
||||||
);
|
);
|
||||||
await updateConfigMapMutation.mutateAsync({
|
await updateConfigMapMutation.mutateAsync({
|
||||||
data: configMapPayload,
|
configMap: configMapPayload,
|
||||||
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
});
|
});
|
||||||
notifySuccess('Success', 'Namespace access updated');
|
notifySuccess('Success', 'Namespace access updated');
|
||||||
|
|||||||
+23
-3
@@ -12,6 +12,8 @@ import { useConfigMap } from '@/react/kubernetes/configs/queries/useConfigMap';
|
|||||||
import { useTeams } from '@/react/portainer/users/teams/queries';
|
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||||
import { useUpdateK8sConfigMapMutation } from '@/react/kubernetes/configs/queries/useUpdateK8sConfigMapMutation';
|
import { useUpdateK8sConfigMapMutation } from '@/react/kubernetes/configs/queries/useUpdateK8sConfigMapMutation';
|
||||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
import { Configuration } from '@/react/kubernetes/configs/types';
|
||||||
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
@@ -28,6 +30,7 @@ export function CreateAccessWidget() {
|
|||||||
const {
|
const {
|
||||||
params: { id: namespaceName },
|
params: { id: namespaceName },
|
||||||
} = useCurrentStateAndParams();
|
} = useCurrentStateAndParams();
|
||||||
|
const { user } = useCurrentUser();
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const isRBACEnabledQuery = useIsRBACEnabled(environmentId);
|
const isRBACEnabledQuery = useIsRBACEnabled(environmentId);
|
||||||
const initialValues: {
|
const initialValues: {
|
||||||
@@ -75,7 +78,9 @@ export function CreateAccessWidget() {
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={onSubmit}
|
onSubmit={(values, formikHelpers) =>
|
||||||
|
onSubmit(values, formikHelpers)
|
||||||
|
}
|
||||||
validateOnMount
|
validateOnMount
|
||||||
>
|
>
|
||||||
{(formikProps) => (
|
{(formikProps) => (
|
||||||
@@ -104,10 +109,10 @@ export function CreateAccessWidget() {
|
|||||||
namespaceAccesses,
|
namespaceAccesses,
|
||||||
values.selectedUsersAndTeams,
|
values.selectedUsersAndTeams,
|
||||||
namespaceName,
|
namespaceName,
|
||||||
configMap
|
configMap ?? newConfigMap(user.Username, user.Id)
|
||||||
);
|
);
|
||||||
await updateConfigMapMutation.mutateAsync({
|
await updateConfigMapMutation.mutateAsync({
|
||||||
data: configMapPayload,
|
configMap: configMapPayload,
|
||||||
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
});
|
});
|
||||||
notifySuccess('Success', 'Namespace access updated');
|
notifySuccess('Success', 'Namespace access updated');
|
||||||
@@ -117,3 +122,18 @@ export function CreateAccessWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newConfigMap(userName: string, userId: number) {
|
||||||
|
const configMap: Configuration = {
|
||||||
|
Type: 1,
|
||||||
|
UID: '',
|
||||||
|
Name: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||||
|
Namespace: PortainerNamespaceAccessesConfigMap.namespace,
|
||||||
|
Data: { [PortainerNamespaceAccessesConfigMap.accessKey]: '{}' },
|
||||||
|
ConfigurationOwner: userName,
|
||||||
|
ConfigurationOwnerId: `${userId}`,
|
||||||
|
IsUsed: false,
|
||||||
|
Yaml: '',
|
||||||
|
};
|
||||||
|
return configMap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
export interface ActivityLog {
|
interface BaseActivityLog {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
action: string;
|
action: string;
|
||||||
context: string;
|
context: string;
|
||||||
id: number;
|
id: number;
|
||||||
payload: object;
|
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
export interface ActivityLogResponse extends BaseActivityLog {
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityLog extends BaseActivityLog {
|
||||||
|
payload: string | object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityLogsResponse {
|
||||||
|
logs: Array<ActivityLogResponse>;
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|||||||
|
|
||||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import { ActivityLog } from './types';
|
import { ActivityLogResponse, ActivityLogsResponse } from './types';
|
||||||
|
|
||||||
export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
|
export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
|
||||||
export type SortKey = (typeof sortKeys)[number];
|
export type SortKey = (typeof sortKeys)[number];
|
||||||
@@ -30,19 +30,18 @@ export function useActivityLogs(query: Query) {
|
|||||||
queryKey: ['activityLogs', query] as const,
|
queryKey: ['activityLogs', query] as const,
|
||||||
queryFn: () => fetchActivityLogs(query),
|
queryFn: () => fetchActivityLogs(query),
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
select: (data) => ({
|
||||||
|
...data,
|
||||||
|
logs: decorateLogs(data.logs),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActivityLogsResponse {
|
|
||||||
logs: Array<ActivityLog>;
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
|
async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
|
||||||
try {
|
try {
|
||||||
if (!isBE) {
|
if (!isBE) {
|
||||||
return {
|
return {
|
||||||
logs: [{}, {}, {}, {}, {}] as Array<ActivityLog>,
|
logs: [{}, {}, {}, {}, {}] as Array<ActivityLogResponse>,
|
||||||
totalCount: 5,
|
totalCount: 5,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -56,3 +55,40 @@ async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
|
|||||||
throw parseAxiosError(err, 'Failed loading user activity logs csv');
|
throw parseAxiosError(err, 'Failed loading user activity logs csv');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorates logs with the payload parsed from base64
|
||||||
|
*/
|
||||||
|
function decorateLogs(logs?: ActivityLogResponse[]) {
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs.map((log) => ({
|
||||||
|
...log,
|
||||||
|
payload: parseBase64AsObject(log.payload),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBase64AsObject(value: string): string | object {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(safeAtob(value));
|
||||||
|
} catch (err) {
|
||||||
|
return safeAtob(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeAtob(value: string) {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return window.atob(value);
|
||||||
|
} catch (err) {
|
||||||
|
// If the payload is not base64 encoded, return the original value
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1
|
github.com/aws/aws-sdk-go-v2 v1.24.1
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1
|
||||||
github.com/cbroglie/mustache v1.4.0
|
github.com/cbroglie/mustache v1.4.0
|
||||||
github.com/compose-spec/compose-go/v2 v2.0.2
|
github.com/compose-spec/compose-go/v2 v2.0.2
|
||||||
github.com/containers/image/v5 v5.30.1
|
github.com/containers/image/v5 v5.30.1
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
|||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
|
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
|
||||||
@@ -66,16 +65,14 @@ github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5g
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 h1:AAZJJAENsQ4yYbnfvqPZT8Nc1YlEd5CZ4usymlC2b4U=
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1 h1:zqXEIhuR7RcHob2gxB/Xf1X4XuMS0vapn7xr+wCPrpg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0/go.mod h1:a3WUi3JjM3MFtIYenSYPJ7UZPXsw7U7vzebnynxucks=
|
github.com/aws/aws-sdk-go-v2/service/ecr v1.24.1/go.mod h1:+rWYJfms9p+D/wUN599tx3FtWvxoXCP25b8Porlrxcc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||||
@@ -86,7 +83,6 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||||
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
|
||||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||||
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
@@ -315,7 +311,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "2.24.0",
|
"version": "2.24.1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/portainer/portainer/pkg/libstack"
|
"github.com/portainer/portainer/pkg/libstack"
|
||||||
|
|
||||||
@@ -18,9 +18,12 @@ import (
|
|||||||
"github.com/docker/cli/cli/flags"
|
"github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/compose/v2/pkg/api"
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
"github.com/docker/compose/v2/pkg/compose"
|
"github.com/docker/compose/v2/pkg/compose"
|
||||||
|
"github.com/docker/docker/registry"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
func withCli(
|
func withCli(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
options libstack.Options,
|
options libstack.Options,
|
||||||
@@ -39,25 +42,20 @@ func withCli(
|
|||||||
opts.Hosts = []string{options.Host}
|
opts.Hosts = []string{options.Host}
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "docker-config")
|
mu.Lock()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create a temporary directory for the Docker config: %w", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
opts.ConfigDir = tempDir
|
|
||||||
|
|
||||||
if err := cli.Initialize(opts); err != nil {
|
if err := cli.Initialize(opts); err != nil {
|
||||||
|
mu.Unlock()
|
||||||
return fmt.Errorf("unable to initialize the Docker client: %w", err)
|
return fmt.Errorf("unable to initialize the Docker client: %w", err)
|
||||||
}
|
}
|
||||||
|
mu.Unlock()
|
||||||
defer cli.Client().Close()
|
defer cli.Client().Close()
|
||||||
|
|
||||||
for _, r := range options.Registries {
|
for _, r := range options.Registries {
|
||||||
creds := cli.ConfigFile().GetCredentialsStore(r.ServerAddress)
|
if r.ServerAddress == "" || r.ServerAddress == registry.DefaultNamespace {
|
||||||
|
r.ServerAddress = registry.IndexServer
|
||||||
if err := creds.Store(r); err != nil {
|
|
||||||
return fmt.Errorf("unable to store the Docker credentials: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cli.ConfigFile().AuthConfigs[r.ServerAddress] = r
|
||||||
}
|
}
|
||||||
|
|
||||||
return cliFn(ctx, cli)
|
return cliFn(ctx, cli)
|
||||||
@@ -72,29 +70,24 @@ func withComposeService(
|
|||||||
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
|
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
|
||||||
composeService := compose.NewComposeService(cli)
|
composeService := compose.NewComposeService(cli)
|
||||||
|
|
||||||
configDetails := types.ConfigDetails{WorkingDir: options.WorkingDir}
|
if len(filePaths) == 0 {
|
||||||
|
return composeFn(composeService, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := parseEnvironment(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configDetails := types.ConfigDetails{
|
||||||
|
Environment: env,
|
||||||
|
WorkingDir: filepath.Dir(filePaths[0]),
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range filePaths {
|
for _, p := range filePaths {
|
||||||
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
|
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
|
||||||
}
|
}
|
||||||
|
|
||||||
envFile := make(map[string]string)
|
|
||||||
|
|
||||||
if options.EnvFilePath != "" {
|
|
||||||
env, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get the environment from the env file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
maps.Copy(envFile, env)
|
|
||||||
|
|
||||||
configDetails.Environment = env
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(configDetails.ConfigFiles) == 0 {
|
|
||||||
return composeFn(composeService, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := loader.LoadWithContext(ctx, configDetails,
|
project, err := loader.LoadWithContext(ctx, configDetails,
|
||||||
func(o *loader.Options) {
|
func(o *loader.Options) {
|
||||||
o.SkipResolveEnvironment = true
|
o.SkipResolveEnvironment = true
|
||||||
@@ -109,21 +102,20 @@ func withComposeService(
|
|||||||
return fmt.Errorf("failed to load the compose file: %w", err)
|
return fmt.Errorf("failed to load the compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.EnvFilePath != "" {
|
// Work around compose path handling
|
||||||
// Work around compose path handling
|
for i, service := range project.Services {
|
||||||
for i, service := range project.Services {
|
for j, envFile := range service.EnvFiles {
|
||||||
for j, envFile := range service.EnvFiles {
|
if !filepath.IsAbs(envFile.Path) {
|
||||||
if !filepath.IsAbs(envFile.Path) {
|
project.Services[i].EnvFiles[j].Path = filepath.Join(configDetails.WorkingDir, envFile.Path)
|
||||||
project.Services[i].EnvFiles[j].Path = filepath.Join(project.WorkingDir, envFile.Path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
|
// Set the services environment variables
|
||||||
project = p
|
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
|
||||||
} else {
|
project = p
|
||||||
return fmt.Errorf("failed to resolve services environment: %w", err)
|
} else {
|
||||||
}
|
return fmt.Errorf("failed to resolve services environment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return composeFn(composeService, project)
|
return composeFn(composeService, project)
|
||||||
@@ -133,7 +125,9 @@ func withComposeService(
|
|||||||
// Deploy creates and starts containers
|
// Deploy creates and starts containers
|
||||||
func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, options libstack.DeployOptions) error {
|
func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, options libstack.DeployOptions) error {
|
||||||
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||||
addServiceLabels(project)
|
addServiceLabels(project, false)
|
||||||
|
|
||||||
|
project = project.WithoutUnnecessaryResources()
|
||||||
|
|
||||||
var opts api.UpOptions
|
var opts api.UpOptions
|
||||||
if options.ForceRecreate {
|
if options.ForceRecreate {
|
||||||
@@ -143,6 +137,10 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
|
|||||||
opts.Create.RemoveOrphans = options.RemoveOrphans
|
opts.Create.RemoveOrphans = options.RemoveOrphans
|
||||||
opts.Start.CascadeStop = options.AbortOnContainerExit
|
opts.Start.CascadeStop = options.AbortOnContainerExit
|
||||||
|
|
||||||
|
if err := composeService.Build(ctx, project, api.BuildOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("compose build operation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := composeService.Up(ctx, project, opts); err != nil {
|
if err := composeService.Up(ctx, project, opts); err != nil {
|
||||||
return fmt.Errorf("compose up operation failed: %w", err)
|
return fmt.Errorf("compose up operation failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -153,14 +151,31 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run runs the given service just once, without considering dependencies
|
||||||
func (c *ComposeDeployer) Run(ctx context.Context, filePaths []string, serviceName string, options libstack.RunOptions) error {
|
func (c *ComposeDeployer) Run(ctx context.Context, filePaths []string, serviceName string, options libstack.RunOptions) error {
|
||||||
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||||
addServiceLabels(project)
|
addServiceLabels(project, true)
|
||||||
|
|
||||||
|
for name, service := range project.Services {
|
||||||
|
if name == serviceName {
|
||||||
|
project.DisabledServices[serviceName] = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Services = make(types.Services)
|
||||||
|
|
||||||
|
if err := composeService.Create(ctx, project, api.CreateOptions{RemoveOrphans: true}); err != nil {
|
||||||
|
return fmt.Errorf("compose create operation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(project.Services, project.DisabledServices)
|
||||||
|
project.DisabledServices = make(types.Services)
|
||||||
|
|
||||||
opts := api.RunOptions{
|
opts := api.RunOptions{
|
||||||
AutoRemove: options.Remove,
|
AutoRemove: options.Remove,
|
||||||
Command: options.Args,
|
Command: options.Args,
|
||||||
Detach: options.Detached,
|
Detach: options.Detached,
|
||||||
|
Service: serviceName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := composeService.RunOneOffContainer(ctx, project, opts); err != nil {
|
if _, err := composeService.RunOneOffContainer(ctx, project, opts); err != nil {
|
||||||
@@ -208,6 +223,7 @@ func (c *ComposeDeployer) Validate(ctx context.Context, filePaths []string, opti
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config returns the compose file with the paths resolved
|
||||||
func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, options libstack.Options) ([]byte, error) {
|
func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, options libstack.Options) ([]byte, error) {
|
||||||
var payload []byte
|
var payload []byte
|
||||||
|
|
||||||
@@ -226,16 +242,47 @@ func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, option
|
|||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addServiceLabels(project *types.Project) {
|
func addServiceLabels(project *types.Project, oneOff bool) {
|
||||||
|
oneOffLabel := "False"
|
||||||
|
if oneOff {
|
||||||
|
oneOffLabel = "True"
|
||||||
|
}
|
||||||
|
|
||||||
for i, s := range project.Services {
|
for i, s := range project.Services {
|
||||||
s.CustomLabels = map[string]string{
|
s.CustomLabels = map[string]string{
|
||||||
api.ProjectLabel: project.Name,
|
api.ProjectLabel: project.Name,
|
||||||
api.ServiceLabel: s.Name,
|
api.ServiceLabel: s.Name,
|
||||||
api.VersionLabel: api.ComposeVersion,
|
api.VersionLabel: api.ComposeVersion,
|
||||||
api.WorkingDirLabel: "/",
|
api.WorkingDirLabel: project.WorkingDir,
|
||||||
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
|
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
|
||||||
api.OneoffLabel: "False",
|
api.OneoffLabel: oneOffLabel,
|
||||||
}
|
}
|
||||||
project.Services[i] = s
|
project.Services[i] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseEnvironment(options libstack.Options) (map[string]string, error) {
|
||||||
|
env := make(map[string]string)
|
||||||
|
|
||||||
|
for _, envLine := range options.Env {
|
||||||
|
e, err := dotenv.UnmarshalWithLookup(envLine, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse environment variables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(env, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.EnvFilePath == "" {
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get the environment from the env file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(env, e)
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,17 +35,14 @@ services:
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
filePathOriginal, err := createFile(dir, "docker-compose.yml", composeFileContent)
|
filePathOriginal := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
||||||
require.NoError(t, err)
|
filePathOverride := createFile(t, dir, "docker-compose-override.yml", overrideComposeFileContent)
|
||||||
|
|
||||||
filePathOverride, err := createFile(dir, "docker-compose-override.yml", overrideComposeFileContent)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
filePaths := []string{filePathOriginal, filePathOverride}
|
filePaths := []string{filePathOriginal, filePathOverride}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
err = w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
err := w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = w.Pull(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
err = w.Pull(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
||||||
@@ -62,7 +59,7 @@ services:
|
|||||||
|
|
||||||
require.True(t, containerExists(composeContainerName))
|
require.True(t, containerExists(composeContainerName))
|
||||||
|
|
||||||
waitResult := <-w.WaitForStatus(ctx, projectName, libstack.StatusCompleted, "")
|
waitResult := <-w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
||||||
|
|
||||||
require.Empty(t, waitResult.ErrorMsg)
|
require.Empty(t, waitResult.ErrorMsg)
|
||||||
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
||||||
@@ -73,14 +70,34 @@ services:
|
|||||||
require.False(t, containerExists(composeContainerName))
|
require.False(t, containerExists(composeContainerName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(dir, fileName, content string) (string, error) {
|
func TestRun(t *testing.T) {
|
||||||
|
w := NewComposeDeployer()
|
||||||
|
|
||||||
|
filePath := createFile(t, t.TempDir(), "docker-compose.yml", `
|
||||||
|
services:
|
||||||
|
updater:
|
||||||
|
image: alpine
|
||||||
|
`)
|
||||||
|
|
||||||
|
filePaths := []string{filePath}
|
||||||
|
serviceName := "updater"
|
||||||
|
|
||||||
|
err := w.Run(context.Background(), filePaths, serviceName, libstack.RunOptions{
|
||||||
|
Remove: true,
|
||||||
|
Options: libstack.Options{
|
||||||
|
ProjectName: "project_name",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFile(t *testing.T, dir, fileName, content string) string {
|
||||||
filePath := filepath.Join(dir, fileName)
|
filePath := filepath.Join(dir, fileName)
|
||||||
|
|
||||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
err := os.WriteFile(filePath, []byte(content), 0o644)
|
||||||
return "", err
|
require.NoError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
return filePath, nil
|
return filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
func containerExists(containerName string) bool {
|
func containerExists(containerName string) bool {
|
||||||
@@ -101,8 +118,7 @@ func Test_Validate(t *testing.T) {
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
filePathOriginal, err := createFile(dir, "docker-compose.yml", invalidComposeFileContent)
|
filePathOriginal := createFile(t, dir, "docker-compose.yml", invalidComposeFileContent)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
filePaths := []string{filePathOriginal}
|
filePaths := []string{filePathOriginal}
|
||||||
|
|
||||||
@@ -110,7 +126,7 @@ func Test_Validate(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
err = w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
err := w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,13 +324,11 @@ networks:
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
composeFilePath, err := createFile(dir, "docker-compose.yml", tc.composeFileContent)
|
composeFilePath := createFile(t, dir, "docker-compose.yml", tc.composeFileContent)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
envFilePath := ""
|
envFilePath := ""
|
||||||
if tc.envFileContent != "" {
|
if tc.envFileContent != "" {
|
||||||
envFilePath, err = createFile(dir, "stack.env", tc.envFileContent)
|
envFilePath = createFile(t, dir, "stack.env", tc.envFileContent)
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w := NewComposeDeployer()
|
w := NewComposeDeployer()
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func aggregateStatuses(services []service) (libstack.Status, string) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ComposeDeployer) WaitForStatus(ctx context.Context, name string, status libstack.Status, _ string) <-chan libstack.WaitResult {
|
func (c *ComposeDeployer) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan libstack.WaitResult {
|
||||||
waitResultCh := make(chan libstack.WaitResult)
|
waitResultCh := make(chan libstack.WaitResult)
|
||||||
waitResult := libstack.WaitResult{Status: status}
|
waitResult := libstack.WaitResult{Status: status}
|
||||||
|
|
||||||
@@ -130,7 +130,10 @@ func (c *ComposeDeployer) WaitForStatus(ctx context.Context, name string, status
|
|||||||
|
|
||||||
if err := withComposeService(ctx, nil, libstack.Options{ProjectName: name}, func(composeService api.Service, project *types.Project) error {
|
if err := withComposeService(ctx, nil, libstack.Options{ProjectName: name}, func(composeService api.Service, project *types.Project) error {
|
||||||
var err error
|
var err error
|
||||||
containerSummaries, err = composeService.Ps(ctx, name, api.PsOptions{All: true})
|
|
||||||
|
psCtx, cancelFunc := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancelFunc()
|
||||||
|
containerSummaries, err = composeService.Ps(psCtx, name, api.PsOptions{All: true})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestComposeProjectStatus(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
status, statusMessage, err := waitForStatus(w, ctx, projectName, libstack.StatusRunning, "")
|
status, statusMessage, err := waitForStatus(w, ctx, projectName, libstack.StatusRunning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("[test: %s] Failed to get compose project status: %v", testCase.TestName, err)
|
t.Fatalf("[test: %s] Failed to get compose project status: %v", testCase.TestName, err)
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ func TestComposeProjectStatus(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(20 * time.Second)
|
time.Sleep(20 * time.Second)
|
||||||
|
|
||||||
status, statusMessage, err = waitForStatus(w, ctx, projectName, libstack.StatusRemoved, "")
|
status, statusMessage, err = waitForStatus(w, ctx, projectName, libstack.StatusRemoved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("[test: %s] Failed to get compose project status: %v", testCase.TestName, err)
|
t.Fatalf("[test: %s] Failed to get compose project status: %v", testCase.TestName, err)
|
||||||
}
|
}
|
||||||
@@ -102,11 +102,11 @@ func TestComposeProjectStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForStatus(deployer libstack.Deployer, ctx context.Context, stackName string, requiredStatus libstack.Status, stackFileLocation string) (libstack.Status, string, error) {
|
func waitForStatus(deployer libstack.Deployer, ctx context.Context, stackName string, requiredStatus libstack.Status) (libstack.Status, string, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus, stackFileLocation)
|
statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus)
|
||||||
result := <-statusCh
|
result := <-statusCh
|
||||||
if result.ErrorMsg == "" {
|
if result.ErrorMsg == "" {
|
||||||
return result.Status, "", nil
|
return result.Status, "", nil
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type Deployer interface {
|
|||||||
Pull(ctx context.Context, filePaths []string, options Options) error
|
Pull(ctx context.Context, filePaths []string, options Options) error
|
||||||
Run(ctx context.Context, filePaths []string, serviceName string, options RunOptions) error
|
Run(ctx context.Context, filePaths []string, serviceName string, options RunOptions) error
|
||||||
Validate(ctx context.Context, filePaths []string, options Options) error
|
Validate(ctx context.Context, filePaths []string, options Options) error
|
||||||
WaitForStatus(ctx context.Context, name string, status Status, stackFileLocation string) <-chan WaitResult
|
WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult
|
||||||
Config(ctx context.Context, filePaths []string, options Options) ([]byte, error)
|
Config(ctx context.Context, filePaths []string, options Options) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
bail: 2,
|
bail: 2,
|
||||||
include: ['./app/**/*.test.ts', './app/**/*.test.tsx'],
|
include: ['./app/**/*.test.ts', './app/**/*.test.tsx'],
|
||||||
|
env: {
|
||||||
|
PORTAINER_EDITION: 'CE',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()],
|
plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user