mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
refactor(stacks): extract auto update logic [EE-4945] (#8545)
This commit is contained in:
parent
085381e6fc
commit
6918da2414
42 changed files with 410 additions and 166 deletions
62
api/git/backup.go
Normal file
62
api/git/backup.go
Normal file
|
@ -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
|
||||
}
|
13
api/git/credentials.go
Normal file
13
api/git/credentials.go
Normal file
|
@ -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
|
||||
}
|
94
api/git/update/update.go
Normal file
94
api/git/update/update.go
Normal file
|
@ -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, "", "")
|
||||
}
|
31
api/git/update/validate.go
Normal file
31
api/git/update/validate.go
Normal file
|
@ -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
|
||||
}
|
42
api/git/update/validate_test.go
Normal file
42
api/git/update/validate_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ValidateAutoUpdate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *portainer.AutoUpdateSettings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "webhook is not a valid UUID",
|
||||
value: &portainer.AutoUpdateSettings{Webhook: "fake-webhook"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "incorrect interval value",
|
||||
value: &portainer.AutoUpdateSettings{Interval: "1dd2hh3mm"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid auto update",
|
||||
value: &portainer.AutoUpdateSettings{
|
||||
Webhook: "8dce8c2f-9ca1-482b-ad20-271e86536ada",
|
||||
Interval: "5h30m40s10ms",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateAutoUpdateSettings(tt.value)
|
||||
assert.Equalf(t, tt.wantErr, err != nil, "received %+v", err)
|
||||
})
|
||||
}
|
||||
}
|
25
api/git/validate.go
Normal file
25
api/git/validate.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue