Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c8434c609 | |||
| 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")
|
||||||
}
|
}
|
||||||
|
|||||||
+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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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,7 +70,10 @@ 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}
|
configDetails := types.ConfigDetails{
|
||||||
|
WorkingDir: options.WorkingDir,
|
||||||
|
Environment: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
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})
|
||||||
@@ -133,7 +134,7 @@ 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)
|
||||||
|
|
||||||
var opts api.UpOptions
|
var opts api.UpOptions
|
||||||
if options.ForceRecreate {
|
if options.ForceRecreate {
|
||||||
@@ -153,14 +154,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 +226,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,7 +245,12 @@ 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,
|
||||||
@@ -234,7 +258,7 @@ func addServiceLabels(project *types.Project) {
|
|||||||
api.VersionLabel: api.ComposeVersion,
|
api.VersionLabel: api.ComposeVersion,
|
||||||
api.WorkingDirLabel: "/",
|
api.WorkingDirLabel: "/",
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user