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/pkg/featureflags"
|
||||
"github.com/portainer/portainer/pkg/libhelm"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
@@ -166,26 +165,6 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
|
||||
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 {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||
}
|
||||
@@ -435,9 +414,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
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 {
|
||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||
}
|
||||
|
||||
@@ -31,15 +31,18 @@ type (
|
||||
// RegistryCredentials holds the credentials for a Docker registry.
|
||||
// Used only for EE
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
// Used only for EE
|
||||
EdgeUpdateID int
|
||||
|
||||
@@ -9,27 +9,32 @@ import (
|
||||
"strings"
|
||||
|
||||
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/factory"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ComposeStackManager is a wrapper for docker-compose binary
|
||||
type ComposeStackManager struct {
|
||||
deployer libstack.Deployer
|
||||
proxyManager *proxy.Manager
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
||||
|
||||
// NewComposeStackManager returns a Compose stack manager
|
||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
|
||||
return &ComposeStackManager{
|
||||
deployer: deployer,
|
||||
proxyManager: proxyManager,
|
||||
}, nil
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
},
|
||||
ForceRecreate: options.ForceRecreate,
|
||||
AbortOnContainerExit: options.AbortOnContainerExit,
|
||||
@@ -90,6 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
},
|
||||
Remove: options.Remove,
|
||||
Args: options.Args,
|
||||
@@ -103,8 +110,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
} else if proxy != nil {
|
||||
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,
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
} else if proxy != nil {
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
@@ -140,6 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
})
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// Copy from stack env vars
|
||||
if err = copyConfigEnvVars(envfile, stack.Env); err != nil {
|
||||
if err := copyConfigEnvVars(envfile, stack.Env); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -219,3 +225,49 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
|
||||
}
|
||||
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()
|
||||
|
||||
w, err := NewComposeStackManager(deployer, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating manager: %s", err)
|
||||
}
|
||||
w := NewComposeStackManager(deployer, nil, nil)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -46,8 +45,7 @@ func NewSwarmStackManager(
|
||||
dataStore: datastore,
|
||||
}
|
||||
|
||||
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
||||
if err != nil {
|
||||
if err := manager.updateDockerCLIConfiguration(manager.configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -63,33 +61,14 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, ®istry)
|
||||
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
|
||||
}
|
||||
|
||||
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
||||
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
if err := runCommandAndCaptureStdErr(command, registryArgs, nil, ""); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
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 {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
@@ -167,8 +147,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
@@ -192,6 +171,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
endpointURL = "tcp://" + tunnelAddr
|
||||
}
|
||||
|
||||
@@ -216,6 +196,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
configFilePath := path.Join(configPath, "config.json")
|
||||
|
||||
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -246,8 +227,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
return make(map[string]any), nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, &config)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
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)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
switch {
|
||||
case httperrors.IsInvalidPayloadError(err):
|
||||
return httperror.BadRequest("Invalid payload", err)
|
||||
|
||||
@@ -57,17 +57,15 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var payload updateEdgeStackPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
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)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr
|
||||
@@ -122,14 +120,12 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
||||
stack.EdgeGroups = groupsIds
|
||||
|
||||
if payload.UpdateVersion {
|
||||
err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds)
|
||||
if err != nil {
|
||||
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
|
||||
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)
|
||||
|
||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
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
|
||||
|
||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
registry.AccessToken = *accessToken
|
||||
registry.AccessTokenExpiry = expiryAt.Unix()
|
||||
|
||||
err = dataStore.Registry().Update(registry.ID, registry)
|
||||
|
||||
return
|
||||
return tx.Registry().Update(registry.ID, registry)
|
||||
}
|
||||
|
||||
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||
return ecrClient.ParseAuthorizationToken(registry.AccessToken)
|
||||
return ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
|
||||
ParseAuthorizationToken(registry.AccessToken)
|
||||
}
|
||||
|
||||
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
|
||||
if registry.Type == portainer.EcrRegistry {
|
||||
if isRegTokenValid(registry) {
|
||||
log.Debug().Msg("current ECR token is still valid")
|
||||
} else {
|
||||
err = doGetRegToken(dataStore, registry)
|
||||
if err != nil {
|
||||
log.Debug().Msg("refresh ECR token")
|
||||
}
|
||||
}
|
||||
func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
if registry.Type != portainer.EcrRegistry {
|
||||
return nil
|
||||
}
|
||||
|
||||
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) {
|
||||
username = registry.Username
|
||||
password = registry.Password
|
||||
|
||||
if registry.Type == portainer.EcrRegistry {
|
||||
username, password, err = parseRegToken(registry)
|
||||
} else {
|
||||
username = registry.Username
|
||||
password = registry.Password
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
var _ portainer.ComposeStackManager = &composeStackManager{}
|
||||
|
||||
type composeStackManager struct{}
|
||||
|
||||
func NewComposeStackManager() *composeStackManager {
|
||||
@@ -31,6 +33,6 @@ func (manager *composeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1367,7 +1367,13 @@ type (
|
||||
ValidateFlags(flags *CLIFlags) error
|
||||
}
|
||||
|
||||
ComposeOptions struct {
|
||||
Registries []Registry
|
||||
}
|
||||
|
||||
ComposeUpOptions struct {
|
||||
ComposeOptions
|
||||
|
||||
// ForceRecreate forces to recreate containers
|
||||
ForceRecreate bool
|
||||
// AbortOnContainerExit will stop the deployment if a container exits.
|
||||
@@ -1379,6 +1385,8 @@ type (
|
||||
}
|
||||
|
||||
ComposeRunOptions struct {
|
||||
ComposeOptions
|
||||
|
||||
// Remove will remove the container after it has stopped
|
||||
Remove bool
|
||||
// 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
|
||||
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeUpOptions) 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
|
||||
|
||||
@@ -58,23 +58,25 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
options := portainer.ComposeOptions{Registries: registries}
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
||||
ForceRecreate: forceRecreate,
|
||||
})
|
||||
if err != nil {
|
||||
if err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
||||
ComposeOptions: options,
|
||||
ForceRecreate: forceRecreate,
|
||||
}); err != nil {
|
||||
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 {
|
||||
|
||||
@@ -59,8 +59,7 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
if err != nil {
|
||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, portainer.ComposeOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function createSwarmStackFromGit({
|
||||
}: SwarmGitRepositoryPayload) {
|
||||
try {
|
||||
const { data } = await axios.post<Stack>(
|
||||
buildCreateUrl('standalone', 'repository'),
|
||||
buildCreateUrl('swarm', 'repository'),
|
||||
payload,
|
||||
{
|
||||
params: { endpointId: environmentId },
|
||||
|
||||
@@ -19,12 +19,27 @@ export function useUpdateK8sConfigMapMutation(
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
data,
|
||||
configMap,
|
||||
configMapName,
|
||||
}: {
|
||||
data: ConfigMap;
|
||||
configMap: ConfigMap;
|
||||
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, [
|
||||
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
|
||||
);
|
||||
await updateConfigMapMutation.mutateAsync({
|
||||
data: configMapPayload,
|
||||
configMap: configMapPayload,
|
||||
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||
});
|
||||
notifySuccess('Success', 'Namespace access updated');
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useConfigMap } from '@/react/kubernetes/configs/queries/useConfigMap';
|
||||
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||
import { useUpdateK8sConfigMapMutation } from '@/react/kubernetes/configs/queries/useUpdateK8sConfigMapMutation';
|
||||
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 { TextTip } from '@@/Tip/TextTip';
|
||||
@@ -28,6 +30,7 @@ export function CreateAccessWidget() {
|
||||
const {
|
||||
params: { id: namespaceName },
|
||||
} = useCurrentStateAndParams();
|
||||
const { user } = useCurrentUser();
|
||||
const environmentId = useEnvironmentId();
|
||||
const isRBACEnabledQuery = useIsRBACEnabled(environmentId);
|
||||
const initialValues: {
|
||||
@@ -75,7 +78,9 @@ export function CreateAccessWidget() {
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={(values, formikHelpers) =>
|
||||
onSubmit(values, formikHelpers)
|
||||
}
|
||||
validateOnMount
|
||||
>
|
||||
{(formikProps) => (
|
||||
@@ -104,10 +109,10 @@ export function CreateAccessWidget() {
|
||||
namespaceAccesses,
|
||||
values.selectedUsersAndTeams,
|
||||
namespaceName,
|
||||
configMap
|
||||
configMap ?? newConfigMap(user.Username, user.Id)
|
||||
);
|
||||
await updateConfigMapMutation.mutateAsync({
|
||||
data: configMapPayload,
|
||||
configMap: configMapPayload,
|
||||
configMapName: PortainerNamespaceAccessesConfigMap.configMapName,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -9,7 +9,7 @@ require (
|
||||
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/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/compose-spec/compose-go/v2 v2.0.2
|
||||
github.com/containers/image/v5 v5.30.1
|
||||
|
||||
9
go.sum
9
go.sum
@@ -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/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/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/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
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/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/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/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/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/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.14.0/go.mod h1:a3WUi3JjM3MFtIYenSYPJ7UZPXsw7U7vzebnynxucks=
|
||||
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.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/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||
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/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/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/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||
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.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.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
|
||||
@@ -18,9 +18,12 @@ import (
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func withCli(
|
||||
ctx context.Context,
|
||||
options libstack.Options,
|
||||
@@ -39,25 +42,20 @@ func withCli(
|
||||
opts.Hosts = []string{options.Host}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "docker-config")
|
||||
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
|
||||
|
||||
mu.Lock()
|
||||
if err := cli.Initialize(opts); err != nil {
|
||||
mu.Unlock()
|
||||
return fmt.Errorf("unable to initialize the Docker client: %w", err)
|
||||
}
|
||||
mu.Unlock()
|
||||
defer cli.Client().Close()
|
||||
|
||||
for _, r := range options.Registries {
|
||||
creds := cli.ConfigFile().GetCredentialsStore(r.ServerAddress)
|
||||
|
||||
if err := creds.Store(r); err != nil {
|
||||
return fmt.Errorf("unable to store the Docker credentials: %w", err)
|
||||
if r.ServerAddress == "" || r.ServerAddress == registry.DefaultNamespace {
|
||||
r.ServerAddress = registry.IndexServer
|
||||
}
|
||||
|
||||
cli.ConfigFile().AuthConfigs[r.ServerAddress] = r
|
||||
}
|
||||
|
||||
return cliFn(ctx, cli)
|
||||
@@ -72,7 +70,10 @@ func withComposeService(
|
||||
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
|
||||
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 {
|
||||
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
|
||||
@@ -133,7 +134,7 @@ func withComposeService(
|
||||
// Deploy creates and starts containers
|
||||
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 {
|
||||
addServiceLabels(project)
|
||||
addServiceLabels(project, false)
|
||||
|
||||
var opts api.UpOptions
|
||||
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 {
|
||||
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{
|
||||
AutoRemove: options.Remove,
|
||||
Command: options.Args,
|
||||
Detach: options.Detached,
|
||||
Service: serviceName,
|
||||
}
|
||||
|
||||
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) {
|
||||
var payload []byte
|
||||
|
||||
@@ -226,7 +245,12 @@ func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, option
|
||||
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 {
|
||||
s.CustomLabels = map[string]string{
|
||||
api.ProjectLabel: project.Name,
|
||||
@@ -234,7 +258,7 @@ func addServiceLabels(project *types.Project) {
|
||||
api.VersionLabel: api.ComposeVersion,
|
||||
api.WorkingDirLabel: "/",
|
||||
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
|
||||
api.OneoffLabel: "False",
|
||||
api.OneoffLabel: oneOffLabel,
|
||||
}
|
||||
project.Services[i] = s
|
||||
}
|
||||
|
||||
@@ -35,17 +35,14 @@ services:
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
filePathOriginal, err := createFile(dir, "docker-compose.yml", composeFileContent)
|
||||
require.NoError(t, err)
|
||||
|
||||
filePathOverride, err := createFile(dir, "docker-compose-override.yml", overrideComposeFileContent)
|
||||
require.NoError(t, err)
|
||||
filePathOriginal := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
||||
filePathOverride := createFile(t, dir, "docker-compose-override.yml", overrideComposeFileContent)
|
||||
|
||||
filePaths := []string{filePathOriginal, filePathOverride}
|
||||
|
||||
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)
|
||||
|
||||
err = w.Pull(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
||||
@@ -62,7 +59,7 @@ services:
|
||||
|
||||
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.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
||||
@@ -73,14 +70,34 @@ services:
|
||||
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)
|
||||
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
err := os.WriteFile(filePath, []byte(content), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return filePath, nil
|
||||
return filePath
|
||||
}
|
||||
|
||||
func containerExists(containerName string) bool {
|
||||
@@ -101,8 +118,7 @@ func Test_Validate(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
filePathOriginal, err := createFile(dir, "docker-compose.yml", invalidComposeFileContent)
|
||||
require.NoError(t, err)
|
||||
filePathOriginal := createFile(t, dir, "docker-compose.yml", invalidComposeFileContent)
|
||||
|
||||
filePaths := []string{filePathOriginal}
|
||||
|
||||
@@ -110,7 +126,7 @@ func Test_Validate(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -308,13 +324,11 @@ networks:
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
composeFilePath, err := createFile(dir, "docker-compose.yml", tc.composeFileContent)
|
||||
require.NoError(t, err)
|
||||
composeFilePath := createFile(t, dir, "docker-compose.yml", tc.composeFileContent)
|
||||
|
||||
envFilePath := ""
|
||||
if tc.envFileContent != "" {
|
||||
envFilePath, err = createFile(dir, "stack.env", tc.envFileContent)
|
||||
require.NoError(t, err)
|
||||
envFilePath = createFile(t, dir, "stack.env", tc.envFileContent)
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
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
|
||||
}); err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestComposeProjectStatus(t *testing.T) {
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
status, statusMessage, err = waitForStatus(w, ctx, projectName, libstack.StatusRemoved, "")
|
||||
status, statusMessage, err = waitForStatus(w, ctx, projectName, libstack.StatusRemoved)
|
||||
if err != nil {
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus, stackFileLocation)
|
||||
statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus)
|
||||
result := <-statusCh
|
||||
if result.ErrorMsg == "" {
|
||||
return result.Status, "", nil
|
||||
|
||||
@@ -16,7 +16,7 @@ type Deployer interface {
|
||||
Pull(ctx context.Context, filePaths []string, options Options) error
|
||||
Run(ctx context.Context, filePaths []string, serviceName string, options RunOptions) 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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user