mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
refactor(stack): stack build process backend only [EE-4342] (#7750)
This commit is contained in:
parent
83a1ce9d2a
commit
e9de484c3e
65 changed files with 2270 additions and 942 deletions
35
api/stacks/deployments/autoupdate.go
Normal file
35
api/stacks/deployments/autoupdate.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func StartAutoupdate(stackID portainer.StackID, interval string, scheduler *scheduler.Scheduler, stackDeployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) (jobID string, e *httperror.HandlerError) {
|
||||
d, err := time.ParseDuration(interval)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Unable to parse stack's auto update interval", err)
|
||||
}
|
||||
|
||||
jobID = scheduler.StartJobEvery(d, func() error {
|
||||
return RedeployWhenChanged(stackID, stackDeployer, datastore, gitService)
|
||||
})
|
||||
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
func StopAutoupdate(stackID portainer.StackID, jobID string, scheduler *scheduler.Scheduler) {
|
||||
if jobID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := scheduler.StopJob(jobID); err != nil {
|
||||
log.Warn().Int("stack_id", int(stackID)).Msg("could not stop the job for the stack")
|
||||
}
|
||||
}
|
171
api/stacks/deployments/deploy.go
Normal file
171
api/stacks/deployments/deploy.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type StackAuthorMissingErr struct {
|
||||
stackID int
|
||||
authorName string
|
||||
}
|
||||
|
||||
func (e *StackAuthorMissingErr) Error() string {
|
||||
return fmt.Sprintf("stack's %v author %s is missing", e.stackID, e.authorName)
|
||||
}
|
||||
|
||||
// RedeployWhenChanged pull and redeploy the stack when git repo changed
|
||||
// Stack will always be redeployed if force deployment is set to true
|
||||
func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) error {
|
||||
log.Debug().Int("stack_id", int(stackID)).Msg("redeploying stack")
|
||||
|
||||
stack, err := datastore.Stack().Stack(stackID)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to get the stack %v", stackID)
|
||||
}
|
||||
|
||||
if stack.GitConfig == nil {
|
||||
return nil // do nothing if it isn't a git-based stack
|
||||
}
|
||||
|
||||
author := stack.UpdatedBy
|
||||
if author == "" {
|
||||
author = stack.CreatedBy
|
||||
}
|
||||
|
||||
user, err := datastore.User().UserByUsername(author)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Int("stack_id", int(stackID)).
|
||||
Str("author", author).
|
||||
Str("stack", stack.Name).
|
||||
Int("endpoint_id", int(stack.EndpointID)).
|
||||
Msg("cannot autoupdate a stack, stack author user is missing")
|
||||
|
||||
return &StackAuthorMissingErr{int(stack.ID), author}
|
||||
}
|
||||
|
||||
username, password := "", ""
|
||||
if stack.GitConfig.Authentication != nil {
|
||||
username, password = stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password
|
||||
}
|
||||
|
||||
newHash, err := gitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, username, password)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)
|
||||
}
|
||||
|
||||
if strings.EqualFold(newHash, string(stack.GitConfig.ConfigHash)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloneParams := &cloneRepositoryParameters{
|
||||
url: stack.GitConfig.URL,
|
||||
ref: stack.GitConfig.ReferenceName,
|
||||
toDir: stack.ProjectPath,
|
||||
}
|
||||
if stack.GitConfig.Authentication != nil {
|
||||
cloneParams.auth = &gitAuth{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
if err := cloneGitRepository(gitService, cloneParams); err != nil {
|
||||
return errors.WithMessagef(err, "failed to do a fresh clone of the stack %v", stack.ID)
|
||||
}
|
||||
|
||||
endpoint, err := datastore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
|
||||
}
|
||||
|
||||
registries, err := getUserRegistries(datastore, user, endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
err := deployer.DeployComposeStack(stack, endpoint, registries, true, false)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
}
|
||||
case portainer.DockerSwarmStack:
|
||||
err := deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
}
|
||||
case portainer.KubernetesStack:
|
||||
log.Debug().
|
||||
Int("stack_id", int(stackID)).
|
||||
Msg("deploying a kube app")
|
||||
|
||||
err := deployer.DeployKubernetesStack(stack, endpoint, user)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a kubternetes app stack %v", stackID)
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.GitConfig.ConfigHash = newHash
|
||||
if err := datastore.Stack().UpdateStack(stack.ID, stack); err != nil {
|
||||
return errors.WithMessagef(err, "failed to update the stack %v", stack.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserRegistries(datastore dataservices.DataStore, user *portainer.User, endpointID portainer.EndpointID) ([]portainer.Registry, error) {
|
||||
registries, err := datastore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "unable to retrieve registries from the database")
|
||||
}
|
||||
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
userMemberships, err := datastore.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "failed to fetch memberships of the stack author [%s]", user.Username)
|
||||
}
|
||||
|
||||
filteredRegistries := make([]portainer.Registry, 0, len(registries))
|
||||
for _, registry := range registries {
|
||||
if security.AuthorizedRegistryAccess(®istry, user, userMemberships, endpointID) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRegistries, nil
|
||||
}
|
||||
|
||||
type cloneRepositoryParameters struct {
|
||||
url string
|
||||
ref string
|
||||
toDir string
|
||||
auth *gitAuth
|
||||
}
|
||||
|
||||
type gitAuth struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
|
||||
if cloneParams.auth != nil {
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password)
|
||||
}
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "")
|
||||
}
|
250
api/stacks/deployments/deploy_test.go
Normal file
250
api/stacks/deployments/deploy_test.go
Normal file
|
@ -0,0 +1,250 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type noopDeployer struct{}
|
||||
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged_FailsWhenCannotFindStack(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
err := RedeployWhenChanged(1, nil, store, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Truef(t, strings.HasPrefix(err.Error(), "failed to get the stack"), "it isn't an error we expected: %v", err.Error())
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged_DoesNothingWhenNotAGitBasedStack(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||
err := store.User().Create(admin)
|
||||
assert.NoError(t, err, "error creating an admin")
|
||||
|
||||
err = store.Stack().Create(&portainer.Stack{ID: 1, CreatedBy: "admin"})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||
err := store.User().Create(admin)
|
||||
assert.NoError(t, err, "error creating an admin")
|
||||
|
||||
err = store.Stack().Create(&portainer.Stack{
|
||||
ID: 1,
|
||||
CreatedBy: "admin",
|
||||
ProjectPath: tmpDir,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "url",
|
||||
ReferenceName: "ref",
|
||||
ConfigHash: "oldHash",
|
||||
}})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{nil, "oldHash"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
||||
cloneErr := errors.New("failed to clone")
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||
err := store.User().Create(admin)
|
||||
assert.NoError(t, err, "error creating an admin")
|
||||
|
||||
err = store.Stack().Create(&portainer.Stack{
|
||||
ID: 1,
|
||||
CreatedBy: "admin",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "url",
|
||||
ReferenceName: "ref",
|
||||
ConfigHash: "oldHash",
|
||||
}})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{cloneErr, "newHash"})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, cloneErr, "should failed to clone but didn't, check test setup")
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
err := store.Endpoint().Create(&portainer.Endpoint{ID: 1})
|
||||
assert.NoError(t, err, "error creating environment")
|
||||
|
||||
username := "user"
|
||||
err = store.User().Create(&portainer.User{Username: username, Role: portainer.AdministratorRole})
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
stack := portainer.Stack{
|
||||
ID: 1,
|
||||
EndpointID: 1,
|
||||
ProjectPath: tmpDir,
|
||||
UpdatedBy: username,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "url",
|
||||
ReferenceName: "ref",
|
||||
ConfigHash: "oldHash",
|
||||
}}
|
||||
err = store.Stack().Create(&stack)
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
t.Run("can deploy docker compose stack", func(t *testing.T) {
|
||||
stack.Type = portainer.DockerComposeStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can deploy docker swarm stack", func(t *testing.T) {
|
||||
stack.Type = portainer.DockerSwarmStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can deploy kube app", func(t *testing.T) {
|
||||
stack.Type = portainer.KubernetesStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getUserRegistries(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
endpointID := 123
|
||||
|
||||
admin := &portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||
err := store.User().Create(admin)
|
||||
assert.NoError(t, err, "error creating an admin")
|
||||
|
||||
user := &portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
|
||||
err = store.User().Create(user)
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
team := portainer.Team{ID: 1, Name: "team"}
|
||||
|
||||
store.TeamMembership().Create(&portainer.TeamMembership{
|
||||
ID: 1,
|
||||
UserID: user.ID,
|
||||
TeamID: team.ID,
|
||||
Role: portainer.TeamMember,
|
||||
})
|
||||
|
||||
registryReachableByUser := portainer.Registry{
|
||||
ID: 1,
|
||||
Name: "registryReachableByUser",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
portainer.EndpointID(endpointID): {
|
||||
UserAccessPolicies: map[portainer.UserID]portainer.AccessPolicy{
|
||||
user.ID: {RoleID: portainer.RoleID(portainer.StandardUserRole)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = store.Registry().Create(®istryReachableByUser)
|
||||
assert.NoError(t, err, "couldn't create a registry")
|
||||
|
||||
registryReachableByTeam := portainer.Registry{
|
||||
ID: 2,
|
||||
Name: "registryReachableByTeam",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
portainer.EndpointID(endpointID): {
|
||||
TeamAccessPolicies: map[portainer.TeamID]portainer.AccessPolicy{
|
||||
team.ID: {RoleID: portainer.RoleID(portainer.StandardUserRole)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = store.Registry().Create(®istryReachableByTeam)
|
||||
assert.NoError(t, err, "couldn't create a registry")
|
||||
|
||||
registryRestricted := portainer.Registry{
|
||||
ID: 3,
|
||||
Name: "registryRestricted",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
portainer.EndpointID(endpointID): {
|
||||
UserAccessPolicies: map[portainer.UserID]portainer.AccessPolicy{
|
||||
user.ID + 100: {RoleID: portainer.RoleID(portainer.StandardUserRole)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = store.Registry().Create(®istryRestricted)
|
||||
assert.NoError(t, err, "couldn't create a registry")
|
||||
|
||||
t.Run("admin should has access to all registries", func(t *testing.T) {
|
||||
registries, err := getUserRegistries(store, admin, portainer.EndpointID(endpointID))
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam, registryRestricted}, registries)
|
||||
})
|
||||
|
||||
t.Run("regular user has access to registries allowed to him and/or his team", func(t *testing.T) {
|
||||
registries, err := getUserRegistries(store, user, portainer.EndpointID(endpointID))
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam}, registries)
|
||||
})
|
||||
}
|
95
api/stacks/deployments/deployer.go
Normal file
95
api/stacks/deployments/deployer.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
)
|
||||
|
||||
type StackDeployer interface {
|
||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error
|
||||
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
|
||||
}
|
||||
|
||||
type stackDeployer struct {
|
||||
lock *sync.Mutex
|
||||
swarmStackManager portainer.SwarmStackManager
|
||||
composeStackManager portainer.ComposeStackManager
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
}
|
||||
|
||||
// NewStackDeployer inits a stackDeployer struct with a SwarmStackManager, a ComposeStackManager and a KubernetesDeployer
|
||||
func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStackManager portainer.ComposeStackManager, kubernetesDeployer portainer.KubernetesDeployer) *stackDeployer {
|
||||
return &stackDeployer{
|
||||
lock: &sync.Mutex{},
|
||||
swarmStackManager: swarmStackManager,
|
||||
composeStackManager: composeStackManager,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
|
||||
return d.swarmStackManager.Deploy(stack, prune, pullImage, endpoint)
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
|
||||
if err != nil {
|
||||
d.composeStackManager.Down(context.TODO(), stack, endpoint)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
appLabels := k.KubeAppLabels{
|
||||
StackID: int(stack.ID),
|
||||
StackName: stack.Name,
|
||||
Owner: user.Username,
|
||||
}
|
||||
|
||||
if stack.GitConfig == nil {
|
||||
appLabels.Kind = "content"
|
||||
} else {
|
||||
appLabels.Kind = "git"
|
||||
}
|
||||
|
||||
k8sDeploymentConfig, err := CreateKubernetesStackDeploymentConfig(stack, d.kubernetesDeployer, appLabels, user, endpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create temp kub deployment files")
|
||||
}
|
||||
|
||||
err = k8sDeploymentConfig.Deploy()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to deploy kubernetes application")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
93
api/stacks/deployments/deployment_compose_config.go
Normal file
93
api/stacks/deployments/deployment_compose_config.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
type ComposeStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
forcePullImage bool
|
||||
ForceCreate bool
|
||||
FileService portainer.FileService
|
||||
StackDeployer StackDeployer
|
||||
}
|
||||
|
||||
func CreateComposeStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, forcePullImage, forceCreate bool) (*ComposeStackDeploymentConfig, error) {
|
||||
user, err := dataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load user information from the database: %w", err)
|
||||
}
|
||||
|
||||
registries, err := dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve registries from the database: %w", err)
|
||||
}
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
config := &ComposeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
forcePullImage: forcePullImage,
|
||||
ForceCreate: forceCreate,
|
||||
FileService: fileService,
|
||||
StackDeployer: deployer,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (config *ComposeStackDeploymentConfig) GetUsername() string {
|
||||
if config.user != nil {
|
||||
return config.user.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (config *ComposeStackDeploymentConfig) Deploy() error {
|
||||
if config.FileService == nil || config.StackDeployer == nil {
|
||||
log.Println("[deployment, compose] file service or stack deployer is not initialised")
|
||||
return errors.New("file service or stack deployer cannot be nil")
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to validate user admin privileges")
|
||||
}
|
||||
|
||||
securitySettings := &config.endpoint.SecuritySettings
|
||||
|
||||
if (!securitySettings.AllowBindMountsForRegularUsers ||
|
||||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
|
||||
!securitySettings.AllowHostNamespaceForRegularUsers ||
|
||||
!securitySettings.AllowDeviceMappingForRegularUsers ||
|
||||
!securitySettings.AllowSysctlSettingForRegularUsers ||
|
||||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
err = stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return config.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate)
|
||||
}
|
||||
|
||||
func (config *ComposeStackDeploymentConfig) GetResponse() string {
|
||||
return ""
|
||||
}
|
7
api/stacks/deployments/deployment_config.go
Normal file
7
api/stacks/deployments/deployment_config.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package deployments
|
||||
|
||||
type StackDeploymentConfiger interface {
|
||||
GetUsername() string
|
||||
Deploy() error
|
||||
GetResponse() string
|
||||
}
|
89
api/stacks/deployments/deployment_kubernetes_config.go
Normal file
89
api/stacks/deployments/deployment_kubernetes_config.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
type KubernetesStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
kuberneteDeployer portainer.KubernetesDeployer
|
||||
appLabels k.KubeAppLabels
|
||||
user *portainer.User
|
||||
endpoint *portainer.Endpoint
|
||||
output string
|
||||
}
|
||||
|
||||
func CreateKubernetesStackDeploymentConfig(stack *portainer.Stack, kubeDeployer portainer.KubernetesDeployer, appLabels k.KubeAppLabels, user *portainer.User, endpoint *portainer.Endpoint) (*KubernetesStackDeploymentConfig, error) {
|
||||
|
||||
return &KubernetesStackDeploymentConfig{
|
||||
stack: stack,
|
||||
kuberneteDeployer: kubeDeployer,
|
||||
appLabels: appLabels,
|
||||
user: user,
|
||||
endpoint: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (config *KubernetesStackDeploymentConfig) GetUsername() string {
|
||||
return config.user.Username
|
||||
}
|
||||
|
||||
func (config *KubernetesStackDeploymentConfig) Deploy() error {
|
||||
fileNames := stackutils.GetStackFilePaths(config.stack, false)
|
||||
|
||||
manifestFilePaths := make([]string, len(fileNames))
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "kub_deployment")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create temp kub deployment directory")
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
for _, fileName := range fileNames {
|
||||
manifestFilePath := filesystem.JoinPaths(tmpDir, fileName)
|
||||
manifestContent, err := ioutil.ReadFile(filesystem.JoinPaths(config.stack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read manifest file")
|
||||
}
|
||||
|
||||
if config.stack.IsComposeFormat {
|
||||
manifestContent, err = config.kuberneteDeployer.ConvertCompose(manifestContent)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
||||
}
|
||||
}
|
||||
|
||||
manifestContent, err = k.AddAppLabels(manifestContent, config.appLabels.ToMap())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to add application labels")
|
||||
}
|
||||
|
||||
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create temp manifest file")
|
||||
}
|
||||
|
||||
manifestFilePaths = append(manifestFilePaths, manifestFilePath)
|
||||
}
|
||||
|
||||
output, err := config.kuberneteDeployer.Deploy(config.user.ID, config.endpoint, manifestFilePaths, config.stack.Namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy kubernete stack: %w", err)
|
||||
}
|
||||
|
||||
config.output = output
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *KubernetesStackDeploymentConfig) GetResponse() string {
|
||||
return config.output
|
||||
}
|
86
api/stacks/deployments/deployment_swarm_config.go
Normal file
86
api/stacks/deployments/deployment_swarm_config.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
type SwarmStackDeploymentConfig struct {
|
||||
stack *portainer.Stack
|
||||
endpoint *portainer.Endpoint
|
||||
registries []portainer.Registry
|
||||
prune bool
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
pullImage bool
|
||||
FileService portainer.FileService
|
||||
StackDeployer StackDeployer
|
||||
}
|
||||
|
||||
func CreateSwarmStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, prune bool, pullImage bool) (*SwarmStackDeploymentConfig, error) {
|
||||
user, err := dataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load user information from the database: %w", err)
|
||||
}
|
||||
|
||||
registries, err := dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve registries from the database: %w", err)
|
||||
}
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
config := &SwarmStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
pullImage: pullImage,
|
||||
FileService: fileService,
|
||||
StackDeployer: deployer,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (config *SwarmStackDeploymentConfig) GetUsername() string {
|
||||
if config.user != nil {
|
||||
return config.user.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (config *SwarmStackDeploymentConfig) Deploy() error {
|
||||
if config.FileService == nil || config.StackDeployer == nil {
|
||||
log.Println("[deployment, swarm] file service or stack deployer is not initialised")
|
||||
return errors.New("file service or stack deployer cannot be nil")
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to validate user admin privileges")
|
||||
}
|
||||
|
||||
settings := &config.endpoint.SecuritySettings
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
err = stackutils.ValidateStackFiles(config.stack, settings, config.FileService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return config.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
||||
}
|
||||
|
||||
func (config *SwarmStackDeploymentConfig) GetResponse() string {
|
||||
return ""
|
||||
}
|
34
api/stacks/deployments/scheduled.go
Normal file
34
api/stacks/deployments/scheduled.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package deployments
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
)
|
||||
|
||||
func StartStackSchedules(scheduler *scheduler.Scheduler, stackdeployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) error {
|
||||
stacks, err := datastore.Stack().RefreshableStacks()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch refreshable stacks")
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
d, err := time.ParseDuration(stack.AutoUpdate.Interval)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse auto update interval")
|
||||
}
|
||||
stackID := stack.ID // to be captured by the scheduled function
|
||||
jobID := scheduler.StartJobEvery(d, func() error {
|
||||
return RedeployWhenChanged(stackID, stackdeployer, datastore, gitService)
|
||||
})
|
||||
|
||||
stack.AutoUpdate.JobID = jobID
|
||||
if err := datastore.Stack().UpdateStack(stack.ID, &stack); err != nil {
|
||||
return errors.Wrap(err, "failed to update stack job id")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue