mirror of https://github.com/portainer/portainer
refactor(stacks): extract auto update logic [EE-4945] (#8545)
parent
085381e6fc
commit
6918da2414
|
@ -68,13 +68,13 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
|
||||||
|
|
||||||
if webhookID == "" {
|
if webhookID == "" {
|
||||||
if b.count%2 == 0 {
|
if b.count%2 == 0 {
|
||||||
stack.AutoUpdate = &portainer.StackAutoUpdate{
|
stack.AutoUpdate = &portainer.AutoUpdateSettings{
|
||||||
Interval: "",
|
Interval: "",
|
||||||
Webhook: "",
|
Webhook: "",
|
||||||
}
|
}
|
||||||
} // else keep AutoUpdate nil
|
} // else keep AutoUpdate nil
|
||||||
} else {
|
} else {
|
||||||
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
|
stack.AutoUpdate = &portainer.AutoUpdateSettings{Webhook: webhookID}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.store.StackService.Create(&stack)
|
err := b.store.StackService.Create(&stack)
|
||||||
|
@ -91,8 +91,8 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
staticStack := portainer.Stack{ID: 1}
|
staticStack := portainer.Stack{ID: 1}
|
||||||
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
|
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
|
||||||
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
|
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.AutoUpdateSettings{Interval: "1m"}}
|
||||||
|
|
||||||
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
|
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
|
||||||
err := store.Stack().Create(stack)
|
err := store.Stack().Create(stack)
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidGitCredential = errors.New("Invalid git credential")
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloneOptions struct {
|
||||||
|
ProjectPath string
|
||||||
|
URL string
|
||||||
|
ReferenceName string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
|
||||||
|
backupProjectPath := fmt.Sprintf("%s-old", options.ProjectPath)
|
||||||
|
cleanUp := false
|
||||||
|
cleanFn := func() {
|
||||||
|
if !cleanUp {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fileService.RemoveDirectory(backupProjectPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("unable to remove git repository directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath)
|
||||||
|
if err != nil {
|
||||||
|
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp = true
|
||||||
|
|
||||||
|
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password)
|
||||||
|
if err != nil {
|
||||||
|
cleanUp = false
|
||||||
|
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
|
||||||
|
if restoreError != nil {
|
||||||
|
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == gittypes.ErrAuthenticationFailure {
|
||||||
|
return cleanFn, errors.WithMessage(err, ErrInvalidGitCredential.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanFn, errors.WithMessage(err, "Unable to clone git repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanFn, nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCredentials(auth *gittypes.GitAuthentication) (string, string, error) {
|
||||||
|
if auth == nil {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.Username, auth.Password, nil
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/git"
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateGitObject updates a git object based on its config
|
||||||
|
func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.DataStore, objId string, gitConfig *gittypes.RepoConfig, autoUpdateConfig *portainer.AutoUpdateSettings, projectPath string) (bool, string, error) {
|
||||||
|
if gitConfig == nil {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("url", gitConfig.URL).
|
||||||
|
Str("ref", gitConfig.ReferenceName).
|
||||||
|
Str("object", objId).
|
||||||
|
Msg("the object has a git config, try to poll from git repository")
|
||||||
|
|
||||||
|
username, password, err := git.GetCredentials(gitConfig.Authentication)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId)
|
||||||
|
}
|
||||||
|
|
||||||
|
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
|
||||||
|
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
||||||
|
if !hashChanged && !forceUpdate {
|
||||||
|
log.Debug().
|
||||||
|
Str("hash", newHash).
|
||||||
|
Str("url", gitConfig.URL).
|
||||||
|
Str("ref", gitConfig.ReferenceName).
|
||||||
|
Str("object", objId).
|
||||||
|
Msg("git repo is up to date")
|
||||||
|
|
||||||
|
return false, newHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneParams := &cloneRepositoryParameters{
|
||||||
|
url: gitConfig.URL,
|
||||||
|
ref: gitConfig.ReferenceName,
|
||||||
|
toDir: projectPath,
|
||||||
|
}
|
||||||
|
if gitConfig.Authentication != nil {
|
||||||
|
cloneParams.auth = &gitAuth{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cloneGitRepository(gitService, cloneParams); err != nil {
|
||||||
|
return false, "", errors.WithMessagef(err, "failed to do a fresh clone of %v", objId)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("hash", newHash).
|
||||||
|
Str("url", gitConfig.URL).
|
||||||
|
Str("ref", gitConfig.ReferenceName).
|
||||||
|
Str("object", objId).
|
||||||
|
Msg("git repo cloned updated")
|
||||||
|
|
||||||
|
return true, newHash, 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, "", "")
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error {
|
||||||
|
if autoUpdate == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if autoUpdate.Webhook == "" && autoUpdate.Interval == "" {
|
||||||
|
return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
|
||||||
|
return httperrors.NewInvalidPayloadError("invalid Webhook format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if autoUpdate.Interval != "" {
|
||||||
|
if _, err := time.ParseDuration(autoUpdate.Interval); err != nil {
|
||||||
|
return httperrors.NewInvalidPayloadError("invalid Interval format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package stackutils
|
package update
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -7,25 +7,25 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_ValidateStackAutoUpdate(t *testing.T) {
|
func Test_ValidateAutoUpdate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
value *portainer.StackAutoUpdate
|
value *portainer.AutoUpdateSettings
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "webhook is not a valid UUID",
|
name: "webhook is not a valid UUID",
|
||||||
value: &portainer.StackAutoUpdate{Webhook: "fake-webhook"},
|
value: &portainer.AutoUpdateSettings{Webhook: "fake-webhook"},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "incorrect interval value",
|
name: "incorrect interval value",
|
||||||
value: &portainer.StackAutoUpdate{Interval: "1dd2hh3mm"},
|
value: &portainer.AutoUpdateSettings{Interval: "1dd2hh3mm"},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid auto update",
|
name: "valid auto update",
|
||||||
value: &portainer.StackAutoUpdate{
|
value: &portainer.AutoUpdateSettings{
|
||||||
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
|
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
|
||||||
Interval: "5h30m40s10ms",
|
Interval: "5h30m40s10ms",
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@ func Test_ValidateStackAutoUpdate(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := ValidateStackAutoUpdate(tt.value)
|
err := ValidateAutoUpdateSettings(tt.value)
|
||||||
assert.Equalf(t, tt.wantErr, err != nil, "received %+v", err)
|
assert.Equalf(t, tt.wantErr, err != nil, "received %+v", err)
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
|
||||||
|
if govalidator.IsNull(repoConfig.URL) || !govalidator.IsURL(repoConfig.URL) {
|
||||||
|
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateRepoAuthentication(repoConfig.Authentication)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {
|
||||||
|
if auth != nil && govalidator.IsNull(auth.Password) {
|
||||||
|
return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
type InvalidPayloadError struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidPayloadError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInvalidPayloadError(msg string) *InvalidPayloadError {
|
||||||
|
return &InvalidPayloadError{msg: msg}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
||||||
|
@ -156,14 +157,14 @@ type composeStackFromGitRepositoryPayload struct {
|
||||||
// Applicable when deploying with multiple stack files
|
// Applicable when deploying with multiple stack files
|
||||||
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
||||||
// Optional auto update configuration
|
// Optional auto update configuration
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
// A list of environment(endpoint) variables used during stack deployment
|
// A list of environment(endpoint) variables used during stack deployment
|
||||||
Env []portainer.Pair
|
Env []portainer.Pair
|
||||||
// Whether the stack is from a app template
|
// Whether the stack is from a app template
|
||||||
FromAppTemplate bool `example:"false"`
|
FromAppTemplate bool `example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.StackAutoUpdate, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||||
return stackbuilders.StackPayload{
|
return stackbuilders.StackPayload{
|
||||||
Name: name,
|
Name: name,
|
||||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||||
|
@ -191,7 +192,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||||
}
|
}
|
||||||
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/git/update"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
||||||
|
@ -44,10 +45,10 @@ type kubernetesGitDeploymentPayload struct {
|
||||||
RepositoryPassword string
|
RepositoryPassword string
|
||||||
ManifestFile string
|
ManifestFile string
|
||||||
AdditionalFiles []string
|
AdditionalFiles []string
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.StackAutoUpdate) stackbuilders.StackPayload {
|
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings) stackbuilders.StackPayload {
|
||||||
return stackbuilders.StackPayload{
|
return stackbuilders.StackPayload{
|
||||||
StackName: name,
|
StackName: name,
|
||||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||||
|
@ -101,7 +102,7 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.ManifestFile) {
|
if govalidator.IsNull(payload.ManifestFile) {
|
||||||
return errors.New("Invalid manifest file in repository")
|
return errors.New("Invalid manifest file in repository")
|
||||||
}
|
}
|
||||||
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.StackName) {
|
if govalidator.IsNull(payload.StackName) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
@ -115,7 +116,7 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||||
// Applicable when deploying with multiple stack files
|
// Applicable when deploying with multiple stack files
|
||||||
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
||||||
// Optional auto update configuration
|
// Optional auto update configuration
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||||
|
@ -131,13 +132,13 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||||
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
|
||||||
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
|
||||||
}
|
}
|
||||||
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.StackAutoUpdate, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||||
return stackbuilders.StackPayload{
|
return stackbuilders.StackPayload{
|
||||||
Name: name,
|
Name: name,
|
||||||
SwarmID: swarmID,
|
SwarmID: swarmID,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/git/update"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
|
@ -17,7 +18,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type stackGitUpdatePayload struct {
|
type stackGitUpdatePayload struct {
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
Env []portainer.Pair
|
Env []portainer.Pair
|
||||||
Prune bool
|
Prune bool
|
||||||
RepositoryReferenceName string
|
RepositoryReferenceName string
|
||||||
|
@ -27,7 +28,7 @@ type stackGitUpdatePayload struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||||
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -11,13 +11,12 @@ import (
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
"github.com/portainer/portainer/api/git"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stackGitRedployPayload struct {
|
type stackGitRedployPayload struct {
|
||||||
|
@ -154,22 +153,12 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||||
repositoryUsername = payload.RepositoryUsername
|
repositoryUsername = payload.RepositoryUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.GitService.CloneRepository(stack.ProjectPath, stack.GitConfig.URL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, git.CloneOptions{ProjectPath: stack.ProjectPath, URL: stack.GitConfig.URL, ReferenceName: stack.GitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
restoreError := filesystem.MoveDirectory(backupProjectPath, stack.ProjectPath)
|
return httperror.InternalServerError("Unable to clone git repository directory", err)
|
||||||
if restoreError != nil {
|
|
||||||
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
|
|
||||||
}
|
|
||||||
|
|
||||||
return httperror.InternalServerError("Unable to clone git repository", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer clean()
|
||||||
err = handler.FileService.RemoveDirectory(backupProjectPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("unable to remove git repository directory")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
httpErr := handler.deployStack(r, stack, payload.PullImage, endpoint)
|
httpErr := handler.deployStack(r, stack, payload.PullImage, endpoint)
|
||||||
if httpErr != nil {
|
if httpErr != nil {
|
||||||
|
|
|
@ -11,10 +11,10 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/git/update"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -30,7 +30,7 @@ type kubernetesGitStackUpdatePayload struct {
|
||||||
RepositoryAuthentication bool
|
RepositoryAuthentication bool
|
||||||
RepositoryUsername string
|
RepositoryUsername string
|
||||||
RepositoryPassword string
|
RepositoryPassword string
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -41,7 +41,7 @@ func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
|
func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error {
|
||||||
if err := stackutils.ValidateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id WebhookInvoke
|
// @id WebhookInvoke
|
||||||
|
@ -43,8 +42,6 @@ func (handler *Handler) webhookInvoke(w http.ResponseWriter, r *http.Request) *h
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "Autoupdate for the stack isn't available", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "Autoupdate for the stack isn't available", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error().Err(err).Msg("failed to update the stack")
|
|
||||||
|
|
||||||
return httperror.InternalServerError("Failed to update the stack", err)
|
return httperror.InternalServerError("Failed to update the stack", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@ func TestHandler_webhookInvoke(t *testing.T) {
|
||||||
|
|
||||||
webhookID := newGuidString(t)
|
webhookID := newGuidString(t)
|
||||||
store.StackService.Create(&portainer.Stack{
|
store.StackService.Create(&portainer.Stack{
|
||||||
AutoUpdate: &portainer.StackAutoUpdate{
|
ID: 1,
|
||||||
|
AutoUpdate: &portainer.AutoUpdateSettings{
|
||||||
Webhook: webhookID,
|
Webhook: webhookID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -32,6 +32,20 @@ type (
|
||||||
// Authorizations represents a set of authorizations associated to a role
|
// Authorizations represents a set of authorizations associated to a role
|
||||||
Authorizations map[Authorization]bool
|
Authorizations map[Authorization]bool
|
||||||
|
|
||||||
|
//AutoUpdateSettings represents the git auto sync config for stack deployment
|
||||||
|
AutoUpdateSettings struct {
|
||||||
|
// Auto update interval
|
||||||
|
Interval string `example:"1m30s"`
|
||||||
|
// A UUID generated from client
|
||||||
|
Webhook string `example:"05de31a2-79fa-4644-9c12-faa67e5c49f0"`
|
||||||
|
// Autoupdate job id
|
||||||
|
JobID string `example:"15"`
|
||||||
|
// Force update ignores repo changes
|
||||||
|
ForceUpdate bool `example:"false"`
|
||||||
|
// Pull latest image
|
||||||
|
ForcePullImage bool `example:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
// AzureCredentials represents the credentials used to connect to an Azure
|
// AzureCredentials represents the credentials used to connect to an Azure
|
||||||
// environment(endpoint).
|
// environment(endpoint).
|
||||||
AzureCredentials struct {
|
AzureCredentials struct {
|
||||||
|
@ -986,7 +1000,7 @@ type (
|
||||||
// Only applies when deploying stack with multiple files
|
// Only applies when deploying stack with multiple files
|
||||||
AdditionalFiles []string `json:"AdditionalFiles"`
|
AdditionalFiles []string `json:"AdditionalFiles"`
|
||||||
// The auto update settings of a git stack
|
// The auto update settings of a git stack
|
||||||
AutoUpdate *StackAutoUpdate `json:"AutoUpdate"`
|
AutoUpdate *AutoUpdateSettings `json:"AutoUpdate"`
|
||||||
// The stack deployment option
|
// The stack deployment option
|
||||||
Option *StackOption `json:"Option"`
|
Option *StackOption `json:"Option"`
|
||||||
// The git config of this stack
|
// The git config of this stack
|
||||||
|
@ -999,16 +1013,6 @@ type (
|
||||||
IsComposeFormat bool `example:"false"`
|
IsComposeFormat bool `example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//StackAutoUpdate represents the git auto sync config for stack deployment
|
|
||||||
StackAutoUpdate struct {
|
|
||||||
// Auto update interval
|
|
||||||
Interval string `example:"1m30s"`
|
|
||||||
// A UUID generated from client
|
|
||||||
Webhook string `example:"05de31a2-79fa-4644-9c12-faa67e5c49f0"`
|
|
||||||
// Autoupdate job id
|
|
||||||
JobID string `example:"15"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StackOption represents the options for stack deployment
|
// StackOption represents the options for stack deployment
|
||||||
StackOption struct {
|
StackOption struct {
|
||||||
// Prune services that are no longer referenced
|
// Prune services that are no longer referenced
|
||||||
|
|
|
@ -2,11 +2,11 @@ package deployments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
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/git/update"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -36,6 +36,11 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
return nil // do nothing if it isn't a git-based stack
|
return nil // do nothing if it isn't a git-based stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
author := stack.UpdatedBy
|
author := stack.UpdatedBy
|
||||||
if author == "" {
|
if author == "" {
|
||||||
author = stack.CreatedBy
|
author = stack.CreatedBy
|
||||||
|
@ -53,39 +58,22 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
return &StackAuthorMissingErr{int(stack.ID), author}
|
return &StackAuthorMissingErr{int(stack.ID), author}
|
||||||
}
|
}
|
||||||
|
|
||||||
username, password := "", ""
|
var gitCommitChangedOrForceUpdate bool
|
||||||
if stack.GitConfig.Authentication != nil {
|
if !stack.FromAppTemplate {
|
||||||
username, password = stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password
|
updated, newHash, err := update.UpdateGitObject(gitService, datastore, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, stack.AutoUpdate, stack.ProjectPath)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
newHash, err := gitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, username, password)
|
if updated {
|
||||||
if err != nil {
|
stack.GitConfig.ConfigHash = newHash
|
||||||
return errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID)
|
stack.UpdateDate = time.Now().Unix()
|
||||||
}
|
gitCommitChangedOrForceUpdate = updated
|
||||||
|
|
||||||
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 {
|
if !gitCommitChangedOrForceUpdate {
|
||||||
return errors.WithMessagef(err, "failed to do a fresh clone of the stack %v", stack.ID)
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
registries, err := getUserRegistries(datastore, user, endpoint.ID)
|
||||||
|
@ -117,8 +105,6 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
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 {
|
if err := datastore.Stack().UpdateStack(stack.ID, stack); err != nil {
|
||||||
return errors.WithMessagef(err, "failed to update the stack %v", stack.ID)
|
return errors.WithMessagef(err, "failed to update the stack %v", stack.ID)
|
||||||
}
|
}
|
||||||
|
@ -150,22 +136,3 @@ func getUserRegistries(datastore dataservices.DataStore, user *portainer.User, e
|
||||||
|
|
||||||
return filteredRegistries, nil
|
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, "", "")
|
|
||||||
}
|
|
||||||
|
|
|
@ -81,6 +81,11 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
||||||
err := store.User().Create(admin)
|
err := store.User().Create(admin)
|
||||||
assert.NoError(t, err, "error creating an admin")
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: 0,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err, "error creating environment")
|
||||||
|
|
||||||
err = store.Stack().Create(&portainer.Stack{
|
err = store.Stack().Create(&portainer.Stack{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
CreatedBy: "admin",
|
CreatedBy: "admin",
|
||||||
|
@ -105,6 +110,11 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
||||||
err := store.User().Create(admin)
|
err := store.User().Create(admin)
|
||||||
assert.NoError(t, err, "error creating an admin")
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: 0,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err, "error creating environment")
|
||||||
|
|
||||||
err = store.Stack().Create(&portainer.Stack{
|
err = store.Stack().Create(&portainer.Stack{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
CreatedBy: "admin",
|
CreatedBy: "admin",
|
||||||
|
@ -142,7 +152,9 @@ func Test_redeployWhenChanged(t *testing.T) {
|
||||||
URL: "url",
|
URL: "url",
|
||||||
ReferenceName: "ref",
|
ReferenceName: "ref",
|
||||||
ConfigHash: "oldHash",
|
ConfigHash: "oldHash",
|
||||||
}}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
err = store.Stack().Create(&stack)
|
err = store.Stack().Create(&stack)
|
||||||
assert.NoError(t, err, "failed to create a test stack")
|
assert.NoError(t, err, "failed to create a test stack")
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ type StackPayload struct {
|
||||||
// A list of environment(endpoint) variables used during stack deployment
|
// A list of environment(endpoint) variables used during stack deployment
|
||||||
Env []portainer.Pair
|
Env []portainer.Pair
|
||||||
// Optional auto update configuration
|
// Optional auto update configuration
|
||||||
AutoUpdate *portainer.StackAutoUpdate
|
AutoUpdate *portainer.AutoUpdateSettings
|
||||||
// Whether the stack is from a app template
|
// Whether the stack is from a app template
|
||||||
FromAppTemplate bool `example:"false"`
|
FromAppTemplate bool `example:"false"`
|
||||||
// Kubernetes stack name
|
// Kubernetes stack name
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package stackutils
|
package stackutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/compose/types"
|
"github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -67,21 +64,6 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateStackAutoUpdate(autoUpdate *portainer.StackAutoUpdate) error {
|
|
||||||
if autoUpdate == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
|
|
||||||
return errors.New("invalid Webhook format")
|
|
||||||
}
|
|
||||||
if autoUpdate.Interval != "" {
|
|
||||||
if _, err := time.ParseDuration(autoUpdate.Interval); err != nil {
|
|
||||||
return errors.New("invalid Interval format")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error {
|
func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error {
|
||||||
for _, file := range GetStackFilePaths(stack, false) {
|
for _, file := range GetStackFilePaths(stack, false) {
|
||||||
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
|
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
|
||||||
|
|
|
@ -199,7 +199,7 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.updateWebEditorInProgress = true;
|
this.state.updateWebEditorInProgress = true;
|
||||||
await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, this.stackFileContent, null);
|
await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, { stackFile: this.stackFileContent });
|
||||||
this.state.isEditorDirty = false;
|
this.state.isEditorDirty = false;
|
||||||
await this.$state.reload(this.$state.current);
|
await this.$state.reload(this.$state.current);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -84,6 +84,8 @@
|
||||||
is-auth-explanation-visible="true"
|
is-auth-explanation-visible="true"
|
||||||
deploy-method="{{ ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE ? 'compose' : 'manifest' }}"
|
deploy-method="{{ ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE ? 'compose' : 'manifest' }}"
|
||||||
base-webhook-url="{{ ctrl.state.baseWebhookUrl }}"
|
base-webhook-url="{{ ctrl.state.baseWebhookUrl }}"
|
||||||
|
webhook-id="{{ ctrl.state.webhookId }}"
|
||||||
|
webhooks-docs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
|
||||||
></git-form>
|
></git-form>
|
||||||
<!-- !repository -->
|
<!-- !repository -->
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
||||||
import { editor, git, customTemplate, url } from '@@/BoxSelector/common-options/build-methods';
|
import { editor, git, customTemplate, url } from '@@/BoxSelector/common-options/build-methods';
|
||||||
import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
||||||
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
|
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||||
|
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
|
@ -45,6 +45,7 @@ class KubernetesDeployController {
|
||||||
templateId: null,
|
templateId: null,
|
||||||
template: null,
|
template: null,
|
||||||
baseWebhookUrl: baseStackWebhookUrl(),
|
baseWebhookUrl: baseStackWebhookUrl(),
|
||||||
|
webhookId: createWebhookId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
|
@ -256,7 +257,7 @@ class KubernetesDeployController {
|
||||||
}
|
}
|
||||||
payload.ManifestFile = this.formValues.ComposeFilePathInRepository;
|
payload.ManifestFile = this.formValues.ComposeFilePathInRepository;
|
||||||
payload.AdditionalFiles = this.formValues.AdditionalFiles;
|
payload.AdditionalFiles = this.formValues.AdditionalFiles;
|
||||||
payload.AutoUpdate = transformAutoUpdateViewModel(this.formValues.AutoUpdate);
|
payload.AutoUpdate = transformAutoUpdateViewModel(this.formValues.AutoUpdate, this.state.webhookId);
|
||||||
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
||||||
payload.StackFileContent = this.formValues.EditorContent;
|
payload.StackFileContent = this.formValues.EditorContent;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const gitFormAutoUpdate: IComponentOptions = {
|
||||||
environment-type="$ctrl.environmentType"
|
environment-type="$ctrl.environmentType"
|
||||||
is-force-pull-visible="$ctrl.isForcePullVisible"
|
is-force-pull-visible="$ctrl.isForcePullVisible"
|
||||||
base-webhook-url="$ctrl.baseWebhookUrl"
|
base-webhook-url="$ctrl.baseWebhookUrl"
|
||||||
|
webhook-id="$ctrl.webhookId"
|
||||||
|
webhooks-docs="$ctrl.webhooksDocs"
|
||||||
errors="$ctrl.errors">
|
errors="$ctrl.errors">
|
||||||
</react-git-form-auto-update-fieldset>
|
</react-git-form-auto-update-fieldset>
|
||||||
</ng-form>`,
|
</ng-form>`,
|
||||||
|
@ -19,6 +21,8 @@ export const gitFormAutoUpdate: IComponentOptions = {
|
||||||
environmentType: '@',
|
environmentType: '@',
|
||||||
isForcePullVisible: '<',
|
isForcePullVisible: '<',
|
||||||
baseWebhookUrl: '@',
|
baseWebhookUrl: '@',
|
||||||
|
webhookId: '@',
|
||||||
|
webhooksDocs: '@',
|
||||||
},
|
},
|
||||||
controller,
|
controller,
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,10 +11,11 @@ export const gitForm: IComponentOptions = {
|
||||||
is-docker-standalone="$ctrl.isDockerStandalone"
|
is-docker-standalone="$ctrl.isDockerStandalone"
|
||||||
deploy-method="$ctrl.deployMethod"
|
deploy-method="$ctrl.deployMethod"
|
||||||
is-additional-files-field-visible="$ctrl.isAdditionalFilesFieldVisible"
|
is-additional-files-field-visible="$ctrl.isAdditionalFilesFieldVisible"
|
||||||
is-auto-update-visible="$ctrl.isAutoUpdateVisible"
|
|
||||||
is-force-pull-visible="$ctrl.isForcePullVisible"
|
is-force-pull-visible="$ctrl.isForcePullVisible"
|
||||||
is-auth-explanation-visible="$ctrl.isAuthExplanationVisible"
|
is-auth-explanation-visible="$ctrl.isAuthExplanationVisible"
|
||||||
base-webhook-url="$ctrl.baseWebhookUrl"
|
base-webhook-url="$ctrl.baseWebhookUrl"
|
||||||
|
webhook-id="$ctrl.webhookId"
|
||||||
|
webhooks-docs="$ctrl.webhooksDocs"
|
||||||
errors="$ctrl.errors">
|
errors="$ctrl.errors">
|
||||||
</react-git-form>
|
</react-git-form>
|
||||||
</ng-form>`,
|
</ng-form>`,
|
||||||
|
@ -25,9 +26,10 @@ export const gitForm: IComponentOptions = {
|
||||||
deployMethod: '@',
|
deployMethod: '@',
|
||||||
baseWebhookUrl: '@',
|
baseWebhookUrl: '@',
|
||||||
isAdditionalFilesFieldVisible: '<',
|
isAdditionalFilesFieldVisible: '<',
|
||||||
isAutoUpdateVisible: '<',
|
|
||||||
isForcePullVisible: '<',
|
isForcePullVisible: '<',
|
||||||
isAuthExplanationVisible: '<',
|
isAuthExplanationVisible: '<',
|
||||||
|
webhookId: '@',
|
||||||
|
webhooksDocs: '@',
|
||||||
},
|
},
|
||||||
controller,
|
controller,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { confirm } from '@@/modals/confirm';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
import { ModalType } from '@@/modals';
|
import { ModalType } from '@@/modals';
|
||||||
import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
||||||
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
|
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
|
|
||||||
class KubernetesRedeployAppGitFormController {
|
class KubernetesRedeployAppGitFormController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -20,6 +20,7 @@ class KubernetesRedeployAppGitFormController {
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
baseWebhookUrl: baseStackWebhookUrl(),
|
baseWebhookUrl: baseStackWebhookUrl(),
|
||||||
|
webhookId: createWebhookId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
|
@ -117,7 +118,7 @@ class KubernetesRedeployAppGitFormController {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
try {
|
try {
|
||||||
this.state.saveGitSettingsInProgress = true;
|
this.state.saveGitSettingsInProgress = true;
|
||||||
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, null, this.formValues);
|
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, { gitConfig: this.formValues, webhookId: this.state.webhookId });
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
this.state.hasUnsavedChanges = false;
|
this.state.hasUnsavedChanges = false;
|
||||||
this.Notifications.success('Success', 'Save stack settings successfully');
|
this.Notifications.success('Success', 'Save stack settings successfully');
|
||||||
|
@ -138,6 +139,10 @@ class KubernetesRedeployAppGitFormController {
|
||||||
|
|
||||||
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
||||||
|
|
||||||
|
if (this.stack.AutoUpdate.Webhook) {
|
||||||
|
this.state.webhookId = this.stack.AutoUpdate.Webhook;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
||||||
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
||||||
this.formValues.RepositoryAuthentication = true;
|
this.formValues.RepositoryAuthentication = true;
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
on-change="($ctrl.onChangeAutoUpdate)"
|
on-change="($ctrl.onChangeAutoUpdate)"
|
||||||
environment-type="KUBERNETES"
|
environment-type="KUBERNETES"
|
||||||
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
|
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
|
||||||
|
webhook-id="{{ $ctrl.state.webhookId }}"
|
||||||
|
webhooks-docs="https://docs.portainer.io/user/kubernetes/applications/webhooks"
|
||||||
></git-form-auto-update-fieldset>
|
></git-form-auto-update-fieldset>
|
||||||
<time-window-display></time-window-display>
|
<time-window-display></time-window-display>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
import { confirmStackUpdate } from '@/react/docker/stacks/common/confirm-stack-update';
|
import { confirmStackUpdate } from '@/react/docker/stacks/common/confirm-stack-update';
|
||||||
|
|
||||||
import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
import { parseAutoUpdateResponse } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
||||||
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
|
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
|
|
||||||
class StackRedeployGitFormController {
|
class StackRedeployGitFormController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -23,6 +23,7 @@ class StackRedeployGitFormController {
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
baseWebhookUrl: baseStackWebhookUrl(),
|
baseWebhookUrl: baseStackWebhookUrl(),
|
||||||
|
webhookId: createWebhookId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
|
@ -132,7 +133,8 @@ class StackRedeployGitFormController {
|
||||||
this.stack.Id,
|
this.stack.Id,
|
||||||
this.stack.EndpointId,
|
this.stack.EndpointId,
|
||||||
this.FormHelper.removeInvalidEnvVars(this.formValues.Env),
|
this.FormHelper.removeInvalidEnvVars(this.formValues.Env),
|
||||||
this.formValues
|
this.formValues,
|
||||||
|
this.state.webhookId
|
||||||
);
|
);
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
this.state.hasUnsavedChanges = false;
|
this.state.hasUnsavedChanges = false;
|
||||||
|
@ -180,6 +182,10 @@ class StackRedeployGitFormController {
|
||||||
|
|
||||||
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
this.formValues.AutoUpdate = parseAutoUpdateResponse(this.stack.AutoUpdate);
|
||||||
|
|
||||||
|
if (this.stack.AutoUpdate.Webhook) {
|
||||||
|
this.state.webhookId = this.stack.AutoUpdate.Webhook;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
||||||
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
||||||
this.formValues.RepositoryAuthentication = true;
|
this.formValues.RepositoryAuthentication = true;
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
environment-type="DOCKER"
|
environment-type="DOCKER"
|
||||||
is-force-pull-visible="$ctrl.stack.Type !== 3"
|
is-force-pull-visible="$ctrl.stack.Type !== 3"
|
||||||
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
|
base-webhook-url="{{ $ctrl.state.baseWebhookUrl }}"
|
||||||
|
webhook-id="{{ $ctrl.state.webhookId }}"
|
||||||
|
webhooks-docs="https://docs.portainer.io/user/docker/stacks/webhooks"
|
||||||
></git-form-auto-update-fieldset>
|
></git-form-auto-update-fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
import { API_ENDPOINT_STACKS, API_ENDPOINT_WEBHOOKS } from '@/constants';
|
import { API_ENDPOINT_STACKS, API_ENDPOINT_WEBHOOKS } from '@/constants';
|
||||||
|
|
||||||
import { baseHref } from './pathHelper';
|
import { baseHref } from './pathHelper';
|
||||||
|
@ -16,6 +18,10 @@ export function stackWebhookUrl(token: string) {
|
||||||
return `${baseStackWebhookUrl()}/${token}`;
|
return `${baseStackWebhookUrl()}/${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createWebhookId() {
|
||||||
|
return uuid();
|
||||||
|
}
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
export function WebhookHelperFactory() {
|
export function WebhookHelperFactory() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -25,6 +25,8 @@ export const gitFormModule = angular
|
||||||
'isAuthExplanationVisible',
|
'isAuthExplanationVisible',
|
||||||
'errors',
|
'errors',
|
||||||
'baseWebhookUrl',
|
'baseWebhookUrl',
|
||||||
|
'webhookId',
|
||||||
|
'webhooksDocs',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
@ -46,6 +48,8 @@ export const gitFormModule = angular
|
||||||
'isForcePullVisible',
|
'isForcePullVisible',
|
||||||
'errors',
|
'errors',
|
||||||
'baseWebhookUrl',
|
'baseWebhookUrl',
|
||||||
|
'webhookId',
|
||||||
|
'webhooksDocs',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -270,7 +270,7 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
).$promise;
|
).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateKubeStack = function (stack, stackFile, gitConfig) {
|
service.updateKubeStack = function (stack, { stackFile, gitConfig, webhookId }) {
|
||||||
let payload = {};
|
let payload = {};
|
||||||
|
|
||||||
if (stackFile) {
|
if (stackFile) {
|
||||||
|
@ -279,7 +279,7 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate),
|
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate, webhookId),
|
||||||
RepositoryReferenceName: gitConfig.RefName,
|
RepositoryReferenceName: gitConfig.RefName,
|
||||||
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
||||||
RepositoryUsername: gitConfig.RepositoryUsername,
|
RepositoryUsername: gitConfig.RepositoryUsername,
|
||||||
|
@ -455,11 +455,11 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
).$promise;
|
).$promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
service.updateGitStackSettings = function (id, endpointId, env, gitConfig) {
|
service.updateGitStackSettings = function (id, endpointId, env, gitConfig, webhookId) {
|
||||||
return Stack.updateGitStackSettings(
|
return Stack.updateGitStackSettings(
|
||||||
{ endpointId, id },
|
{ endpointId, id },
|
||||||
{
|
{
|
||||||
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate),
|
AutoUpdate: transformAutoUpdateViewModel(gitConfig.AutoUpdate, webhookId),
|
||||||
Env: env,
|
Env: env,
|
||||||
RepositoryReferenceName: gitConfig.RefName,
|
RepositoryReferenceName: gitConfig.RefName,
|
||||||
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { renderTemplate } from '@/react/portainer/custom-templates/components/ut
|
||||||
import { editor, upload, git, customTemplate } from '@@/BoxSelector/common-options/build-methods';
|
import { editor, upload, git, customTemplate } from '@@/BoxSelector/common-options/build-methods';
|
||||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||||
import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
|
||||||
import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
|
import { baseStackWebhookUrl, createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer.app')
|
.module('portainer.app')
|
||||||
|
@ -70,6 +70,7 @@ angular
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
selectedTemplateId: null,
|
selectedTemplateId: null,
|
||||||
baseWebhookUrl: baseStackWebhookUrl(),
|
baseWebhookUrl: baseStackWebhookUrl(),
|
||||||
|
webhookId: createWebhookId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
$window.onbeforeunload = () => {
|
$window.onbeforeunload = () => {
|
||||||
|
@ -153,6 +154,7 @@ angular
|
||||||
function createSwarmStack(name, method) {
|
function createSwarmStack(name, method) {
|
||||||
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
|
||||||
const endpointId = +$state.params.endpointId;
|
const endpointId = +$state.params.endpointId;
|
||||||
|
|
||||||
if (method === 'template' || method === 'editor') {
|
if (method === 'template' || method === 'editor') {
|
||||||
var stackFileContent = $scope.formValues.StackFileContent;
|
var stackFileContent = $scope.formValues.StackFileContent;
|
||||||
return StackService.createSwarmStackFromFileContent(name, stackFileContent, env, endpointId);
|
return StackService.createSwarmStackFromFileContent(name, stackFileContent, env, endpointId);
|
||||||
|
@ -172,7 +174,7 @@ angular
|
||||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||||
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
|
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId),
|
||||||
};
|
};
|
||||||
|
|
||||||
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||||
|
@ -198,7 +200,7 @@ angular
|
||||||
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
|
||||||
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
RepositoryUsername: $scope.formValues.RepositoryUsername,
|
||||||
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
RepositoryPassword: $scope.formValues.RepositoryPassword,
|
||||||
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
|
AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate, $scope.state.webhookId),
|
||||||
};
|
};
|
||||||
|
|
||||||
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
|
||||||
|
|
|
@ -83,10 +83,11 @@
|
||||||
on-change="(onChangeFormValues)"
|
on-change="(onChangeFormValues)"
|
||||||
is-docker-standalone="isDockerStandalone"
|
is-docker-standalone="isDockerStandalone"
|
||||||
is-additional-files-field-visible="true"
|
is-additional-files-field-visible="true"
|
||||||
is-auto-update-visible="true"
|
|
||||||
is-auth-explanation-visible="true"
|
is-auth-explanation-visible="true"
|
||||||
is-force-pull-visible="true"
|
is-force-pull-visible="true"
|
||||||
base-webhook-url="{{ state.baseWebhookUrl }}"
|
base-webhook-url="{{ state.baseWebhookUrl }}"
|
||||||
|
webhook-id="{{ state.webhookId }}"
|
||||||
|
webhooks-docs="https://docs.portainer.io/user/docker/stacks/webhooks"
|
||||||
></git-form>
|
></git-form>
|
||||||
|
|
||||||
<div ng-show="state.Method === 'template'">
|
<div ng-show="state.Method === 'template'">
|
||||||
|
|
|
@ -14,6 +14,8 @@ export function AutoUpdateFieldset({
|
||||||
isForcePullVisible = true,
|
isForcePullVisible = true,
|
||||||
errors,
|
errors,
|
||||||
baseWebhookUrl,
|
baseWebhookUrl,
|
||||||
|
webhookId,
|
||||||
|
webhooksDocs,
|
||||||
}: {
|
}: {
|
||||||
value: AutoUpdateModel;
|
value: AutoUpdateModel;
|
||||||
onChange: (value: AutoUpdateModel) => void;
|
onChange: (value: AutoUpdateModel) => void;
|
||||||
|
@ -21,6 +23,8 @@ export function AutoUpdateFieldset({
|
||||||
isForcePullVisible?: boolean;
|
isForcePullVisible?: boolean;
|
||||||
errors?: FormikErrors<AutoUpdateModel>;
|
errors?: FormikErrors<AutoUpdateModel>;
|
||||||
baseWebhookUrl: string;
|
baseWebhookUrl: string;
|
||||||
|
webhookId: string;
|
||||||
|
webhooksDocs?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -45,12 +49,14 @@ export function AutoUpdateFieldset({
|
||||||
|
|
||||||
{value.RepositoryAutomaticUpdates && (
|
{value.RepositoryAutomaticUpdates && (
|
||||||
<AutoUpdateSettings
|
<AutoUpdateSettings
|
||||||
|
webhookId={webhookId}
|
||||||
baseWebhookUrl={baseWebhookUrl}
|
baseWebhookUrl={baseWebhookUrl}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
environmentType={environmentType}
|
environmentType={environmentType}
|
||||||
showForcePullImage={isForcePullVisible}
|
showForcePullImage={isForcePullVisible}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
webhookDocs={webhooksDocs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -19,6 +19,8 @@ export function AutoUpdateSettings({
|
||||||
showForcePullImage,
|
showForcePullImage,
|
||||||
errors,
|
errors,
|
||||||
baseWebhookUrl,
|
baseWebhookUrl,
|
||||||
|
webhookId,
|
||||||
|
webhookDocs,
|
||||||
}: {
|
}: {
|
||||||
value: AutoUpdateModel;
|
value: AutoUpdateModel;
|
||||||
onChange: (value: Partial<AutoUpdateModel>) => void;
|
onChange: (value: Partial<AutoUpdateModel>) => void;
|
||||||
|
@ -26,6 +28,8 @@ export function AutoUpdateSettings({
|
||||||
showForcePullImage: boolean;
|
showForcePullImage: boolean;
|
||||||
errors?: FormikErrors<AutoUpdateModel>;
|
errors?: FormikErrors<AutoUpdateModel>;
|
||||||
baseWebhookUrl: string;
|
baseWebhookUrl: string;
|
||||||
|
webhookId: string;
|
||||||
|
webhookDocs?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -50,12 +54,8 @@ export function AutoUpdateSettings({
|
||||||
{value.RepositoryMechanism === 'Webhook' && (
|
{value.RepositoryMechanism === 'Webhook' && (
|
||||||
<WebhookSettings
|
<WebhookSettings
|
||||||
baseUrl={baseWebhookUrl}
|
baseUrl={baseWebhookUrl}
|
||||||
value={value.RepositoryWebhookId || ''}
|
value={webhookId}
|
||||||
docsLink={
|
docsLink={webhookDocs}
|
||||||
environmentType === 'KUBERNETES'
|
|
||||||
? 'https://docs.portainer.io/user/kubernetes/applications/webhooks'
|
|
||||||
: 'https://docs.portainer.io/user/docker/stacks/webhooks'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function WebhookSettings({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
docsLink,
|
docsLink,
|
||||||
}: {
|
}: {
|
||||||
docsLink: string;
|
docsLink?: string;
|
||||||
value: string;
|
value: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -18,13 +18,15 @@ export function WebhookSettings({
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Webhook"
|
label="Webhook"
|
||||||
tooltip={
|
tooltip={
|
||||||
<>
|
!!docsLink && (
|
||||||
See{' '}
|
<>
|
||||||
<a href={docsLink} target="_blank" rel="noreferrer">
|
See{' '}
|
||||||
Portainer documentation on webhook usage
|
<a href={docsLink} target="_blank" rel="noreferrer">
|
||||||
</a>
|
Portainer documentation on webhook usage
|
||||||
.
|
</a>
|
||||||
</>
|
.
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { AutoUpdateResponse, AutoUpdateModel } from '../types';
|
import { AutoUpdateResponse, AutoUpdateModel } from '../types';
|
||||||
|
|
||||||
export function parseAutoUpdateResponse(
|
export function parseAutoUpdateResponse(
|
||||||
|
@ -11,7 +9,6 @@ export function parseAutoUpdateResponse(
|
||||||
RepositoryAutomaticUpdatesForce: false,
|
RepositoryAutomaticUpdatesForce: false,
|
||||||
RepositoryMechanism: 'Interval',
|
RepositoryMechanism: 'Interval',
|
||||||
RepositoryFetchInterval: '5m',
|
RepositoryFetchInterval: '5m',
|
||||||
RepositoryWebhookId: uuid(),
|
|
||||||
ForcePullImage: false,
|
ForcePullImage: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,28 +17,30 @@ export function parseAutoUpdateResponse(
|
||||||
RepositoryAutomaticUpdates: true,
|
RepositoryAutomaticUpdates: true,
|
||||||
RepositoryMechanism: response.Interval ? 'Interval' : 'Webhook',
|
RepositoryMechanism: response.Interval ? 'Interval' : 'Webhook',
|
||||||
RepositoryFetchInterval: response.Interval || '',
|
RepositoryFetchInterval: response.Interval || '',
|
||||||
RepositoryWebhookId: response.Webhook || uuid(),
|
|
||||||
RepositoryAutomaticUpdatesForce: response.ForceUpdate,
|
RepositoryAutomaticUpdatesForce: response.ForceUpdate,
|
||||||
ForcePullImage: response.ForcePullImage,
|
ForcePullImage: response.ForcePullImage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformAutoUpdateViewModel(
|
export function transformAutoUpdateViewModel(
|
||||||
viewModel?: AutoUpdateModel
|
viewModel?: AutoUpdateModel,
|
||||||
|
webhookId?: string
|
||||||
): AutoUpdateResponse | null {
|
): AutoUpdateResponse | null {
|
||||||
if (!viewModel || !viewModel.RepositoryAutomaticUpdates) {
|
if (!viewModel || !viewModel.RepositoryAutomaticUpdates) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (viewModel.RepositoryMechanism === 'Webhook' && !webhookId) {
|
||||||
|
throw new Error('Webhook ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Interval:
|
Interval:
|
||||||
viewModel.RepositoryMechanism === 'Interval'
|
viewModel.RepositoryMechanism === 'Interval'
|
||||||
? viewModel.RepositoryFetchInterval
|
? viewModel.RepositoryFetchInterval
|
||||||
: '',
|
: '',
|
||||||
Webhook:
|
Webhook:
|
||||||
viewModel.RepositoryMechanism === 'Webhook'
|
viewModel.RepositoryMechanism === 'Webhook' && webhookId ? webhookId : '',
|
||||||
? viewModel.RepositoryWebhookId
|
|
||||||
: '',
|
|
||||||
ForceUpdate: viewModel.RepositoryAutomaticUpdatesForce,
|
ForceUpdate: viewModel.RepositoryAutomaticUpdatesForce,
|
||||||
ForcePullImage: viewModel.ForcePullImage,
|
ForcePullImage: viewModel.ForcePullImage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -90,6 +90,7 @@ export function Primary({
|
||||||
isForcePullVisible={isForcePullVisible}
|
isForcePullVisible={isForcePullVisible}
|
||||||
deployMethod={deployMethod}
|
deployMethod={deployMethod}
|
||||||
baseWebhookUrl="ws://localhost:9000"
|
baseWebhookUrl="ws://localhost:9000"
|
||||||
|
webhookId="1234"
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -28,6 +28,8 @@ interface Props {
|
||||||
isAuthExplanationVisible?: boolean;
|
isAuthExplanationVisible?: boolean;
|
||||||
errors: FormikErrors<GitFormModel>;
|
errors: FormikErrors<GitFormModel>;
|
||||||
baseWebhookUrl: string;
|
baseWebhookUrl: string;
|
||||||
|
webhookId: string;
|
||||||
|
webhooksDocs?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitForm({
|
export function GitForm({
|
||||||
|
@ -40,6 +42,8 @@ export function GitForm({
|
||||||
isAuthExplanationVisible,
|
isAuthExplanationVisible,
|
||||||
errors = {},
|
errors = {},
|
||||||
baseWebhookUrl,
|
baseWebhookUrl,
|
||||||
|
webhookId,
|
||||||
|
webhooksDocs,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<FormSection title="Git repository">
|
<FormSection title="Git repository">
|
||||||
|
@ -89,11 +93,13 @@ export function GitForm({
|
||||||
|
|
||||||
{value.AutoUpdate && (
|
{value.AutoUpdate && (
|
||||||
<AutoUpdateFieldset
|
<AutoUpdateFieldset
|
||||||
|
webhookId={webhookId}
|
||||||
baseWebhookUrl={baseWebhookUrl}
|
baseWebhookUrl={baseWebhookUrl}
|
||||||
value={value.AutoUpdate}
|
value={value.AutoUpdate}
|
||||||
onChange={(value) => handleChange({ AutoUpdate: value })}
|
onChange={(value) => handleChange({ AutoUpdate: value })}
|
||||||
isForcePullVisible={isForcePullVisible}
|
isForcePullVisible={isForcePullVisible}
|
||||||
errors={errors.AutoUpdate as FormikErrors<GitFormModel['AutoUpdate']>}
|
errors={errors.AutoUpdate as FormikErrors<GitFormModel['AutoUpdate']>}
|
||||||
|
webhooksDocs={webhooksDocs}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ export interface RepoConfigResponse {
|
||||||
export type AutoUpdateModel = {
|
export type AutoUpdateModel = {
|
||||||
RepositoryAutomaticUpdates: boolean;
|
RepositoryAutomaticUpdates: boolean;
|
||||||
RepositoryMechanism: AutoUpdateMechanism;
|
RepositoryMechanism: AutoUpdateMechanism;
|
||||||
RepositoryWebhookId: string;
|
|
||||||
RepositoryFetchInterval: string;
|
RepositoryFetchInterval: string;
|
||||||
ForcePullImage: boolean;
|
ForcePullImage: boolean;
|
||||||
RepositoryAutomaticUpdatesForce: boolean;
|
RepositoryAutomaticUpdatesForce: boolean;
|
||||||
|
|
Loading…
Reference in New Issue