mirror of https://github.com/portainer/portainer
feat(k8s): support git automated sync for k8s applications [EE-577] (#5548)
* feat(stack): backport changes to CE EE-1189 * feat(stack): front end backport changes to CE EE-1199 (#5455) * feat(stack): front end backport changes to CE EE-1199 * fix k8s deploy logic * fixed web editor confirmation message typo. EE-1501 * fix(stack): fixed issue auth detail not remembered EE-1502 (#5459) * show status in buttons * removed onChangeRef function. * moved buttons in git form to its own component * removed unused variable. Co-authored-by: ArrisLee <arris_li@hotmail.com> * moved formvalue to kube app component * fix(stack): failed to pull and redeploy compose format k8s stack * fixed form value * fix(k8s): file content overridden when deployment failed with compose format EE-1548 * updated API response to get IsComposeFormat and show appropriate text. * feat(k8s): front end backport to CE * feat(kube): kube app auto update backend (#5547) * error message updates for different file type * not display creation source for external application * added confirmation modal to advanced app created by web editor * stop showing confirmation modal when updating application * disable rollback button when application type is not applicatiom form * only update file after deployment succeded * Revert "only update file after deployment succeded" This reverts commitpull/5643/headb94bd2e96f
. * fix(k8s): file content overridden when deployment failed with compose format EE-1556 * added analytics-on directive to pull and redeploy button * fix(kube): don't valide resource control access for kube (#5568) * added missing question mark to k8s confirmation modal * fixed webhook format issue * added question marks to k8s app confirmation modal * added space in additional file list. * ignoring error on deletion * fix(k8s): Git authentication info not persisted * added RepositoryMechanismTypes constant * updated analytics functions * covert RepositoryMechanism to constant * fixed typo * removed unused function. * post tech review updates * fixed save settings n redeploy button * refact kub deploy logic * Revert "refact kub deploy logic" This reverts commitcbfdd58ece
. * feat(k8s): utilize user token for k8s auto update EE-1594 * feat(k8s): persist kub stack name EE-1630 * feat(k8s): support delete kub stack * fix(app): updated logic to delete stack for different kind apps. (#5648) * fix(app): updated logic to delete stack for different kind apps. * renamed variable * fix import * added StackName field. * fixed stack id not found issue. * fix(k8s): fixed qusetion mark alignment issue in PAT field. (#5611) * fix(k8s): fixed qusetion mark alignment issue in PAT field. * moved inline css to file. * fix(git-form: made auth input text full width * add ignore deleted arg * tech review updates * typo fix * fix(k8s): added console error when deleting k8s service. * fix(console): added no-console config * fix(deploy): added missing service. * fix: use stack editor as an owner when exists (#5678) * fix: tempalte/content based stacks edit/delete * fix(stack): remove stack when no app. (#5769) * fix(stack): remove stack when no app. * support compose format in delete Co-authored-by: ArrisLee <arris_li@hotmail.com> Co-authored-by: Hui <arris_li@hotmail.com> Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Co-authored-by: Felix Han <felix.han@portainer.io>
parent
fce885901f
commit
2ecc8ab5c9
|
@ -553,7 +553,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager)
|
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
||||||
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
|
|
|
@ -47,7 +47,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||||
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to fetch endpoint proxy")
|
return errors.Wrap(err, "failed to fetch environment proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxy != nil {
|
if proxy != nil {
|
||||||
|
@ -80,7 +80,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||||
func (w *ComposeStackManager) NormalizeStackName(name string) string {
|
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||||
r := regexp.MustCompile("[^a-z0-9]+")
|
r := regexp.MustCompile("[^a-z0-9]+")
|
||||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package exectest
|
package exectest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +10,11 @@ func NewKubernetesDeployer() portainer.KubernetesDeployer {
|
||||||
return &kubernetesMockDeployer{}
|
return &kubernetesMockDeployer{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployer *kubernetesMockDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, data string, namespace string) (string, error) {
|
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package exec
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -13,7 +12,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -43,12 +41,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -61,11 +54,16 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
user, err := deployer.dataStore.User().User(userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to fetch the user")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Role == portainer.AdministratorRole {
|
||||||
return tokenManager.GetAdminServiceAccountToken(), nil
|
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
|
token, err := tokenManager.GetUserServiceAccountToken(int(user.ID), endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -76,15 +74,31 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy will deploy a Kubernetes manifest inside an optional namespace in a Kubernetes environment(endpoint).
|
// Deploy upserts Kubernetes resources defined in manifest(s)
|
||||||
// Otherwise it will use kubectl to deploy the manifest.
|
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deletes Kubernetes resources defined in manifest(s)
|
||||||
|
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||||
|
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed generating a user token")
|
||||||
|
}
|
||||||
|
|
||||||
command := path.Join(deployer.binaryPath, "kubectl")
|
command := path.Join(deployer.binaryPath, "kubectl")
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||||
}
|
}
|
||||||
|
|
||||||
args := make([]string, 0)
|
args := []string{"--token", token}
|
||||||
|
if namespace != "" {
|
||||||
|
args = append(args, "--namespace", namespace)
|
||||||
|
}
|
||||||
|
|
||||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||||
url, proxy, err := deployer.getAgentURL(endpoint)
|
url, proxy, err := deployer.getAgentURL(endpoint)
|
||||||
|
@ -97,21 +111,18 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port
|
||||||
args = append(args, "--insecure-skip-tls-verify")
|
args = append(args, "--insecure-skip-tls-verify")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := deployer.getToken(request, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
|
if operation == "delete" {
|
||||||
if err != nil {
|
args = append(args, "--ignore-not-found=true")
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "--token", token)
|
args = append(args, operation)
|
||||||
if namespace != "" {
|
for _, path := range manifestFiles {
|
||||||
args = append(args, "--namespace", namespace)
|
args = append(args, "-f", strings.TrimSpace(path))
|
||||||
}
|
}
|
||||||
args = append(args, "apply", "-f", "-")
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
cmd.Stdin = strings.NewReader(stackConfig)
|
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package filesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteToFile(dst string, content []byte) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to open a file %q", dst)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write(content)
|
||||||
|
return errors.Wrapf(err, "failed to write a file %q", dst)
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package filesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||||
|
|
||||||
|
content := []byte("content")
|
||||||
|
err := WriteToFile(tmpFilePath, content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||||
|
assert.Equal(t, content, fileContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||||
|
|
||||||
|
err := WriteToFile(tmpFilePath, []byte("content"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
content := []byte("new content")
|
||||||
|
err = WriteToFile(tmpFilePath, content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||||
|
assert.Equal(t, content, fileContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tmpFilePath := path.Join(tmpDir, "dir", "sub-dir", "dummy")
|
||||||
|
|
||||||
|
content := []byte("content")
|
||||||
|
err := WriteToFile(tmpFilePath, content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||||
|
assert.Equal(t, content, fileContent)
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package helm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -182,6 +183,11 @@ func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte,
|
||||||
return errors.Wrap(err, "unable to find an endpoint on request context")
|
return errors.Wrap(err, "unable to find an endpoint on request context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to retrieve user details from authentication token")
|
||||||
|
}
|
||||||
|
|
||||||
// extract list of yaml resources from helm manifest
|
// extract list of yaml resources from helm manifest
|
||||||
yamlResources, err := kubernetes.ExtractDocuments(manifest, nil)
|
yamlResources, err := kubernetes.ExtractDocuments(manifest, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -193,6 +199,19 @@ func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte,
|
||||||
for _, resource := range yamlResources {
|
for _, resource := range yamlResources {
|
||||||
resource := resource // https://golang.org/doc/faq#closures_and_goroutines
|
resource := resource // https://golang.org/doc/faq#closures_and_goroutines
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
|
tmpfile, err := ioutil.TempFile("", "helm-manifest-*")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create a tmp helm manifest file")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tmpfile.Close()
|
||||||
|
os.Remove(tmpfile.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := tmpfile.Write(resource); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write a tmp helm manifest file")
|
||||||
|
}
|
||||||
|
|
||||||
// get resource namespace, fallback to provided namespace if not explicit on resource
|
// get resource namespace, fallback to provided namespace if not explicit on resource
|
||||||
resourceNamespace, err := kubernetes.GetNamespace(resource)
|
resourceNamespace, err := kubernetes.GetNamespace(resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -201,7 +220,8 @@ func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte,
|
||||||
if resourceNamespace == "" {
|
if resourceNamespace == "" {
|
||||||
resourceNamespace = namespace
|
resourceNamespace = namespace
|
||||||
}
|
}
|
||||||
_, err = handler.kubernetesDeployer.Deploy(r, endpoint, string(resource), resourceNamespace)
|
|
||||||
|
_, err = handler.kubernetesDeployer.Deploy(tokenData.ID, endpoint, []string{tmpfile.Name()}, resourceNamespace)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,8 @@ func startAutoupdate(stackID portainer.StackID, interval string, scheduler *sche
|
||||||
return "", &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Unable to parse stack's auto update interval", Err: err}
|
return "", &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Unable to parse stack's auto update interval", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
jobID = scheduler.StartJobEvery(d, func() {
|
jobID = scheduler.StartJobEvery(d, func() error {
|
||||||
if err := stacks.RedeployWhenChanged(stackID, stackDeployer, datastore, gitService); err != nil {
|
return stacks.RedeployWhenChanged(stackID, stackDeployer, datastore, gitService)
|
||||||
log.Printf("[ERROR] [http,stacks] [message: failed redeploying] [err: %s]\n", err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return jobID, nil
|
return jobID, nil
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
|
|
||||||
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
|
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
payload.ComposeFile = filesystem.ComposeFileDefaultName
|
payload.ComposeFile = filesystem.ComposeFileDefaultName
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
}
|
}
|
||||||
|
@ -208,11 +208,11 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
|
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
|
||||||
}
|
}
|
||||||
stack.GitConfig.ConfigHash = commitId
|
stack.GitConfig.ConfigHash = commitID
|
||||||
|
|
||||||
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||||
if configErr != nil {
|
if configErr != nil {
|
||||||
|
@ -281,7 +281,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
|
|
||||||
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
|
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -19,16 +18,19 @@ import (
|
||||||
"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/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
|
"github.com/portainer/portainer/api/internal/stackutils"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type kubernetesStringDeploymentPayload struct {
|
type kubernetesStringDeploymentPayload struct {
|
||||||
|
StackName string
|
||||||
ComposeFormat bool
|
ComposeFormat bool
|
||||||
Namespace string
|
Namespace string
|
||||||
StackFileContent string
|
StackFileContent string
|
||||||
}
|
}
|
||||||
|
|
||||||
type kubernetesGitDeploymentPayload struct {
|
type kubernetesGitDeploymentPayload struct {
|
||||||
|
StackName string
|
||||||
ComposeFormat bool
|
ComposeFormat bool
|
||||||
Namespace string
|
Namespace string
|
||||||
RepositoryURL string
|
RepositoryURL string
|
||||||
|
@ -36,10 +38,13 @@ type kubernetesGitDeploymentPayload struct {
|
||||||
RepositoryAuthentication bool
|
RepositoryAuthentication bool
|
||||||
RepositoryUsername string
|
RepositoryUsername string
|
||||||
RepositoryPassword string
|
RepositoryPassword string
|
||||||
FilePathInRepository string
|
ManifestFile string
|
||||||
|
AdditionalFiles []string
|
||||||
|
AutoUpdate *portainer.StackAutoUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
type kubernetesManifestURLDeploymentPayload struct {
|
type kubernetesManifestURLDeploymentPayload struct {
|
||||||
|
StackName string
|
||||||
Namespace string
|
Namespace string
|
||||||
ComposeFormat bool
|
ComposeFormat bool
|
||||||
ManifestURL string
|
ManifestURL string
|
||||||
|
@ -52,6 +57,9 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
|
||||||
if govalidator.IsNull(payload.Namespace) {
|
if govalidator.IsNull(payload.Namespace) {
|
||||||
return errors.New("Invalid namespace")
|
return errors.New("Invalid namespace")
|
||||||
}
|
}
|
||||||
|
if govalidator.IsNull(payload.StackName) {
|
||||||
|
return errors.New("Invalid stack name")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,12 +73,18 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||||
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 govalidator.IsNull(payload.FilePathInRepository) {
|
if govalidator.IsNull(payload.ManifestFile) {
|
||||||
return errors.New("Invalid file path in repository")
|
return errors.New("Invalid manifest file in repository")
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||||
}
|
}
|
||||||
|
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if govalidator.IsNull(payload.StackName) {
|
||||||
|
return errors.New("Invalid stack name")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +92,9 @@ func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request)
|
||||||
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
||||||
return errors.New("Invalid manifest URL")
|
return errors.New("Invalid manifest URL")
|
||||||
}
|
}
|
||||||
|
if govalidator.IsNull(payload.StackName) {
|
||||||
|
return errors.New("Invalid stack name")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +112,13 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||||
}
|
}
|
||||||
|
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
|
}
|
||||||
|
if !isUnique {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
|
||||||
|
}
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
|
@ -102,6 +126,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
Type: portainer.KubernetesStack,
|
Type: portainer.KubernetesStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ManifestFileDefaultName,
|
EntryPoint: filesystem.ManifestFileDefaultName,
|
||||||
|
Name: payload.StackName,
|
||||||
Namespace: payload.Namespace,
|
Namespace: payload.Namespace,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
CreationDate: time.Now().Unix(),
|
CreationDate: time.Now().Unix(),
|
||||||
|
@ -124,9 +149,9 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
doCleanUp := true
|
doCleanUp := true
|
||||||
defer handler.cleanUp(stack, &doCleanUp)
|
defer handler.cleanUp(stack, &doCleanUp)
|
||||||
|
|
||||||
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
Name: stack.Name,
|
StackName: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
Kind: "content",
|
Kind: "content",
|
||||||
})
|
})
|
||||||
|
@ -140,12 +165,11 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
|
||||||
|
|
||||||
resp := &createKubernetesStackResponse{
|
resp := &createKubernetesStackResponse{
|
||||||
Output: output,
|
Output: output,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doCleanUp = false
|
||||||
return response.JSON(w, resp)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,23 +183,44 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||||
}
|
}
|
||||||
|
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
|
}
|
||||||
|
if !isUnique {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
|
||||||
|
}
|
||||||
|
|
||||||
|
//make sure the webhook ID is unique
|
||||||
|
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
|
||||||
|
isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for webhook ID collision", Err: err}
|
||||||
|
}
|
||||||
|
if !isUnique {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Type: portainer.KubernetesStack,
|
Type: portainer.KubernetesStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: payload.FilePathInRepository,
|
EntryPoint: payload.ManifestFile,
|
||||||
GitConfig: &gittypes.RepoConfig{
|
GitConfig: &gittypes.RepoConfig{
|
||||||
URL: payload.RepositoryURL,
|
URL: payload.RepositoryURL,
|
||||||
ReferenceName: payload.RepositoryReferenceName,
|
ReferenceName: payload.RepositoryReferenceName,
|
||||||
ConfigFilePath: payload.FilePathInRepository,
|
ConfigFilePath: payload.ManifestFile,
|
||||||
},
|
},
|
||||||
Namespace: payload.Namespace,
|
Namespace: payload.Namespace,
|
||||||
|
Name: payload.StackName,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
CreationDate: time.Now().Unix(),
|
CreationDate: time.Now().Unix(),
|
||||||
CreatedBy: user.Username,
|
CreatedBy: user.Username,
|
||||||
IsComposeFormat: payload.ComposeFormat,
|
IsComposeFormat: payload.ComposeFormat,
|
||||||
|
AutoUpdate: payload.AutoUpdate,
|
||||||
|
AdditionalFiles: payload.AdditionalFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.RepositoryAuthentication {
|
if payload.RepositoryAuthentication {
|
||||||
|
@ -197,14 +242,21 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
}
|
}
|
||||||
stack.GitConfig.ConfigHash = commitID
|
stack.GitConfig.ConfigHash = commitID
|
||||||
|
|
||||||
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
|
repositoryUsername := payload.RepositoryUsername
|
||||||
if err != nil {
|
repositoryPassword := payload.RepositoryPassword
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
|
if !payload.RepositoryAuthentication {
|
||||||
|
repositoryUsername = ""
|
||||||
|
repositoryPassword = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to clone git repository", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
Name: stack.Name,
|
StackName: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
Kind: "git",
|
Kind: "git",
|
||||||
})
|
})
|
||||||
|
@ -213,17 +265,25 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
|
||||||
|
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.AutoUpdate.JobID = jobID
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
|
||||||
|
|
||||||
resp := &createKubernetesStackResponse{
|
resp := &createKubernetesStackResponse{
|
||||||
Output: output,
|
Output: output,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doCleanUp = false
|
||||||
return response.JSON(w, resp)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +297,13 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
|
||||||
}
|
}
|
||||||
|
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
|
}
|
||||||
|
if !isUnique {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
|
||||||
|
}
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
|
@ -245,6 +312,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ManifestFileDefaultName,
|
EntryPoint: filesystem.ManifestFileDefaultName,
|
||||||
Namespace: payload.Namespace,
|
Namespace: payload.Namespace,
|
||||||
|
Name: payload.StackName,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
CreationDate: time.Now().Unix(),
|
CreationDate: time.Now().Unix(),
|
||||||
CreatedBy: user.Username,
|
CreatedBy: user.Username,
|
||||||
|
@ -267,9 +335,9 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
|
||||||
doCleanUp := true
|
doCleanUp := true
|
||||||
defer handler.cleanUp(stack, &doCleanUp)
|
defer handler.cleanUp(stack, &doCleanUp)
|
||||||
|
|
||||||
output, err := handler.deployKubernetesStack(r, endpoint, string(manifestContent), payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
|
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
Name: stack.Name,
|
StackName: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
Kind: "url",
|
Kind: "url",
|
||||||
})
|
})
|
||||||
|
@ -291,42 +359,14 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
|
||||||
return response.JSON(w, resp)
|
return response.JSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
|
func (handler *Handler) deployKubernetesStack(userID portainer.UserID, endpoint *portainer.Endpoint, stack *portainer.Stack, appLabels k.KubeAppLabels) (string, error) {
|
||||||
handler.stackCreationMutex.Lock()
|
handler.stackCreationMutex.Lock()
|
||||||
defer handler.stackCreationMutex.Unlock()
|
defer handler.stackCreationMutex.Unlock()
|
||||||
|
|
||||||
manifest := []byte(stackConfig)
|
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, handler.KubernetesDeployer, appLabels)
|
||||||
if composeFormat {
|
|
||||||
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(manifest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
return "", errors.Wrap(err, "failed to create temp kub deployment files")
|
||||||
}
|
}
|
||||||
manifest = convertedConfig
|
defer os.RemoveAll(tempDir)
|
||||||
}
|
return handler.KubernetesDeployer.Deploy(userID, endpoint, manifestFilePaths, stack.Namespace)
|
||||||
|
|
||||||
manifest, err := k.AddAppLabels(manifest, appLabels.ToMap())
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to add application labels")
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
|
|
||||||
repositoryUsername := gitInfo.RepositoryUsername
|
|
||||||
repositoryPassword := gitInfo.RepositoryPassword
|
|
||||||
if !gitInfo.RepositoryAuthentication {
|
|
||||||
repositoryUsername = ""
|
|
||||||
repositoryPassword = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
err := handler.GitService.CloneRepository(projectPath, gitInfo.RepositoryURL, gitInfo.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
content, err := ioutil.ReadFile(filepath.Join(projectPath, gitInfo.FilePathInRepository))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(content), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
package stacks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type git struct {
|
|
||||||
content string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *git) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
|
|
||||||
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
|
|
||||||
}
|
|
||||||
func (g *git) ClonePublicRepository(repositoryURL string, referenceName string, destination string) error {
|
|
||||||
return ioutil.WriteFile(path.Join(destination, "deployment.yml"), []byte(g.content), 0755)
|
|
||||||
}
|
|
||||||
func (g *git) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
|
|
||||||
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *git) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloneAndConvertGitRepoFile(t *testing.T) {
|
|
||||||
dir, err := os.MkdirTemp("", "kube-create-stack")
|
|
||||||
assert.NoError(t, err, "failed to create a tmp dir")
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
content := `apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: nginx-deployment
|
|
||||||
labels:
|
|
||||||
app: nginx
|
|
||||||
spec:
|
|
||||||
replicas: 3
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: nginx
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: nginx
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: nginx
|
|
||||||
image: nginx:1.14.2
|
|
||||||
ports:
|
|
||||||
- containerPort: 80`
|
|
||||||
|
|
||||||
h := &Handler{
|
|
||||||
GitService: &git{
|
|
||||||
content: content,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
gitInfo := &kubernetesGitDeploymentPayload{
|
|
||||||
FilePathInRepository: "deployment.yml",
|
|
||||||
}
|
|
||||||
fileContent, err := h.cloneManifestContentFromGitRepo(gitInfo, dir)
|
|
||||||
assert.NoError(t, err, "failed to clone or convert the file from Git repo")
|
|
||||||
assert.Equal(t, content, fileContent)
|
|
||||||
}
|
|
|
@ -51,7 +51,8 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||||
|
|
||||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||||
}
|
}
|
||||||
|
@ -161,7 +162,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||||
|
|
||||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
}
|
}
|
||||||
|
@ -218,11 +219,11 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
|
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
|
||||||
}
|
}
|
||||||
stack.GitConfig.ConfigHash = commitId
|
stack.GitConfig.ConfigHash = commitID
|
||||||
|
|
||||||
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
|
||||||
if configErr != nil {
|
if configErr != nil {
|
||||||
|
@ -298,7 +299,8 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||||
|
|
||||||
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
|
||||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
|
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
|
||||||
stacks, err := handler.DataStore.Stack().Stacks()
|
stacks, err := handler.DataStore.Stack().Stacks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -139,6 +139,15 @@ func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
|
||||||
|
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -171,7 +180,7 @@ func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return isUniqueStackName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkUniqueWebhookID(webhookID string) (bool, error) {
|
func (handler *Handler) checkUniqueWebhookID(webhookID string) (bool, error) {
|
||||||
|
|
|
@ -2,15 +2,20 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"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"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
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/internal/stackutils"
|
"github.com/portainer/portainer/api/internal/stackutils"
|
||||||
|
@ -34,12 +39,12 @@ import (
|
||||||
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
stackID, err := request.RetrieveRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
|
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
|
||||||
|
@ -49,52 +54,52 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
|
|
||||||
id, err := strconv.Atoi(stackID)
|
id, err := strconv.Atoi(stackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id))
|
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
|
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
|
||||||
|
|
||||||
if isOrphaned && !securityContext.IsAdmin {
|
if isOrphaned && !securityContext.IsAdmin {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to remove orphaned stack", Err: errors.New("Permission denied to remove orphaned stack")}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isOrphaned {
|
if !isOrphaned {
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
||||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||||
}
|
}
|
||||||
if !access {
|
if !access {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,26 +109,26 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.Stack().DeleteStack(portainer.StackID(id))
|
err = handler.DataStore.Stack().DeleteStack(portainer.StackID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the stack from the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceControl != nil {
|
if resourceControl != nil {
|
||||||
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the associated resource control from the database", Err: err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove stack files from disk", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
|
@ -132,31 +137,31 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
|
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
|
||||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
if !securityContext.IsAdmin {
|
||||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
return &httperror.HandlerError{StatusCode: http.StatusUnauthorized, Message: "Permission denied to delete the stack", Err: httperrors.ErrUnauthorized}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
||||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for stack existence inside the database", Err: err}
|
||||||
}
|
}
|
||||||
if stack != nil {
|
if stack != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", errors.New("A tag already exists with this name")}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "A stack with this name exists inside the database. Cannot use external delete method", Err: errors.New("A tag already exists with this name")}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = &portainer.Stack{
|
stack = &portainer.Stack{
|
||||||
|
@ -164,18 +169,57 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete stack", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to delete stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
if stack.Type == portainer.DockerSwarmStack {
|
if stack.Type == portainer.DockerSwarmStack {
|
||||||
return handler.SwarmStackManager.Remove(stack, endpoint)
|
return handler.SwarmStackManager.Remove(stack, endpoint)
|
||||||
}
|
}
|
||||||
|
if stack.Type == portainer.DockerComposeStack {
|
||||||
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
|
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
|
||||||
}
|
}
|
||||||
|
if stack.Type == portainer.KubernetesStack {
|
||||||
|
var manifestFiles []string
|
||||||
|
|
||||||
|
//if it is a compose format kub stack, create a temp dir and convert the manifest files into it
|
||||||
|
//then process the remove operation
|
||||||
|
if stack.IsComposeFormat {
|
||||||
|
fileNames := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
|
||||||
|
tmpDir, err := ioutil.TempDir("", "kub_delete")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create temp directory for deleting kub stack")
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
for _, fileName := range fileNames {
|
||||||
|
manifestFilePath := path.Join(tmpDir, fileName)
|
||||||
|
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read manifest file")
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestContent, err = handler.KubernetesDeployer.ConvertCompose(manifestContent)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create temp manifest file")
|
||||||
|
}
|
||||||
|
manifestFiles = append(manifestFiles, manifestFilePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manifestFiles = stackutils.GetStackFilePaths(stack)
|
||||||
|
}
|
||||||
|
out, err := handler.KubernetesDeployer.Remove(userID, endpoint, manifestFiles, stack.Namespace)
|
||||||
|
return errors.WithMessagef(err, "failed to remove kubernetes resources: %q", out)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported stack type: %v", stack.Type)
|
||||||
|
}
|
||||||
|
|
|
@ -50,52 +50,54 @@ func (payload *stackMigratePayload) Validate(r *http.Request) error {
|
||||||
func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload stackMigratePayload
|
var payload stackMigratePayload
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stack.Type == portainer.KubernetesStack {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Migrating a kubernetes stack is not supported", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||||
}
|
|
||||||
|
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||||
}
|
}
|
||||||
if !access {
|
if !access {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
|
@ -103,7 +105,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
// can use the optional EndpointID query parameter to associate a valid environment(endpoint) identifier to the stack.
|
// can use the optional EndpointID query parameter to associate a valid environment(endpoint) identifier to the stack.
|
||||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
|
||||||
}
|
}
|
||||||
if endpointID != int(stack.EndpointID) {
|
if endpointID != int(stack.EndpointID) {
|
||||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||||
|
@ -111,9 +113,9 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
|
|
||||||
targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID))
|
targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.EndpointID = portainer.EndpointID(payload.EndpointID)
|
stack.EndpointID = portainer.EndpointID(payload.EndpointID)
|
||||||
|
@ -126,14 +128,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
stack.Name = payload.Name
|
stack.Name = payload.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
isUnique, err := handler.checkUniqueStackNameInDocker(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isUnique {
|
if !isUnique {
|
||||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on environment '%s'", stack.Name, targetEndpoint.Name)
|
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
|
||||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
||||||
|
@ -142,14 +144,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.Name = oldName
|
stack.Name = oldName
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
||||||
|
@ -175,7 +177,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
|
||||||
|
|
||||||
err := handler.deployComposeStack(config)
|
err := handler.deployComposeStack(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -189,7 +191,7 @@ func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stac
|
||||||
|
|
||||||
err := handler.deploySwarmStack(config)
|
err := handler.deploySwarmStack(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -33,59 +33,61 @@ import (
|
||||||
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stack.Type == portainer.KubernetesStack {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Starting a kubernetes stack is not supported", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||||
}
|
}
|
||||||
if !isUnique {
|
if !isUnique {
|
||||||
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
|
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
|
||||||
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||||
}
|
}
|
||||||
if !access {
|
if !access {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.Status == portainer.StackStatusActive {
|
if stack.Status == portainer.StackStatusActive {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stack is already active", Err: errors.New("Stack is already active")}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
|
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
|
||||||
|
@ -101,13 +103,13 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
|
||||||
|
|
||||||
err = handler.startStack(stack, endpoint)
|
err = handler.startStack(stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start stack", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to start stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.Status = portainer.StackStatusActive
|
stack.Status = portainer.StackStatusActive
|
||||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update stack status", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
|
||||||
|
|
|
@ -46,6 +46,10 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stack.Type == portainer.KubernetesStack {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stopping a kubernetes stack is not supported", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||||
if err == bolterrors.ErrObjectNotFound {
|
if err == bolterrors.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||||
|
@ -58,7 +62,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
@ -71,7 +74,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
if !access {
|
if !access {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if stack.Status == portainer.StackStatusInactive {
|
if stack.Status == portainer.StackStatusInactive {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
|
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/pkg/errors"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
|
@ -98,17 +99,22 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
|
||||||
|
}
|
||||||
|
|
||||||
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
|
||||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
|
||||||
|
@ -127,6 +133,8 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||||
stack.AutoUpdate = payload.AutoUpdate
|
stack.AutoUpdate = payload.AutoUpdate
|
||||||
stack.Env = payload.Env
|
stack.Env = payload.Env
|
||||||
|
stack.UpdatedBy = user.Username
|
||||||
|
stack.UpdateDate = time.Now().Unix()
|
||||||
|
|
||||||
stack.GitConfig.Authentication = nil
|
stack.GitConfig.Authentication = nil
|
||||||
if payload.RepositoryAuthentication {
|
if payload.RepositoryAuthentication {
|
||||||
|
|
|
@ -2,10 +2,8 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -216,14 +214,14 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
|
||||||
if stack.Namespace == "" {
|
if stack.Namespace == "" {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
|
||||||
}
|
}
|
||||||
content, err := ioutil.ReadFile(filepath.Join(stack.ProjectPath, stack.GitConfig.ConfigFilePath))
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to read deployment.yml manifest file", Err: errors.Wrap(err, "failed to read manifest file")}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
|
||||||
}
|
}
|
||||||
_, err = handler.deployKubernetesStack(r, endpoint, string(content), stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
|
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: int(stack.ID),
|
StackID: int(stack.ID),
|
||||||
Name: stack.Name,
|
StackName: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: tokenData.Username,
|
||||||
Kind: "git",
|
Kind: "git",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,7 +2,10 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -10,7 +13,9 @@ 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/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,6 +28,7 @@ type kubernetesGitStackUpdatePayload struct {
|
||||||
RepositoryAuthentication bool
|
RepositoryAuthentication bool
|
||||||
RepositoryUsername string
|
RepositoryUsername string
|
||||||
RepositoryPassword string
|
RepositoryPassword string
|
||||||
|
AutoUpdate *portainer.StackAutoUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -36,12 +42,20 @@ func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error
|
||||||
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
if govalidator.IsNull(payload.RepositoryReferenceName) {
|
||||||
payload.RepositoryReferenceName = defaultGitReferenceName
|
payload.RepositoryReferenceName = defaultGitReferenceName
|
||||||
}
|
}
|
||||||
|
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||||
|
|
||||||
if stack.GitConfig != nil {
|
if stack.GitConfig != nil {
|
||||||
|
//stop the autoupdate job if there is any
|
||||||
|
if stack.AutoUpdate != nil {
|
||||||
|
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||||
|
}
|
||||||
|
|
||||||
var payload kubernetesGitStackUpdatePayload
|
var payload kubernetesGitStackUpdatePayload
|
||||||
|
|
||||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||||
|
@ -49,6 +63,8 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||||
|
stack.AutoUpdate = payload.AutoUpdate
|
||||||
|
|
||||||
if payload.RepositoryAuthentication {
|
if payload.RepositoryAuthentication {
|
||||||
password := payload.RepositoryPassword
|
password := payload.RepositoryPassword
|
||||||
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||||
|
@ -61,6 +77,15 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
} else {
|
} else {
|
||||||
stack.GitConfig.Authentication = nil
|
stack.GitConfig.Authentication = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
|
||||||
|
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
stack.AutoUpdate.JobID = jobID
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,9 +96,25 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFileDir, _ := ioutil.TempDir("", "kub_file_content")
|
||||||
|
defer os.RemoveAll(tempFileDir)
|
||||||
|
|
||||||
|
if err := filesystem.WriteToFile(path.Join(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to persist deployment file in a temp directory", Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
//use temp dir as the stack project path for deployment
|
||||||
|
//so if the deployment failed, the original file won't be over-written
|
||||||
|
stack.ProjectPath = tempFileDir
|
||||||
|
|
||||||
|
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: int(stack.ID),
|
StackID: int(stack.ID),
|
||||||
Name: stack.Name,
|
StackName: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
Kind: "content",
|
Kind: "content",
|
||||||
})
|
})
|
||||||
|
@ -83,7 +124,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fileType := "Manifest"
|
fileType := "Manifest"
|
||||||
if stack.IsComposeFormat {
|
if stack.IsComposeFormat {
|
||||||
|
@ -92,6 +133,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
|
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
|
||||||
}
|
}
|
||||||
|
stack.ProjectPath = projectPath
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
|
|
||||||
|
@ -31,7 +31,10 @@ func (handler *Handler) webhookInvoke(w http.ResponseWriter, r *http.Request) *h
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = stacks.RedeployWhenChanged(stack.ID, handler.StackDeployer, handler.DataStore, handler.GitService); err != nil {
|
if err = stacks.RedeployWhenChanged(stack.ID, handler.StackDeployer, handler.DataStore, handler.GitService); err != nil {
|
||||||
log.Printf("[ERROR] %s\n", err)
|
if _, ok := err.(*stacks.StackAuthorMissingErr); ok {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "Autoupdate for the stack isn't available", Err: err}
|
||||||
|
}
|
||||||
|
logrus.WithError(err).Error("failed to update the stack")
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to update the stack", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to update the stack", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ type ProxyServer struct {
|
||||||
// NewAgentProxy creates a new instance of ProxyServer that wrap http requests with agent headers
|
// NewAgentProxy creates a new instance of ProxyServer that wrap http requests with agent headers
|
||||||
func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||||
urlString := endpoint.URL
|
urlString := endpoint.URL
|
||||||
|
|
||||||
if endpointutils.IsEdgeEndpoint((endpoint)) {
|
if endpointutils.IsEdgeEndpoint((endpoint)) {
|
||||||
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
|
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -11,7 +11,7 @@ func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
|
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint)
|
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
|
||||||
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
|
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
|
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
|
||||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
||||||
|
@ -25,6 +25,7 @@ func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
|
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEdgeEndpoint returns true if this is an Edge endpoint
|
||||||
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
|
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,13 @@ package stackutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResourceControlID returns the stack resource control id
|
// ResourceControlID returns the stack resource control id
|
||||||
|
@ -20,3 +24,39 @@ func GetStackFilePaths(stack *portainer.Stack) []string {
|
||||||
}
|
}
|
||||||
return filePaths
|
return filePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTempK8SDeploymentFiles reads manifest files from original stack project path
|
||||||
|
// then add app labels into the file contents and create temp files for deployment
|
||||||
|
// return temp file paths and temp dir
|
||||||
|
func CreateTempK8SDeploymentFiles(stack *portainer.Stack, kubeDeployer portainer.KubernetesDeployer, appLabels k.KubeAppLabels) ([]string, string, error) {
|
||||||
|
fileNames := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
|
||||||
|
var manifestFilePaths []string
|
||||||
|
tmpDir, err := ioutil.TempDir("", "kub_deployment")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "failed to create temp kub deployment directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fileName := range fileNames {
|
||||||
|
manifestFilePath := path.Join(tmpDir, fileName)
|
||||||
|
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "failed to read manifest file")
|
||||||
|
}
|
||||||
|
if stack.IsComposeFormat {
|
||||||
|
manifestContent, err = kubeDeployer.ConvertCompose(manifestContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manifestContent, err = k.AddAppLabels(manifestContent, appLabels.ToMap())
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "failed to add application labels")
|
||||||
|
}
|
||||||
|
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", errors.Wrap(err, "failed to create temp manifest file")
|
||||||
|
}
|
||||||
|
manifestFilePaths = append(manifestFilePaths, manifestFilePath)
|
||||||
|
}
|
||||||
|
return manifestFilePaths, tmpDir, nil
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
labelPortainerAppStack = "io.portainer.kubernetes.application.stack"
|
||||||
labelPortainerAppStackID = "io.portainer.kubernetes.application.stackid"
|
labelPortainerAppStackID = "io.portainer.kubernetes.application.stackid"
|
||||||
labelPortainerAppName = "io.portainer.kubernetes.application.name"
|
labelPortainerAppName = "io.portainer.kubernetes.application.name"
|
||||||
labelPortainerAppOwner = "io.portainer.kubernetes.application.owner"
|
labelPortainerAppOwner = "io.portainer.kubernetes.application.owner"
|
||||||
|
@ -21,7 +22,7 @@ const (
|
||||||
// KubeAppLabels are labels applied to all resources deployed in a kubernetes stack
|
// KubeAppLabels are labels applied to all resources deployed in a kubernetes stack
|
||||||
type KubeAppLabels struct {
|
type KubeAppLabels struct {
|
||||||
StackID int
|
StackID int
|
||||||
Name string
|
StackName string
|
||||||
Owner string
|
Owner string
|
||||||
Kind string
|
Kind string
|
||||||
}
|
}
|
||||||
|
@ -30,7 +31,8 @@ type KubeAppLabels struct {
|
||||||
func (kal *KubeAppLabels) ToMap() map[string]string {
|
func (kal *KubeAppLabels) ToMap() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
labelPortainerAppStackID: strconv.Itoa(kal.StackID),
|
labelPortainerAppStackID: strconv.Itoa(kal.StackID),
|
||||||
labelPortainerAppName: kal.Name,
|
labelPortainerAppStack: kal.StackName,
|
||||||
|
labelPortainerAppName: kal.StackName,
|
||||||
labelPortainerAppOwner: kal.Owner,
|
labelPortainerAppOwner: kal.Owner,
|
||||||
labelPortainerAppKind: kal.Kind,
|
labelPortainerAppKind: kal.Kind,
|
||||||
}
|
}
|
||||||
|
@ -167,13 +169,6 @@ func addLabels(obj map[string]interface{}, appLabels map[string]string) {
|
||||||
labels[k] = v
|
labels[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to metadata name if name label not explicitly provided
|
|
||||||
if name, ok := labels[labelPortainerAppName]; !ok || name == "" {
|
|
||||||
if n, ok := metadata["name"]; ok {
|
|
||||||
labels[labelPortainerAppName] = n.(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata["labels"] = labels
|
metadata["labels"] = labels
|
||||||
obj["metadata"] = metadata
|
obj["metadata"] = metadata
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: busybox
|
name: busybox
|
||||||
spec:
|
spec:
|
||||||
|
@ -86,6 +87,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: busybox
|
name: busybox
|
||||||
spec:
|
spec:
|
||||||
|
@ -174,6 +176,7 @@ items:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: web
|
name: web
|
||||||
spec:
|
spec:
|
||||||
|
@ -194,6 +197,7 @@ items:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: redis
|
name: redis
|
||||||
spec:
|
spec:
|
||||||
|
@ -216,6 +220,7 @@ items:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: web
|
name: web
|
||||||
spec:
|
spec:
|
||||||
|
@ -297,6 +302,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: busybox
|
name: busybox
|
||||||
spec:
|
spec:
|
||||||
|
@ -322,6 +328,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: web
|
name: web
|
||||||
spec:
|
spec:
|
||||||
|
@ -340,6 +347,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: busybox
|
name: busybox
|
||||||
spec:
|
spec:
|
||||||
|
@ -388,6 +396,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
name: web
|
name: web
|
||||||
spec:
|
spec:
|
||||||
|
@ -403,7 +412,7 @@ spec:
|
||||||
|
|
||||||
labels := KubeAppLabels{
|
labels := KubeAppLabels{
|
||||||
StackID: 123,
|
StackID: 123,
|
||||||
Name: "best-name",
|
StackName: "best-name",
|
||||||
Owner: "best-owner",
|
Owner: "best-owner",
|
||||||
Kind: "git",
|
Kind: "git",
|
||||||
}
|
}
|
||||||
|
@ -417,81 +426,6 @@ spec:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_AddAppLabels_PickingName_WhenLabelNameIsEmpty(t *testing.T) {
|
|
||||||
labels := KubeAppLabels{
|
|
||||||
StackID: 123,
|
|
||||||
Owner: "best-owner",
|
|
||||||
Kind: "git",
|
|
||||||
}
|
|
||||||
|
|
||||||
input := `apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: web
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: "5000"
|
|
||||||
port: 5000
|
|
||||||
targetPort: 5000
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
io.portainer.kubernetes.application.kind: git
|
|
||||||
io.portainer.kubernetes.application.name: web
|
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
|
||||||
name: web
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: "5000"
|
|
||||||
port: 5000
|
|
||||||
targetPort: 5000
|
|
||||||
`
|
|
||||||
|
|
||||||
result, err := AddAppLabels([]byte(input), labels.ToMap())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, string(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_AddAppLabels_PickingName_WhenLabelAndMetadataNameAreEmpty(t *testing.T) {
|
|
||||||
labels := KubeAppLabels{
|
|
||||||
StackID: 123,
|
|
||||||
Owner: "best-owner",
|
|
||||||
Kind: "git",
|
|
||||||
}
|
|
||||||
|
|
||||||
input := `apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: "5000"
|
|
||||||
port: 5000
|
|
||||||
targetPort: 5000
|
|
||||||
`
|
|
||||||
|
|
||||||
expected := `apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
io.portainer.kubernetes.application.kind: git
|
|
||||||
io.portainer.kubernetes.application.name: ""
|
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: "5000"
|
|
||||||
port: 5000
|
|
||||||
targetPort: 5000
|
|
||||||
`
|
|
||||||
|
|
||||||
result, err := AddAppLabels([]byte(input), labels.ToMap())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, string(result))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_AddAppLabels_HelmApp(t *testing.T) {
|
func Test_AddAppLabels_HelmApp(t *testing.T) {
|
||||||
labels := GetHelmAppLabels("best-name", "best-owner")
|
labels := GetHelmAppLabels("best-name", "best-owner")
|
||||||
|
|
||||||
|
@ -659,7 +593,7 @@ spec:
|
||||||
func Test_DocumentSeperator(t *testing.T) {
|
func Test_DocumentSeperator(t *testing.T) {
|
||||||
labels := KubeAppLabels{
|
labels := KubeAppLabels{
|
||||||
StackID: 123,
|
StackID: 123,
|
||||||
Name: "best-name",
|
StackName: "best-name",
|
||||||
Owner: "best-owner",
|
Owner: "best-owner",
|
||||||
Kind: "git",
|
Kind: "git",
|
||||||
}
|
}
|
||||||
|
@ -684,6 +618,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
@ -694,6 +629,7 @@ metadata:
|
||||||
io.portainer.kubernetes.application.kind: git
|
io.portainer.kubernetes.application.kind: git
|
||||||
io.portainer.kubernetes.application.name: best-name
|
io.portainer.kubernetes.application.name: best-name
|
||||||
io.portainer.kubernetes.application.owner: best-owner
|
io.portainer.kubernetes.application.owner: best-owner
|
||||||
|
io.portainer.kubernetes.application.stack: best-name
|
||||||
io.portainer.kubernetes.application.stackid: "123"
|
io.portainer.kubernetes.application.stackid: "123"
|
||||||
`
|
`
|
||||||
result, err := AddAppLabels([]byte(input), labels.ToMap())
|
result, err := AddAppLabels([]byte(input), labels.ToMap())
|
||||||
|
|
|
@ -3,7 +3,6 @@ package portainer
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
@ -1281,7 +1280,8 @@ type (
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
|
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
|
||||||
KubernetesDeployer interface {
|
KubernetesDeployer interface {
|
||||||
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
|
Deploy(userID UserID, endpoint *Endpoint, manifestFiles []string, namespace string) (string, error)
|
||||||
|
Remove(userID UserID, endpoint *Endpoint, manifestFiles []string, namespace string) (string, error)
|
||||||
ConvertCompose(data []byte) ([]byte, error)
|
ConvertCompose(data []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,12 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Scheduler struct {
|
type Scheduler struct {
|
||||||
crontab *cron.Cron
|
crontab *cron.Cron
|
||||||
shutdownCtx context.Context
|
activeJobs map[cron.EntryID]context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScheduler(ctx context.Context) *Scheduler {
|
func NewScheduler(ctx context.Context) *Scheduler {
|
||||||
|
@ -21,6 +22,7 @@ func NewScheduler(ctx context.Context) *Scheduler {
|
||||||
|
|
||||||
s := &Scheduler{
|
s := &Scheduler{
|
||||||
crontab: crontab,
|
crontab: crontab,
|
||||||
|
activeJobs: make(map[cron.EntryID]context.CancelFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
|
@ -43,8 +45,10 @@ func (s *Scheduler) Shutdown() error {
|
||||||
ctx := s.crontab.Stop()
|
ctx := s.crontab.Stop()
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
for _, j := range s.crontab.Entries() {
|
for _, job := range s.crontab.Entries() {
|
||||||
s.crontab.Remove(j.ID)
|
if cancel, ok := s.activeJobs[job.ID]; ok {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ctx.Err()
|
err := ctx.Err()
|
||||||
|
@ -60,14 +64,36 @@ func (s *Scheduler) StopJob(jobID string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed convert jobID %q to int", jobID)
|
return errors.Wrapf(err, "failed convert jobID %q to int", jobID)
|
||||||
}
|
}
|
||||||
s.crontab.Remove(cron.EntryID(id))
|
entryID := cron.EntryID(id)
|
||||||
|
if cancel, ok := s.activeJobs[entryID]; ok {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartJobEvery schedules a new periodic job with a given duration.
|
// StartJobEvery schedules a new periodic job with a given duration.
|
||||||
// Returns job id that could be used to stop the given job
|
// Returns job id that could be used to stop the given job.
|
||||||
func (s *Scheduler) StartJobEvery(duration time.Duration, job func()) string {
|
// When job run returns an error, that job won't be run again.
|
||||||
entryId := s.crontab.Schedule(cron.Every(duration), cron.FuncJob(job))
|
func (s *Scheduler) StartJobEvery(duration time.Duration, job func() error) string {
|
||||||
return strconv.Itoa(int(entryId))
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
j := cron.FuncJob(func() {
|
||||||
|
if err := job(); err != nil {
|
||||||
|
logrus.Debug("job returned an error")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
entryID := s.crontab.Schedule(cron.Every(duration), j)
|
||||||
|
|
||||||
|
s.activeJobs[entryID] = cancel
|
||||||
|
|
||||||
|
go func(entryID cron.EntryID) {
|
||||||
|
<-ctx.Done()
|
||||||
|
logrus.Debug("job cancelled, stopping")
|
||||||
|
s.crontab.Remove(entryID)
|
||||||
|
}(entryID)
|
||||||
|
|
||||||
|
return strconv.Itoa(int(entryID))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,49 +9,92 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_CanStartAndTerminate(t *testing.T) {
|
var jobInterval = time.Second
|
||||||
s := NewScheduler(context.Background())
|
|
||||||
s.StartJobEvery(1*time.Minute, func() { fmt.Println("boop") })
|
|
||||||
|
|
||||||
err := s.Shutdown()
|
func Test_ScheduledJobRuns(t *testing.T) {
|
||||||
assert.NoError(t, err, "Shutdown should return no errors")
|
|
||||||
assert.Empty(t, s.crontab.Entries(), "all jobs should have been removed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_CanTerminateByCancellingContext(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
s := NewScheduler(ctx)
|
|
||||||
s.StartJobEvery(1*time.Minute, func() { fmt.Println("boop") })
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
if len(s.crontab.Entries()) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
t.Fatal("all jobs are expected to be cleaned by now; it might be a timing issue, otherwise implementation defect")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_StartAndStopJob(t *testing.T) {
|
|
||||||
s := NewScheduler(context.Background())
|
s := NewScheduler(context.Background())
|
||||||
defer s.Shutdown()
|
defer s.Shutdown()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
|
||||||
|
|
||||||
var jobOne string
|
|
||||||
var workDone bool
|
var workDone bool
|
||||||
jobOne = s.StartJobEvery(time.Second, func() {
|
s.StartJobEvery(jobInterval, func() error {
|
||||||
assert.Equal(t, 1, len(s.crontab.Entries()), "scheduler should have one active job")
|
|
||||||
workDone = true
|
workDone = true
|
||||||
|
|
||||||
s.StopJob(jobOne)
|
|
||||||
cancel()
|
cancel()
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
assert.True(t, workDone, "value should been set in the job")
|
assert.True(t, workDone, "value should been set in the job")
|
||||||
assert.Equal(t, 0, len(s.crontab.Entries()), "scheduler should have no active jobs")
|
}
|
||||||
|
|
||||||
|
func Test_JobCanBeStopped(t *testing.T) {
|
||||||
|
s := NewScheduler(context.Background())
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
|
||||||
|
|
||||||
|
var workDone bool
|
||||||
|
jobID := s.StartJobEvery(jobInterval, func() error {
|
||||||
|
workDone = true
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
s.StopJob(jobID)
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
assert.False(t, workDone, "job shouldn't had a chance to run")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_JobShouldStop_UponError(t *testing.T) {
|
||||||
|
s := NewScheduler(context.Background())
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
var acc int
|
||||||
|
s.StartJobEvery(jobInterval, func() error {
|
||||||
|
acc++
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
})
|
||||||
|
|
||||||
|
<-time.After(3 * jobInterval)
|
||||||
|
assert.Equal(t, 1, acc, "job stop after the first run because it returns an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CanTerminateAllJobs_ByShuttingDownScheduler(t *testing.T) {
|
||||||
|
s := NewScheduler(context.Background())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
|
||||||
|
|
||||||
|
var workDone bool
|
||||||
|
s.StartJobEvery(jobInterval, func() error {
|
||||||
|
workDone = true
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Shutdown()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
assert.False(t, workDone, "job shouldn't had a chance to run")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CanTerminateAllJobs_ByCancellingParentContext(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
|
||||||
|
s := NewScheduler(ctx)
|
||||||
|
|
||||||
|
var workDone bool
|
||||||
|
s.StartJobEvery(jobInterval, func() error {
|
||||||
|
workDone = true
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
assert.False(t, workDone, "job shouldn't had a chance to run")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, datastore portainer.DataStore, gitService portainer.GitService) error {
|
func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, datastore portainer.DataStore, gitService portainer.GitService) error {
|
||||||
|
logger := log.WithFields(log.Fields{"stackID": stackID})
|
||||||
|
logger.Debug("redeploying stack")
|
||||||
|
|
||||||
stack, err := datastore.Stack().Stack(stackID)
|
stack, err := datastore.Stack().Stack(stackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithMessagef(err, "failed to get the stack %v", stackID)
|
return errors.WithMessagef(err, "failed to get the stack %v", stackID)
|
||||||
|
@ -19,6 +33,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
author := stack.UpdatedBy
|
||||||
|
if author == "" {
|
||||||
|
author = stack.CreatedBy
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := datastore.User().UserByUsername(author)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithFields(log.Fields{"author": author, "stack": stack.Name, "endpointID": stack.EndpointID}).Warn("cannot autoupdate a stack, stack author user is missing")
|
||||||
|
return &StackAuthorMissingErr{int(stack.ID), author}
|
||||||
|
}
|
||||||
|
|
||||||
username, password := "", ""
|
username, password := "", ""
|
||||||
if stack.GitConfig.Authentication != nil {
|
if stack.GitConfig.Authentication != nil {
|
||||||
username, password = stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password
|
username, password = stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password
|
||||||
|
@ -54,12 +79,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
|
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
author := stack.UpdatedBy
|
registries, err := getUserRegistries(datastore, user, endpoint.ID)
|
||||||
if author == "" {
|
|
||||||
author = stack.CreatedBy
|
|
||||||
}
|
|
||||||
|
|
||||||
registries, err := getUserRegistries(datastore, author, endpoint.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -75,6 +95,12 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||||
}
|
}
|
||||||
|
case portainer.KubernetesStack:
|
||||||
|
logger.Debugf("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:
|
default:
|
||||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
||||||
}
|
}
|
||||||
|
@ -88,24 +114,19 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserRegistries(datastore portainer.DataStore, authorUsername string, endpointID portainer.EndpointID) ([]portainer.Registry, error) {
|
func getUserRegistries(datastore portainer.DataStore, user *portainer.User, endpointID portainer.EndpointID) ([]portainer.Registry, error) {
|
||||||
registries, err := datastore.Registry().Registries()
|
registries, err := datastore.Registry().Registries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, "unable to retrieve registries from the database")
|
return nil, errors.WithMessage(err, "unable to retrieve registries from the database")
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := datastore.User().UserByUsername(authorUsername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessagef(err, "failed to fetch a stack's author [%s]", authorUsername)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Role == portainer.AdministratorRole {
|
if user.Role == portainer.AdministratorRole {
|
||||||
return registries, nil
|
return registries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userMemberships, err := datastore.TeamMembership().TeamMembershipsByUserID(user.ID)
|
userMemberships, err := datastore.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessagef(err, "failed to fetch memberships of the stack author [%s]", authorUsername)
|
return nil, errors.WithMessagef(err, "failed to fetch memberships of the stack author [%s]", user.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredRegistries := make([]portainer.Registry, 0, len(registries))
|
filteredRegistries := make([]portainer.Registry, 0, len(registries))
|
||||||
|
|
|
@ -35,6 +35,10 @@ func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *port
|
||||||
return nil
|
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) {
|
func Test_redeployWhenChanged_FailsWhenCannotFindStack(t *testing.T) {
|
||||||
store, teardown := bolt.MustNewTestStore(true)
|
store, teardown := bolt.MustNewTestStore(true)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
@ -48,7 +52,11 @@ func Test_redeployWhenChanged_DoesNothingWhenNotAGitBasedStack(t *testing.T) {
|
||||||
store, teardown := bolt.MustNewTestStore(true)
|
store, teardown := bolt.MustNewTestStore(true)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
err := store.Stack().CreateStack(&portainer.Stack{ID: 1})
|
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||||
|
err := store.User().CreateUser(admin)
|
||||||
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
|
err = store.Stack().CreateStack(&portainer.Stack{ID: 1, CreatedBy: "admin"})
|
||||||
assert.NoError(t, err, "failed to create a test stack")
|
assert.NoError(t, err, "failed to create a test stack")
|
||||||
|
|
||||||
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""})
|
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""})
|
||||||
|
@ -61,8 +69,13 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
||||||
|
|
||||||
tmpDir, _ := ioutil.TempDir("", "stack")
|
tmpDir, _ := ioutil.TempDir("", "stack")
|
||||||
|
|
||||||
err := store.Stack().CreateStack(&portainer.Stack{
|
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||||
|
err := store.User().CreateUser(admin)
|
||||||
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
|
err = store.Stack().CreateStack(&portainer.Stack{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
CreatedBy: "admin",
|
||||||
ProjectPath: tmpDir,
|
ProjectPath: tmpDir,
|
||||||
GitConfig: &gittypes.RepoConfig{
|
GitConfig: &gittypes.RepoConfig{
|
||||||
URL: "url",
|
URL: "url",
|
||||||
|
@ -80,8 +93,13 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
||||||
store, teardown := bolt.MustNewTestStore(true)
|
store, teardown := bolt.MustNewTestStore(true)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
|
|
||||||
err := store.Stack().CreateStack(&portainer.Stack{
|
admin := &portainer.User{ID: 1, Username: "admin"}
|
||||||
|
err := store.User().CreateUser(admin)
|
||||||
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
|
err = store.Stack().CreateStack(&portainer.Stack{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
CreatedBy: "admin",
|
||||||
GitConfig: &gittypes.RepoConfig{
|
GitConfig: &gittypes.RepoConfig{
|
||||||
URL: "url",
|
URL: "url",
|
||||||
ReferenceName: "ref",
|
ReferenceName: "ref",
|
||||||
|
@ -136,12 +154,12 @@ func Test_redeployWhenChanged(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can NOT deploy kube stack", func(t *testing.T) {
|
t.Run("can deploy kube app", func(t *testing.T) {
|
||||||
stack.Type = portainer.KubernetesStack
|
stack.Type = portainer.KubernetesStack
|
||||||
store.Stack().UpdateStack(stack.ID, &stack)
|
store.Stack().UpdateStack(stack.ID, &stack)
|
||||||
|
|
||||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||||
assert.EqualError(t, err, "cannot update stack, type 3 is unsupported")
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,12 +169,12 @@ func Test_getUserRegistries(t *testing.T) {
|
||||||
|
|
||||||
endpointID := 123
|
endpointID := 123
|
||||||
|
|
||||||
admin := portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
admin := &portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||||
err := store.User().CreateUser(&admin)
|
err := store.User().CreateUser(admin)
|
||||||
assert.NoError(t, err, "error creating an admin")
|
assert.NoError(t, err, "error creating an admin")
|
||||||
|
|
||||||
user := portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
|
user := &portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
|
||||||
err = store.User().CreateUser(&user)
|
err = store.User().CreateUser(user)
|
||||||
assert.NoError(t, err, "error creating a user")
|
assert.NoError(t, err, "error creating a user")
|
||||||
|
|
||||||
team := portainer.Team{ID: 1, Name: "team"}
|
team := portainer.Team{ID: 1, Name: "team"}
|
||||||
|
@ -208,13 +226,13 @@ func Test_getUserRegistries(t *testing.T) {
|
||||||
assert.NoError(t, err, "couldn't create a registry")
|
assert.NoError(t, err, "couldn't create a registry")
|
||||||
|
|
||||||
t.Run("admin should has access to all registries", func(t *testing.T) {
|
t.Run("admin should has access to all registries", func(t *testing.T) {
|
||||||
registries, err := getUserRegistries(store, admin.Username, portainer.EndpointID(endpointID))
|
registries, err := getUserRegistries(store, admin, portainer.EndpointID(endpointID))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam, registryRestricted}, registries)
|
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) {
|
t.Run("regular user has access to registries allowed to him and/or his team", func(t *testing.T) {
|
||||||
registries, err := getUserRegistries(store, user.Username, portainer.EndpointID(endpointID))
|
registries, err := getUserRegistries(store, user, portainer.EndpointID(endpointID))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam}, registries)
|
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam}, registries)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,27 +2,36 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/stackutils"
|
||||||
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StackDeployer interface {
|
type StackDeployer interface {
|
||||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
|
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
|
||||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
||||||
|
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type stackDeployer struct {
|
type stackDeployer struct {
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
swarmStackManager portainer.SwarmStackManager
|
swarmStackManager portainer.SwarmStackManager
|
||||||
composeStackManager portainer.ComposeStackManager
|
composeStackManager portainer.ComposeStackManager
|
||||||
|
kubernetesDeployer portainer.KubernetesDeployer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStackManager portainer.ComposeStackManager) *stackDeployer {
|
// 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{
|
return &stackDeployer{
|
||||||
lock: &sync.Mutex{},
|
lock: &sync.Mutex{},
|
||||||
swarmStackManager: swarmStackManager,
|
swarmStackManager: swarmStackManager,
|
||||||
composeStackManager: composeStackManager,
|
composeStackManager: composeStackManager,
|
||||||
|
kubernetesDeployer: kubernetesDeployer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,3 +54,33 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
|
||||||
|
|
||||||
return d.composeStackManager.Up(context.TODO(), stack, endpoint)
|
return d.composeStackManager.Up(context.TODO(), stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, d.kubernetesDeployer, appLabels)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create temp kub deployment files")
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
_, err = d.kubernetesDeployer.Deploy(user.ID, endpoint, manifestFilePaths, stack.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to deploy kubernetes application")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -19,10 +18,9 @@ func StartStackSchedules(scheduler *scheduler.Scheduler, stackdeployer StackDepl
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Unable to parse auto update interval")
|
return errors.Wrap(err, "Unable to parse auto update interval")
|
||||||
}
|
}
|
||||||
jobID := scheduler.StartJobEvery(d, func() {
|
stackID := stack.ID // to be captured by the scheduled function
|
||||||
if err := RedeployWhenChanged(stack.ID, stackdeployer, datastore, gitService); err != nil {
|
jobID := scheduler.StartJobEvery(d, func() error {
|
||||||
log.Printf("[ERROR] %s\n", err)
|
return RedeployWhenChanged(stackID, stackdeployer, datastore, gitService)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
stack.AutoUpdate.JobID = jobID
|
stack.AutoUpdate.JobID = jobID
|
||||||
|
|
|
@ -56,7 +56,7 @@ angular.module('portainer').config([
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
progressBar: true,
|
progressBar: true,
|
||||||
tapToDismiss: false,
|
tapToDismiss: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
Terminal.applyAddon(fit);
|
Terminal.applyAddon(fit);
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="$ctrl.noEndpoints">
|
<div class="form-group" ng-if="$ctrl.noEndpoints">
|
||||||
<div class="col-sm-12 small text-muted"> No Edge environments are available. Head over to the <a ui-sref="portainer.endpoints">Environments view</a> to add environments. </div>
|
<div class="col-sm-12 small text-muted">
|
||||||
|
No Edge environments are available. Head over to the <a ui-sref="portainer.endpoints">Environments view</a> to add environments.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !StaticGroup -->
|
<!-- !StaticGroup -->
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
<span class="interactive" tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Kubeconfig file will {{ $ctrl.state.expiryDays }}">
|
<span
|
||||||
|
class="interactive"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
uib-tooltip="Kubeconfig file will {{ $ctrl.state.expiryDays }}"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
ng-if="$ctrl.state.isHTTPS"
|
ng-if="$ctrl.state.isHTTPS"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -15,3 +15,8 @@ export const KubernetesDeployRequestMethods = Object.freeze({
|
||||||
STRING: 'string',
|
STRING: 'string',
|
||||||
URL: 'url',
|
URL: 'url',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const RepositoryMechanismTypes = Object.freeze({
|
||||||
|
WEBHOOK: 'Webhook',
|
||||||
|
INTERVAL: 'Interval',
|
||||||
|
});
|
||||||
|
|
|
@ -102,7 +102,8 @@ class KubernetesServiceService {
|
||||||
const namespace = service.Namespace;
|
const namespace = service.Namespace;
|
||||||
await this.KubernetesServices(namespace).delete(params).$promise;
|
await this.KubernetesServices(namespace).delete(params).$promise;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new PortainerError('Unable to remove service', err);
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('unable to remove service', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models
|
||||||
|
|
||||||
class KubernetesApplicationsController {
|
class KubernetesApplicationsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, KubernetesApplicationService, HelmService, KubernetesConfigurationService, Authentication, ModalService, LocalStorage) {
|
constructor($async, $state, Notifications, KubernetesApplicationService, HelmService, KubernetesConfigurationService, Authentication, ModalService, LocalStorage, StackService) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
@ -17,6 +17,7 @@ class KubernetesApplicationsController {
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
|
this.StackService = StackService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.getApplications = this.getApplications.bind(this);
|
this.getApplications = this.getApplications.bind(this);
|
||||||
|
@ -36,8 +37,18 @@ class KubernetesApplicationsController {
|
||||||
let actionCount = selectedItems.length;
|
let actionCount = selectedItems.length;
|
||||||
for (const stack of selectedItems) {
|
for (const stack of selectedItems) {
|
||||||
try {
|
try {
|
||||||
|
const isAppFormCreated = stack.Applications.some((x) => !x.ApplicationKind);
|
||||||
|
|
||||||
|
if (isAppFormCreated) {
|
||||||
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
} else {
|
||||||
|
const application = stack.Applications.find((x) => x.StackId !== null);
|
||||||
|
if (application && application.StackId) {
|
||||||
|
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.Notifications.success('Stack successfully removed', stack.Name);
|
this.Notifications.success('Stack successfully removed', stack.Name);
|
||||||
_.remove(this.state.stacks, { Name: stack.Name });
|
_.remove(this.state.stacks, { Name: stack.Name });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -70,6 +81,14 @@ class KubernetesApplicationsController {
|
||||||
await this.HelmService.uninstall(this.endpoint.Id, application);
|
await this.HelmService.uninstall(this.endpoint.Id, application);
|
||||||
} else {
|
} else {
|
||||||
await this.KubernetesApplicationService.delete(application);
|
await this.KubernetesApplicationService.delete(application);
|
||||||
|
// Update applications in stack
|
||||||
|
const stack = this.state.stacks.find((x) => x.Name === application.StackName);
|
||||||
|
const index = stack.Applications.indexOf(application);
|
||||||
|
stack.Applications.splice(index, 1);
|
||||||
|
// remove stack if no app left in the stack
|
||||||
|
if (stack.Applications.length === 0 && application.StackId) {
|
||||||
|
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.Notifications.success('Application successfully removed', application.Name);
|
this.Notifications.success('Application successfully removed', application.Name);
|
||||||
const index = this.state.applications.indexOf(application);
|
const index = this.state.applications.indexOf(application);
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
class-name="text-muted"
|
class-name="text-muted"
|
||||||
url="ctrl.stack.GitConfig.URL"
|
url="ctrl.stack.GitConfig.URL"
|
||||||
config-file-path="ctrl.stack.GitConfig.ConfigFilePath"
|
config-file-path="ctrl.stack.GitConfig.ConfigFilePath"
|
||||||
|
additional-files="ctrl.stack.AdditionalFiles"
|
||||||
|
type="application"
|
||||||
></git-form-info-panel>
|
></git-form-info-panel>
|
||||||
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
||||||
Namespace
|
Namespace
|
||||||
|
@ -56,11 +58,11 @@
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region Git repository -->
|
<!-- #region Git repository -->
|
||||||
<kubernetes-app-git-form
|
<kubernetes-redeploy-app-git-form
|
||||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
|
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
|
||||||
stack="ctrl.stack"
|
stack="ctrl.stack"
|
||||||
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
|
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
|
||||||
></kubernetes-app-git-form>
|
></kubernetes-redeploy-app-git-form>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region web editor -->
|
<!-- #region web editor -->
|
||||||
|
|
|
@ -29,6 +29,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
<div class="col-lg-11 col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="ctrl.formValues.StackName" id="stack_name" placeholder="my-app" auto-focus />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Build method
|
Build method
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,32 +49,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- repository -->
|
<!-- repository -->
|
||||||
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
|
<git-form
|
||||||
<div class="col-sm-12 form-section-title">
|
ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT"
|
||||||
Git repository
|
model="ctrl.formValues"
|
||||||
</div>
|
on-change="(ctrl.onChangeFormValues)"
|
||||||
<git-form-url-field value="ctrl.formValues.RepositoryURL" on-change="(ctrl.onRepoUrlChange)"></git-form-url-field>
|
additional-file="true"
|
||||||
<git-form-ref-field value="ctrl.formValues.RepositoryReferenceName" on-change="(ctrl.onRepoRefChange)"></git-form-ref-field>
|
auto-update="true"
|
||||||
<div class="form-group">
|
show-auth-explanation="true"
|
||||||
<span class="col-sm-12 text-muted small">
|
path-text-title="Manifest path"
|
||||||
Indicate the path to the yaml file from the root of your repository.
|
path-placeholder="deployment.yml"
|
||||||
</span>
|
></git-form>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Manifest path</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="ctrl.formValues.FilePathInRepository"
|
|
||||||
id="stack_manifest_path"
|
|
||||||
placeholder="deployment.yml"
|
|
||||||
data-cy="k8sAppDeploy-gitManifestPath"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<git-form-auth-fieldset model="ctrl.formValues" on-change="(ctrl.onChangeFormValues)"></git-form-auth-fieldset>
|
|
||||||
</div>
|
|
||||||
<!-- !repository -->
|
<!-- !repository -->
|
||||||
|
|
||||||
<custom-template-selector
|
<custom-template-selector
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import stripAnsi from 'strip-ansi';
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import uuidv4 from 'uuid/v4';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy';
|
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { buildOption } from '@/portainer/components/box-selector';
|
import { buildOption } from '@/portainer/components/box-selector';
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, $window, Authentication, CustomTemplateService, ModalService, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
constructor(
|
||||||
|
$async,
|
||||||
|
$state,
|
||||||
|
$window,
|
||||||
|
Authentication,
|
||||||
|
ModalService,
|
||||||
|
Notifications,
|
||||||
|
EndpointProvider,
|
||||||
|
KubernetesResourcePoolService,
|
||||||
|
StackService,
|
||||||
|
WebhookHelper,
|
||||||
|
CustomTemplateService
|
||||||
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.$window = $window;
|
this.$window = $window;
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
this.CustomTemplateService = CustomTemplateService;
|
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.EndpointProvider = EndpointProvider;
|
this.EndpointProvider = EndpointProvider;
|
||||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||||
this.StackService = StackService;
|
this.StackService = StackService;
|
||||||
|
this.WebhookHelper = WebhookHelper;
|
||||||
|
this.CustomTemplateService = CustomTemplateService;
|
||||||
|
|
||||||
this.deployOptions = [
|
this.deployOptions = [
|
||||||
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
||||||
|
@ -41,7 +55,20 @@ class KubernetesDeployController {
|
||||||
templateId: null,
|
templateId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = {};
|
this.formValues = {
|
||||||
|
StackName: '',
|
||||||
|
RepositoryURL: '',
|
||||||
|
RepositoryReferenceName: '',
|
||||||
|
RepositoryAuthentication: true,
|
||||||
|
RepositoryUsername: '',
|
||||||
|
RepositoryPassword: '',
|
||||||
|
AdditionalFiles: [],
|
||||||
|
ComposeFilePathInRepository: 'deployment.yml',
|
||||||
|
RepositoryAutomaticUpdates: true,
|
||||||
|
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||||
|
RepositoryFetchInterval: '5m',
|
||||||
|
RepositoryWebhookURL: this.WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||||
|
};
|
||||||
this.ManifestDeployTypes = KubernetesDeployManifestTypes;
|
this.ManifestDeployTypes = KubernetesDeployManifestTypes;
|
||||||
this.BuildMethods = KubernetesDeployBuildMethods;
|
this.BuildMethods = KubernetesDeployBuildMethods;
|
||||||
this.endpointId = this.EndpointProvider.endpointID();
|
this.endpointId = this.EndpointProvider.endpointID();
|
||||||
|
@ -51,8 +78,6 @@ class KubernetesDeployController {
|
||||||
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
this.onChangeFileContent = this.onChangeFileContent.bind(this);
|
||||||
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.onRepoUrlChange = this.onRepoUrlChange.bind(this);
|
|
||||||
this.onRepoRefChange = this.onRepoRefChange.bind(this);
|
|
||||||
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
|
this.buildAnalyticsProperties = this.buildAnalyticsProperties.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +86,7 @@ class KubernetesDeployController {
|
||||||
type: buildLabel(this.state.BuildMethod),
|
type: buildLabel(this.state.BuildMethod),
|
||||||
format: formatLabel(this.state.DeployType),
|
format: formatLabel(this.state.DeployType),
|
||||||
role: roleLabel(this.Authentication.isAdmin()),
|
role: roleLabel(this.Authentication.isAdmin()),
|
||||||
|
'automatic-updates': automaticUpdatesLabel(this.formValues.RepositoryAutomaticUpdates, this.formValues.RepositoryMechanism),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.BuildMethod === KubernetesDeployBuildMethods.GIT) {
|
if (this.state.BuildMethod === KubernetesDeployBuildMethods.GIT) {
|
||||||
|
@ -69,6 +95,17 @@ class KubernetesDeployController {
|
||||||
|
|
||||||
return { metadata };
|
return { metadata };
|
||||||
|
|
||||||
|
function automaticUpdatesLabel(repositoryAutomaticUpdates, repositoryMechanism) {
|
||||||
|
switch (repositoryAutomaticUpdates && repositoryMechanism) {
|
||||||
|
case RepositoryMechanismTypes.INTERVAL:
|
||||||
|
return 'polling';
|
||||||
|
case RepositoryMechanismTypes.WEBHOOK:
|
||||||
|
return 'webhook';
|
||||||
|
default:
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function roleLabel(isAdmin) {
|
function roleLabel(isAdmin) {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return 'admin';
|
return 'admin';
|
||||||
|
@ -99,17 +136,14 @@ class KubernetesDeployController {
|
||||||
disableDeploy() {
|
disableDeploy() {
|
||||||
const isGitFormInvalid =
|
const isGitFormInvalid =
|
||||||
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
this.state.BuildMethod === KubernetesDeployBuildMethods.GIT &&
|
||||||
(!this.formValues.RepositoryURL ||
|
(!this.formValues.RepositoryURL || !this.formValues.FilePathInRepository || (this.formValues.RepositoryAuthentication && !this.formValues.RepositoryPassword)) &&
|
||||||
!this.formValues.FilePathInRepository ||
|
|
||||||
(this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) &&
|
|
||||||
_.isEmpty(this.formValues.Namespace);
|
_.isEmpty(this.formValues.Namespace);
|
||||||
const isWebEditorInvalid =
|
const isWebEditorInvalid =
|
||||||
this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace);
|
this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace);
|
||||||
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
|
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
|
||||||
|
|
||||||
const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
|
const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
|
||||||
|
return !this.formValues.StackName || isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress || isNamespaceInvalid;
|
||||||
return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress || isNamespaceInvalid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFormValues(values) {
|
onChangeFormValues(values) {
|
||||||
|
@ -119,14 +153,6 @@ class KubernetesDeployController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onRepoUrlChange(value) {
|
|
||||||
this.onChangeFormValues({ RepositoryURL: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onRepoRefChange(value) {
|
|
||||||
this.onChangeFormValues({ RepositoryReferenceName: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeTemplateId(templateId) {
|
onChangeTemplateId(templateId) {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
if (this.state.templateId === templateId) {
|
if (this.state.templateId === templateId) {
|
||||||
|
@ -184,6 +210,7 @@ class KubernetesDeployController {
|
||||||
const payload = {
|
const payload = {
|
||||||
ComposeFormat: composeFormat,
|
ComposeFormat: composeFormat,
|
||||||
Namespace: this.formValues.Namespace,
|
Namespace: this.formValues.Namespace,
|
||||||
|
StackName: this.formValues.StackName,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (method === KubernetesDeployRequestMethods.REPOSITORY) {
|
if (method === KubernetesDeployRequestMethods.REPOSITORY) {
|
||||||
|
@ -194,7 +221,16 @@ class KubernetesDeployController {
|
||||||
payload.RepositoryUsername = this.formValues.RepositoryUsername;
|
payload.RepositoryUsername = this.formValues.RepositoryUsername;
|
||||||
payload.RepositoryPassword = this.formValues.RepositoryPassword;
|
payload.RepositoryPassword = this.formValues.RepositoryPassword;
|
||||||
}
|
}
|
||||||
payload.FilePathInRepository = this.formValues.FilePathInRepository;
|
payload.ManifestFile = this.formValues.ComposeFilePathInRepository;
|
||||||
|
payload.AdditionalFiles = this.formValues.AdditionalFiles;
|
||||||
|
if (this.formValues.RepositoryAutomaticUpdates) {
|
||||||
|
payload.AutoUpdate = {};
|
||||||
|
if (this.formValues.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
|
||||||
|
payload.AutoUpdate.Interval = this.formValues.RepositoryFetchInterval;
|
||||||
|
} else if (this.formValues.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
|
||||||
|
payload.AutoUpdate.Webhook = this.formValues.RepositoryWebhookURL.split('/').reverse()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
} else if (method === KubernetesDeployRequestMethods.STRING) {
|
||||||
payload.StackFileContent = this.formValues.EditorContent;
|
payload.StackFileContent = this.formValues.EditorContent;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one table to the
|
You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one table
|
||||||
other.
|
to the other.
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12" style="margin-top: 20px;">
|
<div class="col-sm-12" style="margin-top: 20px;">
|
||||||
<!-- available-endpoints -->
|
<!-- available-endpoints -->
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
.inline-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 15px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-input {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 15px;
|
||||||
|
width: calc(100% - 235px);
|
||||||
|
}
|
|
@ -15,8 +15,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="$ctrl.model.RepositoryAuthentication">
|
<div ng-if="$ctrl.model.RepositoryAuthentication">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="repository_username" class="col-sm-2 control-label text-left">Username</label>
|
<label for="repository_username" class="control-label text-left inline-label">
|
||||||
<div class="col-sm-3">
|
Username
|
||||||
|
</label>
|
||||||
|
<div class="inline-input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@ -29,11 +31,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="repository_password" class="col-sm-2 control-label text-left">
|
<label for="repository_password" class="control-label text-left inline-label">
|
||||||
Personal Access Token
|
Personal Access Token
|
||||||
<portainer-tooltip position="bottom" message="Provide a personal access token or password"></portainer-tooltip>
|
<portainer-tooltip position="bottom" message="Provide a personal access token or password"></portainer-tooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-3">
|
<div class="inline-input">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import controller from './git-form-auth-fieldset.controller.js';
|
import controller from './git-form-auth-fieldset.controller.js';
|
||||||
|
import './git-form-auth-fieldset.css';
|
||||||
|
|
||||||
export const gitFormAuthFieldset = {
|
export const gitFormAuthFieldset = {
|
||||||
templateUrl: './git-form-auth-fieldset.html',
|
templateUrl: './git-form-auth-fieldset.html',
|
||||||
|
|
|
@ -2,7 +2,6 @@ export const gitFormComposePathField = {
|
||||||
templateUrl: './git-form-compose-path-field.html',
|
templateUrl: './git-form-compose-path-field.html',
|
||||||
bindings: {
|
bindings: {
|
||||||
deployMethod: '@',
|
deployMethod: '@',
|
||||||
|
|
||||||
value: '<',
|
value: '<',
|
||||||
onChange: '<',
|
onChange: '<',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="form-group" ng-class="$ctrl.className">
|
<div class="form-group" ng-class="$ctrl.className">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<p>
|
<p>
|
||||||
This stack was deployed from the git repository <code>{{ $ctrl.url }}</code>
|
This {{ $ctrl.type }} was deployed from the git repository <code>{{ $ctrl.url }}</code>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<code
|
<code
|
||||||
>{{ $ctrl.configFilePath }}<span ng-if="$ctrl.additionalFiles.length > 0">, {{ $ctrl.additionalFiles.join(',') }}</span></code
|
>{{ $ctrl.configFilePath }}<span ng-if="$ctrl.additionalFiles.length > 0">, {{ $ctrl.additionalFiles.join(',') }}</span></code
|
||||||
>
|
>
|
||||||
in git and pull from here to update the stack.
|
in git and pull from here to update the {{ $ctrl.type }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,5 +5,6 @@ export const gitFormInfoPanel = {
|
||||||
configFilePath: '<',
|
configFilePath: '<',
|
||||||
additionalFiles: '<',
|
additionalFiles: '<',
|
||||||
className: '@',
|
className: '@',
|
||||||
|
type: '@',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,8 +43,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one table to
|
You can select which environment should be part of this group by moving them to the associated environments table. Simply click on any environment entry to move it from one
|
||||||
the other.
|
table to the other.
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12" style="margin-top: 20px;">
|
<div class="col-sm-12" style="margin-top: 20px;">
|
||||||
<!-- available-endpoints -->
|
<!-- available-endpoints -->
|
||||||
|
|
|
@ -83,7 +83,6 @@ class KubernetesAppGitFormController {
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
console.log(this);
|
|
||||||
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
|
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
|
||||||
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;
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
import uuidv4 from 'uuid/v4';
|
||||||
|
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
|
class KubernetesRedeployAppGitFormController {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper) {
|
||||||
|
this.$async = $async;
|
||||||
|
this.$state = $state;
|
||||||
|
this.StackService = StackService;
|
||||||
|
this.ModalService = ModalService;
|
||||||
|
this.Notifications = Notifications;
|
||||||
|
this.WebhookHelper = WebhookHelper;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
saveGitSettingsInProgress: false,
|
||||||
|
redeployInProgress: false,
|
||||||
|
showConfig: false,
|
||||||
|
isEdit: false,
|
||||||
|
hasUnsavedChanges: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.formValues = {
|
||||||
|
RefName: '',
|
||||||
|
RepositoryAuthentication: false,
|
||||||
|
RepositoryUsername: '',
|
||||||
|
RepositoryPassword: '',
|
||||||
|
// auto update
|
||||||
|
AutoUpdate: {
|
||||||
|
RepositoryAutomaticUpdates: false,
|
||||||
|
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||||
|
RepositoryFetchInterval: '5m',
|
||||||
|
RepositoryWebhookURL: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.onChangeRef = this.onChangeRef.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeRef(value) {
|
||||||
|
this.onChange({ RefName: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(values) {
|
||||||
|
this.formValues = {
|
||||||
|
...this.formValues,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
this.state.hasUnsavedChanges = angular.toJson(this.savedFormValues) !== angular.toJson(this.formValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAnalyticsProperties() {
|
||||||
|
const metadata = {
|
||||||
|
'automatic-updates': automaticUpdatesLabel(this.formValues.AutoUpdate.RepositoryAutomaticUpdates, this.formValues.AutoUpdate.RepositoryMechanism),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { metadata };
|
||||||
|
|
||||||
|
function automaticUpdatesLabel(repositoryAutomaticUpdates, repositoryMechanism) {
|
||||||
|
switch (repositoryAutomaticUpdates && repositoryMechanism) {
|
||||||
|
case RepositoryMechanismTypes.INTERVAL:
|
||||||
|
return 'polling';
|
||||||
|
case RepositoryMechanismTypes.WEBHOOK:
|
||||||
|
return 'webhook';
|
||||||
|
default:
|
||||||
|
return 'off';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pullAndRedeployApplication() {
|
||||||
|
return this.$async(async () => {
|
||||||
|
try {
|
||||||
|
const confirmed = await this.ModalService.confirmAsync({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: 'Any changes to this application will be overriden by the definition in git and may cause a service interruption. Do you wish to continue?',
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: 'Update',
|
||||||
|
className: 'btn-warning',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.redeployInProgress = true;
|
||||||
|
await this.StackService.updateKubeGit(this.stack.Id, this.stack.EndpointId, this.namespace, this.formValues);
|
||||||
|
this.Notifications.success('Pulled and redeployed stack successfully');
|
||||||
|
await this.$state.reload();
|
||||||
|
} catch (err) {
|
||||||
|
this.Notifications.error('Failure', err, 'Failed redeploying application');
|
||||||
|
} finally {
|
||||||
|
this.state.redeployInProgress = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveGitSettings() {
|
||||||
|
return this.$async(async () => {
|
||||||
|
try {
|
||||||
|
this.state.saveGitSettingsInProgress = true;
|
||||||
|
await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, null, this.formValues);
|
||||||
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
|
this.state.hasUnsavedChanges = false;
|
||||||
|
this.Notifications.success('Save stack settings successfully');
|
||||||
|
} catch (err) {
|
||||||
|
this.Notifications.error('Failure', err, 'Unable to save application settings');
|
||||||
|
} finally {
|
||||||
|
this.state.saveGitSettingsInProgress = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitButtonDisabled() {
|
||||||
|
return this.state.saveGitSettingsInProgress || this.state.redeployInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.formValues.RefName = this.stack.GitConfig.ReferenceName;
|
||||||
|
// Init auto update
|
||||||
|
if (this.stack.AutoUpdate && (this.stack.AutoUpdate.Interval || this.stack.AutoUpdate.Webhook)) {
|
||||||
|
this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true;
|
||||||
|
|
||||||
|
if (this.stack.AutoUpdate.Interval) {
|
||||||
|
this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.INTERVAL;
|
||||||
|
this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval;
|
||||||
|
} else if (this.stack.AutoUpdate.Webhook) {
|
||||||
|
this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.WEBHOOK;
|
||||||
|
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.formValues.AutoUpdate.RepositoryWebhookURL) {
|
||||||
|
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(uuidv4());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stack.GitConfig && this.stack.GitConfig.Authentication) {
|
||||||
|
this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username;
|
||||||
|
this.formValues.RepositoryAuthentication = true;
|
||||||
|
this.state.isEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KubernetesRedeployAppGitFormController;
|
|
@ -0,0 +1,64 @@
|
||||||
|
<form name="$ctrl.redeployGitForm">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Redeploy from git repository
|
||||||
|
</div>
|
||||||
|
<div class="form-group text-muted">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<p>
|
||||||
|
Pull the latest manifest from git and redeploy the application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<p>
|
||||||
|
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
|
||||||
|
<i ng-class="['fa space-right', { 'fa-minus': $ctrl.state.showConfig, 'fa-plus': !$ctrl.state.showConfig }]" aria-hidden="true"></i>
|
||||||
|
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.formValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
|
||||||
|
<git-form-auth-fieldset
|
||||||
|
ng-if="$ctrl.state.showConfig"
|
||||||
|
model="$ctrl.formValues"
|
||||||
|
is-edit="$ctrl.state.isEdit"
|
||||||
|
on-change="($ctrl.onChange)"
|
||||||
|
show-auth-explanation="true"
|
||||||
|
></git-form-auth-fieldset>
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<!-- #Git buttons -->
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
ng-click="$ctrl.pullAndRedeployApplication()"
|
||||||
|
ng-if="!$ctrl.formValues.AutoUpdate.RepositoryAutomaticUpdates"
|
||||||
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || $ctrl.state.hasUnsavedChanges|| !$ctrl.redeployGitForm.$valid"
|
||||||
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
|
button-spinner="$ctrl.state.redeployInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-application-edit-git-pull"
|
||||||
|
>
|
||||||
|
<span ng-show="!$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and update application </span>
|
||||||
|
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
ng-click="$ctrl.saveGitSettings()"
|
||||||
|
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.state.hasUnsavedChanges|| !$ctrl.redeployGitForm.$valid"
|
||||||
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
|
button-spinner="$ctrl.state.saveGitSettingsInProgress"
|
||||||
|
analytics-on
|
||||||
|
analytics-category="kubernetes"
|
||||||
|
analytics-event="kubernetes-application-edit"
|
||||||
|
analytics-properties="$ctrl.buildAnalyticsProperties()"
|
||||||
|
>
|
||||||
|
<span ng-show="!$ctrl.state.saveGitSettingsInProgress"> Save settings </span>
|
||||||
|
<span ng-show="$ctrl.state.saveGitSettingsInProgress">In progress...</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
import controller from './kubernetes-redeploy-app-git-form.controller';
|
||||||
|
|
||||||
|
const kubernetesRedeployAppGitForm = {
|
||||||
|
templateUrl: './kubernetes-redeploy-app-git-form.html',
|
||||||
|
controller,
|
||||||
|
bindings: {
|
||||||
|
stack: '<',
|
||||||
|
namespace: '<',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.module('portainer.app').component('kubernetesRedeployAppGitForm', kubernetesRedeployAppGitForm);
|
|
@ -1,5 +1,5 @@
|
||||||
import uuidv4 from 'uuid/v4';
|
import uuidv4 from 'uuid/v4';
|
||||||
|
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
class StackRedeployGitFormController {
|
class StackRedeployGitFormController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
constructor($async, $state, StackService, ModalService, Notifications, WebhookHelper, FormHelper) {
|
||||||
|
@ -28,7 +28,7 @@ class StackRedeployGitFormController {
|
||||||
// auto update
|
// auto update
|
||||||
AutoUpdate: {
|
AutoUpdate: {
|
||||||
RepositoryAutomaticUpdates: false,
|
RepositoryAutomaticUpdates: false,
|
||||||
RepositoryMechanism: 'Interval',
|
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||||
RepositoryFetchInterval: '5m',
|
RepositoryFetchInterval: '5m',
|
||||||
RepositoryWebhookURL: '',
|
RepositoryWebhookURL: '',
|
||||||
},
|
},
|
||||||
|
@ -50,9 +50,9 @@ class StackRedeployGitFormController {
|
||||||
|
|
||||||
function autoSyncLabel(type) {
|
function autoSyncLabel(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Interval':
|
case RepositoryMechanismTypes.INTERVAL:
|
||||||
return 'polling';
|
return 'polling';
|
||||||
case 'Webhook':
|
case RepositoryMechanismTypes.WEBHOOK:
|
||||||
return 'webhook';
|
return 'webhook';
|
||||||
}
|
}
|
||||||
return 'off';
|
return 'off';
|
||||||
|
@ -156,10 +156,10 @@ class StackRedeployGitFormController {
|
||||||
this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true;
|
this.formValues.AutoUpdate.RepositoryAutomaticUpdates = true;
|
||||||
|
|
||||||
if (this.stack.AutoUpdate.Interval) {
|
if (this.stack.AutoUpdate.Interval) {
|
||||||
this.formValues.AutoUpdate.RepositoryMechanism = `Interval`;
|
this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.INTERVAL;
|
||||||
this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval;
|
this.formValues.AutoUpdate.RepositoryFetchInterval = this.stack.AutoUpdate.Interval;
|
||||||
} else if (this.stack.AutoUpdate.Webhook) {
|
} else if (this.stack.AutoUpdate.Webhook) {
|
||||||
this.formValues.AutoUpdate.RepositoryMechanism = `Webhook`;
|
this.formValues.AutoUpdate.RepositoryMechanism = RepositoryMechanismTypes.WEBHOOK;
|
||||||
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook);
|
this.formValues.AutoUpdate.RepositoryWebhookURL = this.WebhookHelper.returnStackWebhookUrl(this.stack.AutoUpdate.Webhook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { StackViewModel, OrphanedStackViewModel } from '../../models/stack';
|
import { StackViewModel, OrphanedStackViewModel } from '../../models/stack';
|
||||||
|
|
||||||
angular.module('portainer.app').factory('StackService', [
|
angular.module('portainer.app').factory('StackService', [
|
||||||
|
@ -277,7 +278,17 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
StackFileContent: stackFile,
|
StackFileContent: stackFile,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
const autoUpdate = {};
|
||||||
|
if (gitConfig.AutoUpdate && gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
|
||||||
|
if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
|
||||||
|
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
|
||||||
|
} else if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
|
||||||
|
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
|
AutoUpdate: autoUpdate,
|
||||||
RepositoryReferenceName: gitConfig.RefName,
|
RepositoryReferenceName: gitConfig.RefName,
|
||||||
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
RepositoryAuthentication: gitConfig.RepositoryAuthentication,
|
||||||
RepositoryUsername: gitConfig.RepositoryUsername,
|
RepositoryUsername: gitConfig.RepositoryUsername,
|
||||||
|
@ -455,9 +466,9 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
const autoUpdate = {};
|
const autoUpdate = {};
|
||||||
|
|
||||||
if (gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
|
if (gitConfig.AutoUpdate.RepositoryAutomaticUpdates) {
|
||||||
if (gitConfig.AutoUpdate.RepositoryMechanism === 'Interval') {
|
if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
|
||||||
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
|
autoUpdate.Interval = gitConfig.AutoUpdate.RepositoryFetchInterval;
|
||||||
} else if (gitConfig.AutoUpdate.RepositoryMechanism === 'Webhook') {
|
} else if (gitConfig.AutoUpdate.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
|
||||||
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
|
autoUpdate.Webhook = gitConfig.AutoUpdate.RepositoryWebhookURL.split('/').reverse()[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,13 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<groups-datatable title-text="Environment groups" title-icon="fa-object-group" dataset="groups" table-key="groups" order-by="Name" remove-action="removeAction"></groups-datatable>
|
<groups-datatable
|
||||||
|
title-text="Environment groups"
|
||||||
|
title-icon="fa-object-group"
|
||||||
|
dataset="groups"
|
||||||
|
table-key="groups"
|
||||||
|
order-by="Name"
|
||||||
|
remove-action="removeAction"
|
||||||
|
></groups-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import uuidv4 from 'uuid/v4';
|
import uuidv4 from 'uuid/v4';
|
||||||
|
import { RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
|
@ -42,7 +42,7 @@ angular
|
||||||
ComposeFilePathInRepository: 'docker-compose.yml',
|
ComposeFilePathInRepository: 'docker-compose.yml',
|
||||||
AccessControlData: new AccessControlFormData(),
|
AccessControlData: new AccessControlFormData(),
|
||||||
RepositoryAutomaticUpdates: true,
|
RepositoryAutomaticUpdates: true,
|
||||||
RepositoryMechanism: 'Interval',
|
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||||
RepositoryFetchInterval: '5m',
|
RepositoryFetchInterval: '5m',
|
||||||
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||||
};
|
};
|
||||||
|
@ -111,9 +111,9 @@ angular
|
||||||
|
|
||||||
function autoSyncLabel(type) {
|
function autoSyncLabel(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'Interval':
|
case RepositoryMechanismTypes.INTERVAL:
|
||||||
return 'polling';
|
return 'polling';
|
||||||
case 'Webhook':
|
case RepositoryMechanismTypes.WEBHOOK:
|
||||||
return 'webhook';
|
return 'webhook';
|
||||||
}
|
}
|
||||||
return 'off';
|
return 'off';
|
||||||
|
@ -166,9 +166,9 @@ angular
|
||||||
function getAutoUpdatesProperty(repositoryOptions) {
|
function getAutoUpdatesProperty(repositoryOptions) {
|
||||||
if ($scope.formValues.RepositoryAutomaticUpdates) {
|
if ($scope.formValues.RepositoryAutomaticUpdates) {
|
||||||
repositoryOptions.AutoUpdate = {};
|
repositoryOptions.AutoUpdate = {};
|
||||||
if ($scope.formValues.RepositoryMechanism === 'Interval') {
|
if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
|
||||||
repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
|
repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
|
||||||
} else if ($scope.formValues.RepositoryMechanism === 'Webhook') {
|
} else if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
|
||||||
repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
|
repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,8 @@
|
||||||
additional-file="true"
|
additional-file="true"
|
||||||
auto-update="true"
|
auto-update="true"
|
||||||
show-auth-explanation="true"
|
show-auth-explanation="true"
|
||||||
|
path-text-title="Compose path"
|
||||||
|
path-placeholder="docker-compose.yml"
|
||||||
></git-form>
|
></git-form>
|
||||||
|
|
||||||
<custom-template-selector
|
<custom-template-selector
|
||||||
|
|
Loading…
Reference in New Issue