mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 07:49:41 +02:00
feat(kube): kube app auto update backend (#5547)
This commit is contained in:
parent
a5058e8f1e
commit
0e60f40937
22 changed files with 450 additions and 364 deletions
|
@ -56,6 +56,34 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||||
|
if tunnel.Status != portainer.EdgeAgentIdle {
|
||||||
|
return tunnel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.EdgeCheckinInterval == 0 {
|
||||||
|
settings, err := service.dataStore.Settings().Settings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||||
|
time.Sleep(waitForAgentToConnect * 2)
|
||||||
|
|
||||||
|
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||||
|
|
||||||
|
return tunnel, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||||
// It sets the status to ACTIVE.
|
// It sets the status to ACTIVE.
|
||||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||||
|
|
|
@ -100,8 +100,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer
|
||||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
|
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||||
|
@ -455,7 +455,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||||
|
|
||||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
|
||||||
|
|
||||||
if dataStore.IsNew() {
|
if dataStore.IsNew() {
|
||||||
err = updateSettingsFromFlags(dataStore, flags)
|
err = updateSettingsFromFlags(dataStore, flags)
|
||||||
|
@ -523,7 +523,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{
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||||
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
url, proxy, err := w.fetchEndpointProxy(endpoint)
|
url, proxy, err := w.fetchEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to featch endpoint proxy")
|
return errors.Wrap(err, "failed to fetch endpoint proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxy != nil {
|
if proxy != nil {
|
||||||
|
@ -88,7 +88,7 @@ func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (
|
||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
|
proxy, err := w.proxyManager.CreateAgentProxyServer(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,22 @@ package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
"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/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"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
||||||
|
@ -30,10 +28,11 @@ type KubernetesDeployer struct {
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
kubernetesClientFactory *cli.ClientFactory
|
kubernetesClientFactory *cli.ClientFactory
|
||||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
|
proxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
|
||||||
return &KubernetesDeployer{
|
return &KubernetesDeployer{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
dataStore: datastore,
|
dataStore: datastore,
|
||||||
|
@ -41,23 +40,28 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
kubernetesClientFactory: kubernetesClientFactory,
|
kubernetesClientFactory: kubernetesClientFactory,
|
||||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||||
|
proxyManager: proxyManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool, getAdminToken bool) (string, error) {
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||||
|
|
||||||
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
|
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if getAdminToken {
|
||||||
|
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -79,154 +83,61 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
|
||||||
|
|
||||||
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
||||||
// Otherwise it will use kubectl to deploy the manifest.
|
// Otherwise it will use kubectl to deploy the manifest.
|
||||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, manifestFiles []string, namespace string, deployAsAdmin bool) (string, error) {
|
||||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
token, err := deployer.getToken(request, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment, deployAsAdmin)
|
||||||
token, err := deployer.getToken(request, endpoint, true)
|
if err != nil {
|
||||||
if err != nil {
|
return "", err
|
||||||
return "", err
|
}
|
||||||
|
|
||||||
|
command := path.Join(deployer.binaryPath, "kubectl")
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||||
|
}
|
||||||
|
|
||||||
|
args := make([]string, 0)
|
||||||
|
|
||||||
|
if endpoint.Type != portainer.KubernetesLocalEnvironment {
|
||||||
|
url := endpoint.URL
|
||||||
|
switch endpoint.Type {
|
||||||
|
case portainer.AgentOnKubernetesEnvironment:
|
||||||
|
agentUrl, agentProxy, err := deployer.getAgentURL(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||||
|
}
|
||||||
|
url = agentUrl
|
||||||
|
defer agentProxy.Close()
|
||||||
|
case portainer.EdgeAgentOnKubernetesEnvironment:
|
||||||
|
url, err = deployer.getEdgeUrl(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command := path.Join(deployer.binaryPath, "kubectl")
|
args = append(args, "--server", url)
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
|
||||||
}
|
|
||||||
|
|
||||||
args := make([]string, 0)
|
|
||||||
args = append(args, "--server", endpoint.URL)
|
|
||||||
args = append(args, "--insecure-skip-tls-verify")
|
args = append(args, "--insecure-skip-tls-verify")
|
||||||
args = append(args, "--token", token)
|
|
||||||
args = append(args, "--namespace", namespace)
|
|
||||||
args = append(args, "apply", "-f", "-")
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd := exec.Command(command, args...)
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
cmd.Stdin = strings.NewReader(stackConfig)
|
|
||||||
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New(stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(output), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// agent
|
args = append(args, "--token", token)
|
||||||
|
args = append(args, "--namespace", namespace)
|
||||||
|
|
||||||
endpointURL := endpoint.URL
|
var fileArgs []string
|
||||||
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
for _, path := range manifestFiles {
|
||||||
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
fileArgs = append(fileArgs, "-f")
|
||||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
fileArgs = append(fileArgs, strings.TrimSpace(path))
|
||||||
|
|
||||||
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := deployer.dataStore.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
|
||||||
time.Sleep(waitForAgentToConnect * 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
|
||||||
}
|
}
|
||||||
|
args = append(args, "apply")
|
||||||
|
args = append(args, fileArgs...)
|
||||||
|
|
||||||
transport := &http.Transport{}
|
var stderr bytes.Buffer
|
||||||
|
cmd := exec.Command(command, args...)
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLS {
|
output, err := cmd.Output()
|
||||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
transport.TLSClientConfig = tlsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
httpCli := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(endpointURL, "http") {
|
|
||||||
endpointURL = fmt.Sprintf("https://%s", endpointURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
reqPayload, err := json.Marshal(
|
return string(output), nil
|
||||||
struct {
|
|
||||||
StackConfig string
|
|
||||||
Namespace string
|
|
||||||
}{
|
|
||||||
StackConfig: stackConfig,
|
|
||||||
Namespace: namespace,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := deployer.getToken(request, endpoint, false)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
|
||||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
|
||||||
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
|
||||||
|
|
||||||
resp, err := httpCli.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
var errorResponseData struct {
|
|
||||||
Message string
|
|
||||||
Details string
|
|
||||||
}
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
|
|
||||||
if err != nil {
|
|
||||||
output, parseStringErr := ioutil.ReadAll(resp.Body)
|
|
||||||
if parseStringErr != nil {
|
|
||||||
return "", parseStringErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseData struct{ Output string }
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&responseData)
|
|
||||||
if err != nil {
|
|
||||||
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
|
|
||||||
if parseStringErr != nil {
|
|
||||||
return "", parseStringErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseData.Output, nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
||||||
|
@ -251,3 +162,21 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (deployer *KubernetesDeployer) getEdgeUrl(endpoint *portainer.Endpoint) (string, error) {
|
||||||
|
tunnel, err := deployer.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed activating tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||||
|
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
|
||||||
|
}
|
||||||
|
|
23
api/filesystem/write.go
Normal file
23
api/filesystem/write.go
Normal file
|
@ -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)
|
||||||
|
}
|
48
api/filesystem/write_test.go
Normal file
48
api/filesystem/write_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
"github.com/portainer/portainer/api/internal/stackutils"
|
||||||
k "github.com/portainer/portainer/api/kubernetes"
|
k "github.com/portainer/portainer/api/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,7 +35,9 @@ type kubernetesGitDeploymentPayload struct {
|
||||||
RepositoryAuthentication bool
|
RepositoryAuthentication bool
|
||||||
RepositoryUsername string
|
RepositoryUsername string
|
||||||
RepositoryPassword string
|
RepositoryPassword string
|
||||||
FilePathInRepository string
|
ManifestFile string
|
||||||
|
AdditionalFiles []string
|
||||||
|
AutoUpdate *portainer.StackAutoUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
|
||||||
|
@ -57,12 +60,15 @@ 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
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +110,7 @@ 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(r, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
Name: stack.Name,
|
Name: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
|
@ -140,22 +146,34 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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,
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||||
|
@ -170,12 +188,19 @@ 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(r, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: stackID,
|
StackID: stackID,
|
||||||
Name: stack.Name,
|
Name: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
|
@ -186,6 +211,15 @@ 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}
|
||||||
|
@ -199,43 +233,14 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||||
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(r *http.Request, 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 {
|
|
||||||
return "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
|
|
||||||
}
|
|
||||||
manifest = convertedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest, err := k.AddAppLabels(manifest, appLabels)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to add application labels")
|
return "", errors.Wrap(err, "failed to create temp kub deployment files")
|
||||||
}
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
|
return handler.KubernetesDeployer.Deploy(r, endpoint, manifestFilePaths, stack.Namespace, false)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -33,12 +34,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)
|
||||||
|
@ -48,51 +49,51 @@ 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 endpoint 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 endpoint 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 endpoint", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", 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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,24 +104,24 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(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)
|
||||||
|
@ -129,31 +130,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 endpoint 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 endpoint 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 endpoint", err}
|
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = &portainer.Stack{
|
stack = &portainer.Stack{
|
||||||
|
@ -163,7 +164,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(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)
|
||||||
|
@ -173,6 +174,11 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
|
||||||
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(stack, endpoint)
|
return handler.ComposeStackManager.Down(stack, endpoint)
|
||||||
|
}
|
||||||
|
if stack.Type == portainer.KubernetesStack {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Unsupported stack type: %v", stack.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,11 +214,7 @@ 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))
|
_, err := handler.deployKubernetesStack(r, endpoint, stack, k.KubeAppLabels{
|
||||||
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")}
|
|
||||||
}
|
|
||||||
_, err = handler.deployKubernetesStack(r, endpoint, string(content), stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
|
|
||||||
StackID: int(stack.ID),
|
StackID: int(stack.ID),
|
||||||
Name: stack.Name,
|
Name: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
|
|
|
@ -22,6 +22,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 {
|
||||||
|
@ -35,12 +36,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 {
|
||||||
|
@ -48,6 +57,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 {
|
||||||
|
@ -60,6 +71,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +90,14 @@ 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{
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
|
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
|
||||||
|
}
|
||||||
|
stack.ProjectPath = projectPath
|
||||||
|
|
||||||
|
_, err = handler.deployKubernetesStack(r, endpoint, stack, k.KubeAppLabels{
|
||||||
StackID: int(stack.ID),
|
StackID: int(stack.ID),
|
||||||
Name: stack.Name,
|
Name: stack.Name,
|
||||||
Owner: stack.CreatedBy,
|
Owner: stack.CreatedBy,
|
||||||
|
@ -81,11 +108,5 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack via file content", Err: err}
|
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack via file content", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
|
||||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,29 +6,37 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
|
"github.com/portainer/portainer/api/http/proxy/factory/agent"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyServer provide an extedned proxy with a local server to forward requests
|
// ProxyServer provide an extended proxy with a local server to forward requests
|
||||||
type ProxyServer struct {
|
type ProxyServer struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
Port int
|
Port int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
// NewAgentProxy creates a new instance of ProxyServer that wrap http requests with agent headers
|
||||||
|
func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||||
|
if endpointutils.IsEdgeEndpoint((endpoint)) {
|
||||||
|
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed starting tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
|
||||||
return &ProxyServer{
|
return &ProxyServer{
|
||||||
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
|
Port: tunnel.Port,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointURL, err := url.Parse(endpoint.URL)
|
endpointURL, err := parseURL(endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointURL.Scheme = "http"
|
endpointURL.Scheme = "http"
|
||||||
|
@ -37,7 +45,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
|
||||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||||
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.WithMessage(err, "failed generating tls configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
httpTransport.TLSClientConfig = config
|
httpTransport.TLSClientConfig = config
|
||||||
|
@ -46,7 +54,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
|
||||||
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||||
|
|
||||||
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
proxy.Transport = agent.NewTransport(factory.signatureService, httpTransport)
|
||||||
|
|
||||||
proxyServer := &ProxyServer{
|
proxyServer := &ProxyServer{
|
||||||
server: &http.Server{
|
server: &http.Server{
|
||||||
|
@ -57,7 +65,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
|
||||||
|
|
||||||
err = proxyServer.start()
|
err = proxyServer.start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.Wrap(err, "failed starting proxy server")
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxyServer, err
|
return proxyServer, err
|
||||||
|
@ -91,3 +99,15 @@ func (proxy *ProxyServer) Close() {
|
||||||
proxy.server.Close()
|
proxy.server.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseURL parses the endpointURL using url.Parse.
|
||||||
|
//
|
||||||
|
// to prevent an error when url has port but no protocol prefix
|
||||||
|
// we add `//` prefix if needed
|
||||||
|
func parseURL(endpointURL string) (*url.URL, error) {
|
||||||
|
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
|
||||||
|
endpointURL = fmt.Sprintf("//%s", endpointURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.Parse(endpointURL)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package dockercompose
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -7,17 +7,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
|
// Transport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
|
||||||
AgentTransport struct {
|
Transport struct {
|
||||||
httpTransport *http.Transport
|
httpTransport *http.Transport
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
endpointIdentifier portainer.EndpointID
|
endpointIdentifier portainer.EndpointID
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
// NewTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
|
func NewTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *Transport {
|
||||||
transport := &AgentTransport{
|
transport := &Transport{
|
||||||
httpTransport: httpTransport,
|
httpTransport: httpTransport,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpT
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
|
||||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -48,10 +48,10 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComposeProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
// CreateAgentProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||||
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
|
func (manager *Manager) CreateAgentProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
|
||||||
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
|
return manager.proxyFactory.NewAgentProxy(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEndpointProxy returns the proxy associated to a key
|
// GetEndpointProxy returns the proxy associated to a key
|
||||||
|
|
|
@ -11,6 +11,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
|
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
// IsKubernetesEndpoint returns true if this is a kubernetes 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 ||
|
||||||
|
|
|
@ -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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
cmap "github.com/orcaman/concurrent-map"
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
@ -116,36 +115,18 @@ func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response,
|
||||||
func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||||
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
|
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
|
||||||
|
|
||||||
return factory.createRemoteClient(endpointURL);
|
return factory.createRemoteClient(endpointURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||||
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||||
|
if err != nil {
|
||||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
return nil, errors.Wrap(err, "failed activating tunnel")
|
||||||
err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint.EdgeCheckinInterval == 0 {
|
|
||||||
settings, err := factory.dataStore.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
|
||||||
time.Sleep(waitForAgentToConnect * 2)
|
|
||||||
|
|
||||||
tunnel = factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
|
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
|
||||||
|
|
||||||
return factory.createRemoteClient(endpointURL);
|
return factory.createRemoteClient(endpointURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernetes.Clientset, error) {
|
func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernetes.Clientset, error) {
|
||||||
|
|
|
@ -1235,7 +1235,7 @@ type (
|
||||||
|
|
||||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint
|
||||||
KubernetesDeployer interface {
|
KubernetesDeployer interface {
|
||||||
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
|
Deploy(request *http.Request, endpoint *Endpoint, manifestFiles []string, namespace string, deployAsAdmin bool) (string, error)
|
||||||
ConvertCompose(data []byte) ([]byte, error)
|
ConvertCompose(data []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1284,6 +1284,7 @@ type (
|
||||||
SetTunnelStatusToRequired(endpointID EndpointID) error
|
SetTunnelStatusToRequired(endpointID EndpointID) error
|
||||||
SetTunnelStatusToIdle(endpointID EndpointID)
|
SetTunnelStatusToIdle(endpointID EndpointID)
|
||||||
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
|
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
|
||||||
|
GetActiveTunnel(endpoint *Endpoint) (*TunnelDetails, error)
|
||||||
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
|
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
|
||||||
RemoveEdgeJob(edgeJobID EdgeJobID)
|
RemoveEdgeJob(edgeJobID EdgeJobID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,13 @@ import (
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -75,6 +79,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)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) 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()
|
||||||
|
@ -136,12 +140,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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,35 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"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) 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 {
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,3 +52,33 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
|
||||||
|
|
||||||
return d.composeStackManager.Up(stack, endpoint)
|
return d.composeStackManager.Up(stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
appLabels := k.KubeAppLabels{
|
||||||
|
StackID: int(stack.ID),
|
||||||
|
Name: stack.Name,
|
||||||
|
Owner: stack.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(nil, endpoint, manifestFilePaths, stack.Namespace, true)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to deploy kubernetes application")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"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/scheduler"
|
"github.com/portainer/portainer/api/scheduler"
|
||||||
|
@ -19,9 +20,10 @@ 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")
|
||||||
}
|
}
|
||||||
|
stackID := stack.ID // to be captured by the scheduled function
|
||||||
jobID := scheduler.StartJobEvery(d, func() {
|
jobID := scheduler.StartJobEvery(d, func() {
|
||||||
if err := RedeployWhenChanged(stack.ID, stackdeployer, datastore, gitService); err != nil {
|
if err := RedeployWhenChanged(stackID, stackdeployer, datastore, gitService); err != nil {
|
||||||
log.Printf("[ERROR] %s\n", err)
|
log.WithFields(log.Fields{"stackID": stackID}).WithError(err).Error("faile to auto-deploy a stack")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue