Compare commits

...

10 Commits

Author SHA1 Message Date
Dmitry Salakhov
a6b289c98e fix: docker-compose use custom config.json to access private images 2021-02-04 16:26:48 +13:00
LP B
e401724d43 fix(k8s/resource-pool): unusable RP access management (#4810) 2021-02-03 18:38:56 +13:00
yi-portainer
d2d7f6fdb9 Squashed commit of the following:
commit e4605d990d
Author: yi-portainer <yi.chen@portainer.io>
Date:   Tue Feb 2 17:42:57 2021 +1300

    * update portainer version

commit 768697157c
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Tue Feb 2 05:00:19 2021 +0100

    sec(app): remove unused and vulnerable dependencies (#4801)

commit d3086da139
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:10:06 2021 +1300

    fix(k8s) trigger port validation while changing protocol (ce#394) (#4804)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 95894e8047
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:03:11 2021 +1300

    fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 81de55fedd
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Feb 2 11:12:40 2021 +1300

    * fix missing kubectl download (#4802)

commit 84827b8782
Author: Steven Kang <skan070@gmail.com>
Date:   Sun Jan 31 17:32:30 2021 +1300

    feat(build): introducing buildx for Windows (#4792)

    * feat(build): introducing buildx for Windows

    * feat(build): re-ordered USER

    * feat(build): Fixed Typo

    * feat(build): fixed typo
2021-02-02 22:37:37 +13:00
LP B
b747f5f81e sec(app): remove unused and vulnerable dependencies (#4801) 2021-02-02 17:00:19 +13:00
Yi Chen
afbd353808 Merge windows buildx to develop (#4796)
* feat(build): introducing buildx for Windows

* feat(build): re-ordered USER

* feat(build): Fixed Typo

* feat(build): fixed typo

Co-authored-by: ssbkang <skan070@gmail.com>
2021-01-31 17:46:45 +13:00
Dmitry Salakhov
a71e71f481 feat(compose): add docker-compose wrapper (#4713)
* feat(compose): add docker-compose wrapper

ce-187

* fix(compose): pick compose implementation upon startup

* Add static compose build for linux

* Fix wget

* Fix platofrm specific docker-compose download

* Keep amd64 architecture as download parameter

* Add tmp folder for docker-compose

* fix: line endings

* add proxy server

* logs

* Proxy

* Add lite transport for compose

* Fix local deployment

* refactor: pass proxyManager by ref

* fix: string conversion

* refactor: compose wrapper remove unused code

* fix: tests

* Add edge

* Fix merge issue

* refactor: remove unused code

* Move server to proxy implementation

* Cleanup wrapper and manager

* feat: pass max supported compose syntax version with each endpoint

* fix: pick compose syntax version

* fix: store wrapper version in portainer

* Get and show composeSyntaxMaxVersion at stack creation screen

* Get and show composeSyntaxMaxVersion at stack editor screen

* refactor: proxy server

* Fix used tmp

* Bump docker-compose to 1.28.0

* remove message for docker compose limitation

* fix: markup typo

* Rollback docker compose to 1.27.4

* * attempt to fix the windows build issue

* * attempt to debug grunt issue

* * use console log in grunt file

* fix: try to fix windows build by removing indirect deps from go.mod

* Remove tmp folder

* Remove builder stage

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose

* feat(build/windows): add git for Docker Compose - fixed verbose output

* refactor: renames

* fix(stack): get endpoint by EndpointProvider

* fix(stack): use margin to add space between line instead of using br tag

Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: yi-portainer <yi.chen@portainer.io>
Co-authored-by: Steven Kang <skan070@gmail.com>
2021-01-26 08:16:53 +13:00
LP B
83f4c5ec0b fix(k8s/app): remove advanced deployment panel from app details view (#4730) 2021-01-25 14:43:54 +13:00
Maxime Bajeux
41308d570d feat(configurations): Review UI/UX configurations (#4691)
* feat(configurations): Review UI/UX configurations

* feat(configurations): fix binary secret value

* fix(frontend): populate data between simple and advanced modes (#4503)

* fix(configuration): parseYaml before create configuration

* fix(configurations): change c to C in ConfigurationOwner

* fix(application): change configuration index to configuration key in the view

* fix(configuration): resolve problem in application create with configuration not overriden.

* fix(configuration): fix bad import in helper

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-01-25 14:14:35 +13:00
Chaim Lev-Ari
46ff8a01bc fix(kubernetes/pods): save note (#4675)
* feat(kubernetes/pods): introduce patch api

* feat(k8s/pods): pod converter

* feat(kubernetes/pods): introduce patch api

* feat(k8s/pod): add annotations only if needed

* fix(k8s/pod): replace class with factory function
2021-01-22 14:08:08 +13:00
yi-portainer
2b257d2785 Squashed commit of the following 2.0.1 release fixes:
commit f90d6b55d6
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Wed Jan 13 00:56:19 2021 +0200

    feat(service): clear source volume when change type (#4627)

    * feat(service): clear source volume when change type

    * feat(service): init volume source to the correct value

commit 1b82b450d7
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Thu Jan 7 14:47:32 2021 +1300

    * bump the APIVersion to 2.0.1 (#4688)

commit b78d804881
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Wed Dec 30 23:03:43 2020 +1300

    Revert "chore(build): bump Kompose version (#4475)" (#4676)

    This reverts commit 380f106571.

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

commit 51b72c12f9
Author: Anthony Lapenna <anthony.lapenna@portainer.io>
Date:   Wed Dec 23 14:45:32 2020 +1300

    fix(docker/stack-details): do not display editor tab for external stack (#4650)

commit 58c04bdbe3
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Dec 22 13:47:11 2020 +1300

    + silently continue when downloading artifacts in windows (#4637)

commit a6320d5222
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Dec 22 13:38:54 2020 +1300

    fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

    * fix(frontend) rephrase comments (#4629)

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-01-21 00:04:15 +13:00
68 changed files with 1244 additions and 311 deletions

View File

@@ -17,6 +17,8 @@ import (
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
@@ -71,7 +73,12 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
return store
}
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, dataStorePath, proxyManager)
if composeWrapper != nil {
return composeWrapper
}
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
@@ -384,8 +391,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
@@ -452,27 +461,29 @@ func main() {
}
var server portainer.Server = &http.Server{
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)

136
api/exec/compose_wrapper.go Normal file
View File

@@ -0,0 +1,136 @@
package exec
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
)
// ComposeWrapper is a wrapper for docker-compose binary
type ComposeWrapper struct {
binaryPath string
dataPath string
proxyManager *proxy.Manager
}
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeWrapper(binaryPath, dataPath string, proxyManager *proxy.Manager) *ComposeWrapper {
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
return nil
}
return &ComposeWrapper{
binaryPath: binaryPath,
dataPath: dataPath,
proxyManager: proxyManager,
}
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
return err
}
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
if endpoint == nil {
return nil, errors.New("cannot call a compose command on an empty endpoint")
}
program := programPath(w.binaryPath, "docker-compose")
options := setComposeFile(stack)
options = addProjectNameOption(options, stack)
options, err := addEnvFileOption(options, stack)
if err != nil {
return nil, err
}
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return nil, err
}
defer proxy.Close()
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
}
args := append(options, command...)
var stderr bytes.Buffer
cmd := exec.Command(program, args...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONFIG=%s", w.dataPath))
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return out, errors.New(stderr.String())
}
return out, nil
}
func setComposeFile(stack *portainer.Stack) []string {
options := make([]string, 0)
if stack == nil || stack.EntryPoint == "" {
return options
}
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
options = append(options, "-f", composeFilePath)
return options
}
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
if stack == nil || stack.Name == "" {
return options
}
options = append(options, "-p", stack.Name)
return options
}
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
return options, nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return options, err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
options = append(options, "--env-file", envFilePath)
return options, nil
}

View File

@@ -0,0 +1,75 @@
// +build integration
package exec
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
)
const composeFile = `version: "3.9"
services:
busybox:
image: "alpine:latest"
container_name: "compose_wrapper_test"`
const composedContainerName = "compose_wrapper_test"
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
dir := t.TempDir()
composeFileName := "compose_wrapper_test.yml"
f, _ := os.Create(filepath.Join(dir, composeFileName))
f.WriteString(composeFile)
stack := &portainer.Stack{
ProjectPath: dir,
EntryPoint: composeFileName,
Name: "project-name",
}
endpoint := &portainer.Endpoint{}
return stack, endpoint
}
func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w := NewComposeWrapper("", "", nil)
err := w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
if containerExists(composedContainerName) == false {
t.Fatal("container should exist")
}
err = w.Down(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose down: %s", err)
}
if containerExists(composedContainerName) {
t.Fatal("container should be removed")
}
}
func containerExists(contaierName string) bool {
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
out, err := cmd.Output()
if err != nil {
log.Fatalf("failed to list containers: %s", err)
}
return strings.Contains(string(out), contaierName)
}

View File

@@ -0,0 +1,143 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_setComposeFile(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should return empty result if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should return empty result if stack don't have entrypoint",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: []string{"-f", path.Join("dir", "file")},
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: []string{"-f", "file"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := setComposeFile(tt.stack)
assert.ElementsMatch(t, tt.expected, result)
})
}
}
func Test_addProjectNameOption(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected []string
}{
{
name: "should not add project option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add project option if stack doesn't have name",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should add project name option if stack has a name",
stack: &portainer.Stack{
Name: "project-name",
},
expected: []string{"-p", "project-name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result := addProjectNameOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
})
}
}
func Test_addEnvFileOption(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected []string
expectedContent string
}{
{
name: "should not add env file option if stack is missing",
stack: nil,
expected: []string{},
},
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{},
expected: []string{},
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: []string{},
},
{
name: "should add env file option if stack has env variables",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{
{Name: "var1", Value: "value1"},
{Name: "var2", Value: "value2"},
},
},
expected: []string{"--env-file", path.Join(dir, "stack.env")},
expectedContent: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := []string{"-a", "b"}
result, _ := addEnvFileOption(options, tt.stack)
assert.ElementsMatch(t, append(options, tt.expected...), result)
if tt.expectedContent != "" {
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expectedContent, string(content))
}
})
}
}

24
api/exec/utils.go Normal file
View File

@@ -0,0 +1,24 @@
package exec
import (
"os/exec"
"path/filepath"
"runtime"
)
func osProgram(program string) string {
if runtime.GOOS == "windows" {
program += ".exe"
}
return program
}
func programPath(rootPath, program string) string {
return filepath.Join(rootPath, osProgram(program))
}
// IsBinaryPresent returns true if corresponding program exists on PATH
func IsBinaryPresent(program string) bool {
_, err := exec.LookPath(program)
return err == nil
}

16
api/exec/utils_test.go Normal file
View File

@@ -0,0 +1,16 @@
package exec
import (
"testing"
)
func Test_isBinaryPresent(t *testing.T) {
if !IsBinaryPresent("docker") {
t.Error("expect docker binary to exist on the path")
}
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
t.Error("expect binary with a random name to be missing on the path")
}
}

View File

@@ -28,6 +28,7 @@ require (
github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45

View File

@@ -262,12 +262,15 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
@@ -392,6 +395,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -6,7 +6,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
@@ -30,6 +30,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
}
hideFields(endpoint)
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
return response.JSON(w, endpoint)
}

View File

@@ -5,12 +5,11 @@ import (
"strconv"
"strings"
"github.com/portainer/portainer/api"
"github.com/portainer/libhttp/request"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -89,6 +88,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
for idx := range paginatedEndpoints {
hideFields(&paginatedEndpoints[idx])
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
}
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))

View File

@@ -27,6 +27,7 @@ type Handler struct {
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
ComposeStackManager portainer.ComposeStackManager
}
// NewHandler creates a handler to manage endpoint operations.

View File

@@ -357,7 +357,6 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err

View File

@@ -7,7 +7,7 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)

View File

@@ -155,5 +155,6 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
if stack.Type == portainer.DockerSwarmStack {
return handler.SwarmStackManager.Remove(stack, endpoint)
}
return handler.ComposeStackManager.Down(stack, endpoint)
}

View File

@@ -4,13 +4,13 @@ import (
"errors"
"net/http"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)

View File

@@ -4,15 +4,14 @@ import (
"errors"
"net/http"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
// POST request on /api/stacks/:id/stop

View File

@@ -0,0 +1,88 @@
package factory
import (
"fmt"
"log"
"net"
"net/http"
"net/url"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
)
// ProxyServer provide an extedned proxy with a local server to forward requests
type ProxyServer struct {
server *http.Server
Port int
}
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return &ProxyServer{
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
}, nil
}
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
endpointURL.Scheme = "http"
httpTransport := &http.Transport{}
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}
httpTransport.TLSClientConfig = config
endpointURL.Scheme = "https"
}
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
proxyServer := &ProxyServer{
&http.Server{
Handler: proxy,
},
0,
}
return proxyServer, proxyServer.start()
}
func (proxy *ProxyServer) start() error {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return err
}
proxy.Port = listener.Addr().(*net.TCPAddr).Port
go func() {
proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port)
log.Printf("Starting Proxy server on %s...\n", proxyHost)
err := proxy.server.Serve(listener)
log.Printf("Exiting Proxy server %s\n", proxyHost)
if err != http.ErrServerClosed {
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
}
}()
return nil
}
// Close shuts down the server
func (proxy *ProxyServer) Close() {
if proxy.server != nil {
proxy.server.Close()
}
}

View File

@@ -0,0 +1,40 @@
package dockercompose
import (
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
AgentTransport struct {
httpTransport *http.Transport
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
)
// NewAgentTransport 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 {
transport := &AgentTransport{
httpTransport: httpTransport,
signatureService: signatureService,
}
return transport
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
return transport.httpTransport.RoundTrip(request)
}

View File

@@ -1,6 +1,7 @@
package proxy
import (
"fmt"
"net/http"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
@@ -43,13 +44,19 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
return nil, err
}
manager.endpointProxies.Set(string(endpoint.ID), proxy)
manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy)
return proxy, nil
}
// CreateComposeProxyServer 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.
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
}
// GetEndpointProxy returns the proxy associated to a key
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))
proxy, ok := manager.endpointProxies.Get(fmt.Sprint(endpoint.ID))
if !ok {
return nil
}
@@ -61,7 +68,7 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand
// and cleans the k8s endpoint client cache. DeleteEndpointProxy
// is currently only called for edge connection clean up.
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
manager.endpointProxies.Remove(string(endpoint.ID))
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
manager.k8sClientFactory.RemoveKubeClient(endpoint)
}

View File

@@ -39,39 +39,41 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
}
// Start starts the HTTP server
func (server *Server) Start() error {
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
kubernetesTokenCacheManager := server.KubernetesTokenCacheManager
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
@@ -82,7 +84,7 @@ func (server *Server) Start() error {
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.LDAPService = server.LDAPService
authHandler.ProxyManager = proxyManager
authHandler.ProxyManager = server.ProxyManager
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
authHandler.OAuthService = server.OAuthService
@@ -116,10 +118,10 @@ func (server *Server) Start() error {
var endpointHandler = endpoints.NewHandler(requestBouncer)
endpointHandler.DataStore = server.DataStore
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ProxyManager = server.ProxyManager
endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.DataStore = server.DataStore
@@ -131,7 +133,7 @@ func (server *Server) Start() error {
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.DataStore = server.DataStore
endpointProxyHandler.ProxyManager = proxyManager
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
@@ -141,7 +143,7 @@ func (server *Server) Start() error {
var registryHandler = registries.NewHandler(requestBouncer)
registryHandler.DataStore = server.DataStore
registryHandler.FileService = server.FileService
registryHandler.ProxyManager = proxyManager
registryHandler.ProxyManager = server.ProxyManager
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
resourceControlHandler.DataStore = server.DataStore

View File

@@ -13,11 +13,12 @@ import (
"github.com/portainer/libcompose/lookup"
"github.com/portainer/libcompose/project"
"github.com/portainer/libcompose/project/options"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
const (
dockerClientVersion = "1.24"
dockerClientVersion = "1.24"
composeSyntaxMaxVersion = "2"
)
// ComposeStackManager represents a service for managing compose stacks.
@@ -58,6 +59,11 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
return client.NewDefaultFactory(clientOpts)
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return composeSyntaxMaxVersion
}
// Up will deploy a compose stack (equivalent of docker-compose up)
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {

View File

@@ -190,24 +190,25 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -778,6 +779,7 @@ type (
// ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
}
@@ -1123,9 +1125,11 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.0.1"
APIVersion = "2.1.0"
// DBVersion is the version number of the Portainer database
DBVersion = 25
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved

View File

@@ -309,8 +309,8 @@
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.Source = null">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Source = null">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
@@ -333,7 +333,7 @@
ng-model="volume.Source"
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
>
<option selected disabled hidden value="">Select a volume</option>
<option selected disabled value="">Select a volume</option>
</select>
</div>
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>

View File

@@ -3,13 +3,13 @@
Data
</div>
<div class="form-group">
<div class="form-group" ng-if="$ctrl.isCreation">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = false">
<a class="small interactive" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.showAdvancedMode()">
<i class="fa fa-list-ol space-right" aria-hidden="true"></i> Advanced mode
</a>
<a class="small interactive" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = true">
<a class="small interactive" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.showSimpleMode()">
<i class="fa fa-edit space-right" aria-hidden="true"></i> Simple mode
</a>
</p>
@@ -61,7 +61,7 @@
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="form-group" ng-if="$ctrl.formValues.IsSimple && !entry.IsBinary">
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11">
<textarea
@@ -80,6 +80,13 @@
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple && entry.IsBinary">
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11 control-label small text-muted text-left"
>Binary data <portainer-tooltip position="bottom" message="This key holds binary data and cannot be displayed."></portainer-tooltip
></div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-1"></div>
<div class="col-sm-11">

View File

@@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
bindings: {
formValues: '=',
isValid: '=',
isCreation: '=',
},
});

View File

@@ -1,7 +1,10 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
import chardet from 'chardet';
import { Base64 } from 'js-base64';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesConfigurationDataController {
/* @ngInject */
@@ -12,6 +15,8 @@ class KubernetesConfigurationDataController {
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
this.onFileLoad = this.onFileLoad.bind(this);
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
this.showSimpleMode = this.showSimpleMode.bind(this);
this.showAdvancedMode = this.showAdvancedMode.bind(this);
}
onChangeKey() {
@@ -20,7 +25,7 @@ class KubernetesConfigurationDataController {
}
addEntry() {
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
}
removeEntry(index) {
@@ -37,9 +42,20 @@ class KubernetesConfigurationDataController {
}
async onFileLoadAsync(event) {
const entry = new KubernetesConfigurationFormValuesDataEntry();
const entry = new KubernetesConfigurationFormValuesEntry();
const encoding = chardet.detect(Buffer.from(event.target.result));
const decoder = new TextDecoder(encoding);
entry.Key = event.target.fileName;
entry.Value = event.target.result;
entry.IsBinary = KubernetesConfigurationHelper.isBinary(encoding);
if (!entry.IsBinary) {
entry.Value = decoder.decode(event.target.result);
} else {
const stringValue = decoder.decode(event.target.result);
entry.Value = Base64.encode(stringValue);
}
this.formValues.Data.push(entry);
this.onChangeKey();
}
@@ -53,10 +69,20 @@ class KubernetesConfigurationDataController {
const temporaryFileReader = new FileReader();
temporaryFileReader.fileName = file.name;
temporaryFileReader.onload = this.onFileLoad;
temporaryFileReader.readAsText(file);
temporaryFileReader.readAsArrayBuffer(file);
}
}
showSimpleMode() {
this.formValues.IsSimple = true;
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
}
showAdvancedMode() {
this.formValues.IsSimple = false;
this.formValues.DataYaml = KubernetesConfigurationHelper.parseData(this.formValues);
}
$onInit() {
this.state = {
duplicateKeys: {},

View File

@@ -1,10 +1,32 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
import { KubernetesConfigMap, KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesConfigMapConverter {
static apiToPortainerAccessConfigMap(data) {
const res = new KubernetesPortainerAccessConfigMap();
res.Id = data.metadata.uid;
res.Data = data.data;
return res;
}
static createAccessPayload(data) {
const res = new KubernetesConfigMapCreatePayload();
_.unset(res, 'binaryData');
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.data = data.Data;
return res;
}
static updateAccessPayload(data) {
const res = KubernetesConfigMapConverter.createAccessPayload(data);
res.metadata.uid = data.Id;
return res;
}
/**
* API ConfigMap to front ConfigMap
*/
@@ -16,7 +38,23 @@ class KubernetesConfigMapConverter {
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = data.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = data.data;
res.Data = _.concat(
_.map(data.data, (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
return entry;
}),
_.map(data.binaryData, (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
entry.IsBinary = true;
return entry;
})
);
return res;
}
@@ -41,7 +79,14 @@ class KubernetesConfigMapConverter {
res.metadata.namespace = data.Namespace;
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
res.data = data.Data;
_.forEach(data.Data, (entry) => {
if (entry.IsBinary) {
res.binaryData[entry.Key] = entry.Value;
} else {
res.data[entry.Key] = entry.Value;
}
});
return res;
}
@@ -54,7 +99,13 @@ class KubernetesConfigMapConverter {
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
res.data = data.Data;
_.forEach(data.Data, (entry) => {
if (entry.IsBinary) {
res.binaryData[entry.Key] = entry.Value;
} else {
res.data[entry.Key] = entry.Value;
}
});
return res;
}
@@ -64,18 +115,7 @@ class KubernetesConfigMapConverter {
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
res.Data = formValues.Data;
return res;
}
}

View File

@@ -1,3 +1,4 @@
import _ from 'lodash-es';
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationConverter {
@@ -9,7 +10,9 @@ class KubernetesConfigurationConverter {
res.Namespace = secret.Namespace;
res.CreationDate = secret.CreationDate;
res.Yaml = secret.Yaml;
res.Data = secret.Data;
_.forEach(secret.Data, (entry) => {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = secret.ConfigurationOwner;
return res;
}
@@ -22,7 +25,9 @@ class KubernetesConfigurationConverter {
res.Namespace = configMap.Namespace;
res.CreationDate = configMap.CreationDate;
res.Yaml = configMap.Yaml;
res.Data = configMap.Data;
_.forEach(configMap.Data, (entry) => {
res.Data[entry.Key] = entry.Value;
});
res.ConfigurationOwner = configMap.ConfigurationOwner;
return res;
}

View File

@@ -1,17 +1,30 @@
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import YAML from 'yaml';
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import _ from 'lodash-es';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
const configurationOwner = _.truncate(secret.configurationOwner, { length: 63, omission: '' });
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
res.stringData = secret.Data;
let annotation = '';
_.forEach(secret.Data, (entry) => {
if (entry.IsBinary) {
res.data[entry.Key] = entry.Value;
annotation += annotation !== '' ? '|' + entry.Key : entry.Key;
} else {
res.stringData[entry.Key] = entry.Value;
}
});
if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
}
return res;
}
@@ -20,7 +33,19 @@ class KubernetesSecretConverter {
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
res.stringData = secret.Data;
let annotation = '';
_.forEach(secret.Data, (entry) => {
if (entry.IsBinary) {
res.data[entry.Key] = entry.Value;
annotation += annotation !== '' ? '|' + entry.Key : entry.Key;
} else {
res.stringData[entry.Key] = entry.Value;
}
});
if (annotation !== '') {
res.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] = annotation;
}
return res;
}
@@ -32,7 +57,21 @@ class KubernetesSecretConverter {
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = payload.data;
res.Data = _.map(payload.data, (value, key) => {
const annotations = payload.metadata.annotations ? payload.metadata.annotations[KubernetesPortainerConfigurationDataAnnotation] : '';
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.IsBinary = _.includes(annotations, entry.Key);
if (!entry.IsBinary) {
entry.Value = atob(value);
} else {
entry.Value = value;
}
return entry;
});
return res;
}
@@ -41,18 +80,7 @@ class KubernetesSecretConverter {
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
res.Data = formValues.Data;
return res;
}
}

View File

@@ -1,5 +1,7 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import _ from 'lodash-es';
import YAML from 'yaml';
class KubernetesConfigurationHelper {
static getUsingApplications(config, applications) {
@@ -21,6 +23,10 @@ class KubernetesConfigurationHelper {
return _.startsWith(config.Name, 'default-token-');
}
static isBinary(encoding) {
return encoding !== '' && !_.includes(encoding, 'ISO') && !_.includes(encoding, 'UTF');
}
static setConfigurationUsed(config) {
config.Used = config.Applications && config.Applications.length !== 0;
}
@@ -32,6 +38,33 @@ class KubernetesConfigurationHelper {
});
}
static parseYaml(formValues) {
YAML.defaultOptions.customTags = ['binary'];
const data = _.map(YAML.parse(formValues.DataYaml), (value, key) => {
const entry = new KubernetesConfigurationFormValuesEntry();
entry.Key = key;
entry.Value = value;
const oldEntry = _.find(formValues.Data, { Key: entry.Key });
entry.IsBinary = oldEntry ? oldEntry.IsBinary : false;
return entry;
});
return data;
}
static parseData(formValues) {
if (!formValues.Data.length) return '';
const data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
return YAML.stringify(data);
}
static isExternalConfiguration(configuration) {
return !configuration.ConfigurationOwner;
}

View File

@@ -2,6 +2,15 @@ export const KubernetesPortainerConfigMapNamespace = 'portainer';
export const KubernetesPortainerConfigMapConfigName = 'portainer-config';
export const KubernetesPortainerConfigMapAccessKey = 'NamespaceAccessPolicies';
export function KubernetesPortainerAccessConfigMap() {
return {
Id: 0,
Name: KubernetesPortainerConfigMapConfigName,
Namespace: KubernetesPortainerConfigMapNamespace,
Data: {},
};
}
/**
* ConfigMap Model
*/
@@ -11,7 +20,7 @@ const _KubernetesConfigMap = Object.freeze({
Namespace: '',
Yaml: '',
ConfigurationOwner: '',
Data: {},
Data: [],
});
export class KubernetesConfigMap {

View File

@@ -6,6 +6,7 @@ import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloa
const _KubernetesConfigMapCreatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
data: {},
binaryData: {},
});
export class KubernetesConfigMapCreatePayload {
constructor() {
@@ -19,6 +20,7 @@ export class KubernetesConfigMapCreatePayload {
const _KubernetesConfigMapUpdatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
data: {},
binaryData: {},
});
export class KubernetesConfigMapUpdatePayload {
constructor() {

View File

@@ -20,16 +20,14 @@ export class KubernetesConfigurationFormValues {
}
}
/**
* KubernetesConfigurationEntry Model
*/
const _KubernetesConfigurationFormValuesDataEntry = Object.freeze({
const _KubernetesConfigurationFormValuesEntry = Object.freeze({
Key: '',
Value: '',
IsBinary: false,
});
export class KubernetesConfigurationFormValuesDataEntry {
export class KubernetesConfigurationFormValuesEntry {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValuesDataEntry)));
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValuesEntry)));
}
}

View File

@@ -1,4 +1,5 @@
export const KubernetesPortainerConfigurationOwnerLabel = 'io.portainer.kubernetes.configuration.owner';
export const KubernetesPortainerConfigurationDataAnnotation = 'io.portainer.kubernetes.configuration.data';
/**
* Configuration Model (Composite)

View File

@@ -8,7 +8,7 @@ const _KubernetesApplicationSecret = Object.freeze({
CreationDate: '',
ConfigurationOwner: '',
Yaml: '',
Data: {},
Data: [],
});
export class KubernetesApplicationSecret {

View File

@@ -7,6 +7,7 @@ const _KubernetesSecretCreatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque',
data: {},
stringData: {},
});
export class KubernetesSecretCreatePayload {
@@ -22,6 +23,7 @@ const _KubernetesSecretUpdatePayload = Object.freeze({
metadata: new KubernetesCommonMetadataPayload(),
type: 'Opaque',
data: {},
stringData: {},
});
export class KubernetesSecretUpdatePayload {

View File

@@ -1,5 +1,16 @@
import * as JsonPatch from 'fast-json-patch';
import _ from 'lodash-es';
import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes } from 'Kubernetes/pod/models';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import {
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationNote,
} from 'Kubernetes/models/application/models';
import { createPayloadFactory } from './payloads/create';
import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes } from './models';
function computeStatus(statuses) {
const containerStatuses = _.map(statuses, 'state');
@@ -104,4 +115,48 @@ export default class KubernetesPodConverter {
res.Tolerations = computeTolerations(data.spec.tolerations);
return res;
}
static patchPayload(oldPod, newPod) {
const oldPayload = createPayload(oldPod);
const newPayload = createPayload(newPod);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
function createPayload(pod) {
const payload = createPayloadFactory();
payload.metadata.name = pod.Name;
payload.metadata.namespace = pod.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = pod.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = pod.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pod.ApplicationOwner;
if (pod.Note) {
payload.metadata.annotations[KubernetesPortainerApplicationNote] = pod.Note;
} else {
payload.metadata.annotations = undefined;
}
payload.spec.replicas = pod.ReplicaCount;
payload.spec.selector.matchLabels.app = pod.Name;
payload.spec.template.metadata.labels.app = pod.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = pod.ApplicationName;
payload.spec.template.spec.containers[0].name = pod.Name;
payload.spec.template.spec.containers[0].image = pod.Image;
payload.spec.template.spec.affinity = pod.Affinity;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', pod.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', pod.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', pod.Volumes);
if (pod.MemoryLimit) {
payload.spec.template.spec.containers[0].resources.limits.memory = pod.MemoryLimit;
payload.spec.template.spec.containers[0].resources.requests.memory = pod.MemoryLimit;
}
if (pod.CpuLimit) {
payload.spec.template.spec.containers[0].resources.limits.cpu = pod.CpuLimit;
payload.spec.template.spec.containers[0].resources.requests.cpu = pod.CpuLimit;
}
if (!pod.CpuLimit && !pod.MemoryLimit) {
delete payload.spec.template.spec.containers[0].resources;
}
return payload;
}

View File

@@ -0,0 +1,45 @@
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
export function createPayloadFactory() {
return {
metadata: new KubernetesCommonMetadataPayload(),
spec: {
replicas: 0,
selector: {
matchLabels: {
app: '',
},
},
strategy: {
type: 'RollingUpdate',
rollingUpdate: {
maxSurge: 0,
maxUnavailable: '100%',
},
},
template: {
metadata: {
labels: {
app: '',
},
},
spec: {
affinity: {},
containers: [
{
name: '',
image: '',
env: [],
resources: {
limits: {},
requests: {},
},
volumeMounts: [],
},
],
volumes: [],
},
},
},
};
}

View File

@@ -2,6 +2,7 @@ import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesPodConverter from './converter';
class KubernetesPodService {
/* @ngInject */
@@ -13,6 +14,7 @@ class KubernetesPodService {
this.getAllAsync = this.getAllAsync.bind(this);
this.logsAsync = this.logsAsync.bind(this);
this.deleteAsync = this.deleteAsync.bind(this);
this.patchAsync = this.patchAsync.bind(this);
}
async getAsync(namespace, name) {
@@ -74,6 +76,29 @@ class KubernetesPodService {
return this.$async(this.logsAsync, namespace, podName, containerName);
}
/**
* PATCH
*/
async patchAsync(oldPod, newPod) {
try {
const params = new KubernetesCommonParams();
params.id = newPod.Name;
const namespace = newPod.Namespace;
const payload = KubernetesPodConverter.patchPayload(oldPod, newPod);
if (!payload.length) {
return;
}
const data = await this.KubernetesPods(namespace).patch(params, payload).$promise;
return data;
} catch (err) {
throw new PortainerError('Unable to patch pod', err);
}
}
patch(oldPod, newPod) {
return this.$async(this.patchAsync, oldPod, newPod);
}
/**
* DELETE
*/

View File

@@ -31,6 +31,12 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
create: { method: 'POST' },
update: { method: 'PUT' },
delete: { method: 'DELETE' },
patch: {
method: 'PATCH',
headers: {
'Content-Type': 'application/json-patch+json',
},
},
logs: {
method: 'GET',
params: { action: 'log' },

View File

@@ -3,6 +3,7 @@ import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import { KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
class KubernetesConfigMapService {
/* @ngInject */
@@ -17,6 +18,54 @@ class KubernetesConfigMapService {
this.deleteAsync = this.deleteAsync.bind(this);
}
getAccess(namespace, name) {
return this.$async(async () => {
try {
const params = new KubernetesCommonParams();
params.id = name;
const raw = await this.KubernetesConfigMaps(namespace).get(params).$promise;
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(raw);
} catch (err) {
if (err.status === 404) {
return new KubernetesPortainerAccessConfigMap();
}
throw new PortainerError('Unable to retrieve Portainer accesses', err);
}
});
}
createAccess(config) {
return this.$async(async () => {
try {
const payload = KubernetesConfigMapConverter.createAccessPayload(config);
const params = {};
const namespace = payload.metadata.namespace;
const data = await this.KubernetesConfigMaps(namespace).create(params, payload).$promise;
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(data);
} catch (err) {
throw new PortainerError('Unable to create Portainer accesses', err);
}
});
}
updateAccess(config) {
return this.$async(async () => {
try {
if (!config.Id) {
return await this.createAccess(config);
}
const payload = KubernetesConfigMapConverter.updateAccessPayload(config);
const params = new KubernetesCommonParams();
params.id = payload.metadata.name;
const namespace = payload.metadata.namespace;
const data = await this.KubernetesConfigMaps(namespace).update(params, payload).$promise;
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(data);
} catch (err) {
throw new PortainerError('Unable to update Portainer accesses', err);
}
});
}
/**
* GET
*/
@@ -29,12 +78,12 @@ class KubernetesConfigMapService {
this.KubernetesConfigMaps(namespace).getYaml(params).$promise,
]);
if (_.get(rawPromise, 'reason.status') == 404 && _.get(yamlPromise, 'reason.status') == 404) {
if (_.get(rawPromise, 'reason.status') === 404 && _.get(yamlPromise, 'reason.status') === 404) {
return KubernetesConfigMapConverter.defaultConfigMap(namespace, name);
}
// Saving binary data to 'data' field in configMap Object is not allowed by kubernetes and getYaml() may get
// an error. We should keep binary data to 'binaryData' field instead of 'data'. Before that, we
// Saving binary data to 'data' field in configMap Object is not allowed by kubernetes and getYaml() may get
// an error. We should keep binary data to 'binaryData' field instead of 'data'. Before that, we
// use response from get() and ignore 500 error as a workaround.
if (rawPromise.value) {
return KubernetesConfigMapConverter.apiToConfigMap(rawPromise.value, yamlPromise.value);

View File

@@ -1299,7 +1299,7 @@
ng-min="1"
ng-max="65535"
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingLoadBalancerPort()"
ng-change="ctrl.onChangePortMappingLoadBalancer()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
</div>

View File

@@ -409,13 +409,14 @@ class KubernetesCreateApplicationController {
}
onChangePortProtocol(index) {
this.onChangePortMappingContainerPort();
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
const newPorts = _.filter(this.formValues.PublishedPorts, { IsNew: true });
_.forEach(newPorts, (port) => {
port.Protocol = index ? this.formValues.PublishedPorts[index].Protocol : newPorts[0].Protocol;
});
this.onChangePortMappingLoadBalancer();
}
this.onChangePortMappingContainerPort();
}
/* #endregion */

View File

@@ -7,8 +7,6 @@
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div ng-if="ctrl.state.isAdmin" ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
@@ -57,9 +55,7 @@
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
<td>
<div>Resource reservations</div>
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small">
per instance
</div>
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small"> per instance </div>
</td>
<td>
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>

View File

@@ -1,5 +1,3 @@
require('../../../templates/advancedDeploymentPanel.html');
import angular from 'angular';
import * as _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
@@ -103,7 +101,6 @@ class KubernetesApplicationController {
Notifications,
LocalStorage,
ModalService,
Authentication,
KubernetesApplicationService,
KubernetesEventService,
KubernetesStackService,
@@ -117,7 +114,6 @@ class KubernetesApplicationController {
this.Notifications = Notifications;
this.LocalStorage = LocalStorage;
this.ModalService = ModalService;
this.Authentication = Authentication;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService;
@@ -339,7 +335,6 @@ class KubernetesApplicationController {
placementWarning: false,
expandedNote: false,
useIngress: false,
isAdmin: this.Authentication.isAdmin(),
};
this.state.activeTab = this.LocalStorage.getActiveTab('application');

View File

@@ -115,8 +115,12 @@
<a href="https://kubernetes.io/docs/concepts/configuration/secret/#secret-types" target="_blank">official documentation</a>.
</div>
</div>
<kubernetes-configuration-data ng-if="ctrl.formValues" form-values="ctrl.formValues" is-valid="ctrl.state.isDataValid"></kubernetes-configuration-data>
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
is-valid="ctrl.state.isDataValid"
is-creation="true"
></kubernetes-configuration-data>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">

View File

@@ -1,7 +1,8 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
class KubernetesCreateConfigurationController {
/* @ngInject */
@@ -41,6 +42,9 @@ class KubernetesCreateConfigurationController {
try {
this.state.actionInProgress = true;
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
if (!this.formValues.IsSimple) {
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
}
await this.KubernetesConfigurationService.create(this.formValues);
this.Notifications.success('Configuration succesfully created');
this.$state.go('kubernetes.configurations');
@@ -76,7 +80,7 @@ class KubernetesCreateConfigurationController {
};
this.formValues = new KubernetesConfigurationFormValues();
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
try {
const resourcePools = await this.KubernetesResourcePoolService.get();

View File

@@ -77,7 +77,12 @@
<rd-widget>
<rd-widget-body>
<form ng-if="!ctrl.isSystemNamespace()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
<kubernetes-configuration-data ng-if="ctrl.formValues" form-values="ctrl.formValues" is-valid="ctrl.state.isDataValid"></kubernetes-configuration-data>
<kubernetes-configuration-data
ng-if="ctrl.formValues"
form-values="ctrl.formValues"
is-valid="ctrl.state.isDataValid"
is-creation="false"
></kubernetes-configuration-data>
<!-- actions -->
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">

View File

@@ -1,7 +1,8 @@
import angular from 'angular';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import _ from 'lodash-es';
@@ -14,6 +15,8 @@ class KubernetesConfigurationController {
Notifications,
LocalStorage,
KubernetesConfigurationService,
KubernetesConfigMapService,
KubernetesSecretService,
KubernetesResourcePoolService,
ModalService,
KubernetesApplicationService,
@@ -32,6 +35,8 @@ class KubernetesConfigurationController {
this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesSecretService = KubernetesSecretService;
this.onInit = this.onInit.bind(this);
this.getConfigurationAsync = this.getConfigurationAsync.bind(this);
@@ -126,7 +131,18 @@ class KubernetesConfigurationController {
this.state.configurationLoading = true;
const name = this.$transition$.params().name;
const namespace = this.$transition$.params().namespace;
this.configuration = await this.KubernetesConfigurationService.get(namespace, name);
const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]);
if (secret.status === 'fulfilled') {
this.configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value);
this.formValues.Data = secret.value.Data;
} else {
this.configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value);
this.formValues.Data = configMap.value.Data;
}
this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace);
this.formValues.Id = this.configuration.Id;
this.formValues.Name = this.configuration.Name;
this.formValues.Type = this.configuration.Type;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
} finally {
@@ -211,20 +227,6 @@ class KubernetesConfigurationController {
await this.getConfiguration();
await this.getApplications(this.configuration.Namespace);
await this.getEvents(this.configuration.Namespace);
this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace);
this.formValues.Id = this.configuration.Id;
this.formValues.Name = this.configuration.Name;
this.formValues.Type = this.configuration.Type;
this.formValues.Data = _.map(this.configuration.Data, (value, key) => {
if (this.configuration.Type === KubernetesConfigurationTypes.SECRET) {
value = atob(value);
}
this.formValues.DataYaml += key + ': ' + value + '\n';
const entry = new KubernetesConfigurationFormValuesDataEntry();
entry.Key = key;
entry.Value = value;
return entry;
});
await this.getConfigurations();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');

View File

@@ -52,7 +52,7 @@ class KubernetesResourcePoolAccessController {
let [endpoint, pool, configMap] = await Promise.all([
this.EndpointService.endpoint(this.endpointId),
this.KubernetesResourcePoolService.get(name),
this.KubernetesConfigMapService.get(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
]);
const group = await this.GroupService.group(endpoint.GroupId);
const roles = [];
@@ -96,7 +96,7 @@ class KubernetesResourcePoolAccessController {
this.state.actionInProgress = true;
const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput);
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
await this.KubernetesConfigMapService.update(accessConfigMap);
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
this.Notifications.success('Access successfully created');
this.$state.reload();
} catch (err) {
@@ -116,7 +116,7 @@ class KubernetesResourcePoolAccessController {
this.state.actionInProgress = true;
const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems);
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
await this.KubernetesConfigMapService.update(accessConfigMap);
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
this.Notifications.success('Access successfully removed');
this.$state.reload();
} catch (err) {

View File

@@ -14,7 +14,8 @@ angular
FormValidator,
ResourceControlService,
FormHelper,
CustomTemplateService
CustomTemplateService,
EndpointProvider
) {
$scope.formValues = {
Name: '',
@@ -177,6 +178,13 @@ angular
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve Custom Templates');
}
try {
const endpoint = EndpointProvider.currentEndpoint();
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
}
}
initView();

View File

@@ -20,10 +20,15 @@
<span class="col-sm-12 text-muted small" ng-if="state.StackType === 1">
This stack will be deployed using the equivalent of the <code>docker stack deploy</code> command.
</span>
<span class="col-sm-12 text-muted small" ng-if="state.StackType === 2">
This stack will be deployed using the equivalent of <code>docker-compose</code>. Only Compose file format version <b>2</b> is supported at the moment. <br /><br />
<div class="col-sm-12 text-muted small" ng-if="state.StackType === 2 && composeSyntaxMaxVersion == 2">
<div style="margin-bottom: 7px;">
This stack will be deployed using the equivalent of <code>docker-compose</code>. Only Compose file format version <b>2</b> is supported at the moment.
</div>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Note: Due to a limitation of libcompose, the name of the stack will be standardized to remove all special characters and uppercase letters.
</div>
<span class="col-sm-12 text-muted small" ng-if="state.StackType === 2 && composeSyntaxMaxVersion > 2">
This stack will be deployed using <code>docker-compose</code>.
</span>
</div>
<!-- build-method -->

View File

@@ -94,6 +94,12 @@
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px;">
<div class="form-group">
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px;" ng-if="stackType == 2 && composeSyntaxMaxVersion == 2">
This stack will be deployed using the equivalent of <code>docker-compose</code>. Only Compose file format version <b>2</b> is supported at the moment.
</span>
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px;" ng-if="stackType == 2 && composeSyntaxMaxVersion > 2">
This stack will be deployed using <code>docker-compose</code>.
</span>
<span class="col-sm-12 text-muted small">
You can get more information about Compose file format in the <a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</span>

View File

@@ -359,7 +359,7 @@ angular.module('portainer.app').controller('StackController', [
});
}
function initView() {
async function initView() {
var stackName = $transition$.params().name;
$scope.stackName = stackName;
var external = $transition$.params().external;
@@ -372,6 +372,15 @@ angular.module('portainer.app').controller('StackController', [
var stackId = $transition$.params().id;
loadStack(stackId);
}
try {
const endpoint = EndpointProvider.currentEndpoint();
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve the ComposeSyntaxMaxVersion');
}
$scope.stackType = $transition$.params().type;
}
initView();

View File

@@ -1,24 +0,0 @@
param (
[string]$platform,
[string]$arch
)
$ErrorActionPreference = "Stop";
$binary = "portainer.exe"
$go_path = "$($(Get-ITEM -Path env:AGENT_TEMPDIRECTORY).Value)\go"
Set-Item env:GOPATH "$go_path"
New-Item -Name dist -Path "." -ItemType Directory -Force | Out-Null
New-Item -Name portainer -Path "$go_path\src\github.com\portainer" -ItemType Directory -Force | Out-Null
Copy-Item -Path "api" -Destination "$go_path\src\github.com\portainer\portainer\api" -Recurse -Force
Set-Location -Path "api\cmd\portainer"
go get -t -d -v ./...
## go build -v
& cmd /c 'go build -v 2>&1'
Copy-Item -Path "portainer.exe" -Destination "$($env:BUILD_SOURCESDIRECTORY)\dist\portainer.exe" -Force

View File

@@ -1,3 +1,8 @@
#!/usr/bin/env bash
PLATFORM=$1
ARCH=$2
export GOPATH="/tmp/go"
binary="portainer"
@@ -10,6 +15,10 @@ cp -R api ${GOPATH}/src/github.com/portainer/portainer/api
cd 'api/cmd/portainer'
go get -t -d -v ./...
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
GOOS=${PLATFORM} GOARCH=${ARCH} CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
if [ "${PLATFORM}" == 'windows' ]; then
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/${binary}.exe" "$BUILD_SOURCESDIRECTORY/dist/portainer.exe"
else
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
fi

View File

@@ -1,15 +0,0 @@
param (
[string]$docker_version
)
$ErrorActionPreference = "Stop";
$ProgressPreference = "SilentlyContinue";
New-Item -Path "docker-binary" -ItemType Directory | Out-Null
$download_folder = "docker-binary"
Invoke-WebRequest -O "$($download_folder)/docker-binaries.zip" "https://dockermsft.azureedge.net/dockercontainer/docker-$($docker_version).zip"
Expand-Archive -Path "$($download_folder)/docker-binaries.zip" -DestinationPath "$($download_folder)"
Move-Item -Path "$($download_folder)/docker/docker.exe" -Destination "dist"
Move-Item -Path "$($download_folder)/docker/*.dll" -Destination "dist"

View File

@@ -10,9 +10,10 @@ rm -rf "${DOWNLOAD_FOLDER}"
mkdir -pv "${DOWNLOAD_FOLDER}"
if [ "${PLATFORM}" == 'win' ]; then
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.zip" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.zip"
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.zip" "https://dockermsft.azureedge.net/dockercontainer/docker-${DOCKER_VERSION}.zip"
unzip "${DOWNLOAD_FOLDER}/docker-binaries.zip" -d "${DOWNLOAD_FOLDER}"
mv "${DOWNLOAD_FOLDER}/docker/docker.exe" dist/
mv ${DOWNLOAD_FOLDER}/docker/*.dll dist/
else
wget -O "${DOWNLOAD_FOLDER}/docker-binaries.tgz" "https://download.docker.com/${PLATFORM}/static/stable/${ARCH}/docker-${DOCKER_VERSION}.tgz"
tar -xf "${DOWNLOAD_FOLDER}/docker-binaries.tgz" -C "${DOWNLOAD_FOLDER}"

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
PLATFORM=$1
ARCH=$2
DOCKER_COMPOSE_VERSION=$3
if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" == 'amd64' ]; then
wget -O "dist/docker-compose" "https://github.com/portainer/docker-compose-linux-amd64-static-binary/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose"
chmod +x "dist/docker-compose"
elif [ "${PLATFORM}" == 'mac' ]; then
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Darwin-x86_64"
chmod +x "dist/docker-compose"
elif [ "${PLATFORM}" == 'win' ]; then
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-Windows-x86_64.exe"
chmod +x "dist/docker-compose.exe"
fi
exit 0

View File

@@ -1,8 +0,0 @@
param (
[string]$kompose_version
)
$ErrorActionPreference = "Stop";
$ProgressPreference = "SilentlyContinue";
Invoke-WebRequest -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/$($kompose_version)/kompose-windows-amd64.exe"

View File

@@ -4,7 +4,12 @@ PLATFORM=$1
ARCH=$2
KOMPOSE_VERSION=$3
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}"
chmod +x "dist/kompose"
if [ "${PLATFORM}" == 'linux' ]; then
wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}"
chmod +x "dist/kompose"
elif [ "${PLATFORM}" == 'windows' ]; then
wget -O "dist/kompose.exe" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-windows-amd64.exe"
chmod +x "dist/kompose.exe"
fi
exit 0

View File

@@ -1,8 +0,0 @@
param (
[string]$kubectl_version
)
$ErrorActionPreference = "Stop";
$ProgressPreference = "SilentlyContinue";
Invoke-WebRequest -O "dist/kubectl.exe" "https://storage.googleapis.com/kubernetes-release/release/$($kubectl_version)/bin/windows/amd64/kubectl.exe"

View File

@@ -4,7 +4,12 @@ PLATFORM=$1
ARCH=$2
KUBECTL_VERSION=$3
wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl"
chmod +x "dist/kubectl"
if [ "${PLATFORM}" == 'linux' ]; then
wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl"
chmod +x "dist/kubectl"
elif [ "${PLATFORM}" == 'windows' ]; then
wget -O "dist/kubectl.exe" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/windows/amd64/kubectl.exe"
chmod +x "dist/kubectl.exe"
fi
exit 0

24
build/windows/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
ARG OSVERSION
FROM --platform=linux/amd64 gcr.io/k8s-staging-e2e-test-images/windows-servercore-cache:1.0-linux-amd64-${OSVERSION} as core
FROM --platform=linux/amd64 alpine:3.13.0 as downloader
ENV GIT_VERSION 2.30.0
ENV GIT_PATCH_VERSION 2
RUN mkdir mingit/ \
&& wget https://github.com/git-for-windows/git/releases/download/v$GIT_VERSION.windows.$GIT_PATCH_VERSION/MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip \
&& unzip MinGit-$GIT_VERSION.$GIT_PATCH_VERSION-busybox-64-bit.zip -d mingit/
FROM mcr.microsoft.com/windows/nanoserver:${OSVERSION}
ENV PATH "C:\mingit\cmd;C:\Windows\system32;C:\Windows;"
COPY --from=downloader /mingit mingit/
COPY --from=core /Windows/System32/netapi32.dll /Windows/System32/netapi32.dll
USER ContainerAdministrator
COPY dist /
EXPOSE 9000
EXPOSE 8000
ENTRYPOINT ["/portainer.exe"]

View File

@@ -1,15 +0,0 @@
FROM mcr.microsoft.com/windows/servercore:ltsc2019 as core
FROM mcr.microsoft.com/windows/nanoserver:1809-amd64
USER ContainerAdministrator
COPY --from=core /windows/system32/netapi32.dll /windows/system32/netapi32.dll
COPY dist /
WORKDIR /
EXPOSE 9000
EXPOSE 8000
ENTRYPOINT ["/portainer.exe"]

View File

@@ -19,6 +19,8 @@ module.exports = function (grunt) {
binaries: {
dockerLinuxVersion: '19.03.13',
dockerWindowsVersion: '19-03-12',
dockerLinuxComposeVersion: '1.27.4',
dockerWindowsComposeVersion: '1.28.0',
komposeVersion: 'v1.22.0',
kubectlVersion: 'v1.18.0',
},
@@ -37,6 +39,7 @@ module.exports = function (grunt) {
grunt.registerTask('build:server', [
'shell:build_binary:linux:' + arch,
'shell:download_docker_binary:linux:' + arch,
'shell:download_docker_compose_binary:linux:' + arch,
'shell:download_kompose_binary:linux:' + arch,
'shell:download_kubectl_binary:linux:' + arch,
]);
@@ -63,6 +66,7 @@ module.exports = function (grunt) {
'copy:assets',
'shell:build_binary:' + p + ':' + a,
'shell:download_docker_binary:' + p + ':' + a,
'shell:download_docker_compose_binary:' + p + ':' + a,
'shell:download_kompose_binary:' + p + ':' + a,
'shell:download_kubectl_binary:' + p + ':' + a,
'webpack:prod',
@@ -77,6 +81,7 @@ module.exports = function (grunt) {
'copy:assets',
'shell:build_binary_azuredevops:' + p + ':' + a,
'shell:download_docker_binary:' + p + ':' + a,
'shell:download_docker_compose_binary:' + p + ':' + a,
'shell:download_kompose_binary:' + p + ':' + a,
'shell:download_kubectl_binary:' + p + ':' + a,
'webpack:prod',
@@ -138,6 +143,7 @@ gruntfile_cfg.shell = {
download_docker_binary: { command: shell_download_docker_binary },
download_kompose_binary: { command: shell_download_kompose_binary },
download_kubectl_binary: { command: shell_download_kubectl_binary },
download_docker_compose_binary: { command: shell_download_docker_compose_binary },
run_container: { command: shell_run_container },
run_localserver: { command: shell_run_localserver, options: { async: true } },
install_yarndeps: { command: shell_install_yarndeps },
@@ -159,11 +165,7 @@ function shell_build_binary(p, a) {
}
function shell_build_binary_azuredevops(p, a) {
if (p === 'linux') {
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
} else {
return 'powershell -Command ".\\build\\build_binary_azuredevops.ps1 -platform ' + p + ' -arch ' + a + '"';
}
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
}
function shell_run_container() {
@@ -171,7 +173,7 @@ function shell_run_container() {
'docker rm -f portainer',
'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' +
portainer_data +
':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer',
':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer',
].join(';');
}
@@ -189,51 +191,52 @@ function shell_download_docker_binary(p, a) {
var ip = ps[p] === undefined ? p : ps[p];
var ia = as[a] === undefined ? a : as[a];
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>';
return [
'if [ -f dist/docker ] || [ -f dist/docker.exe ]; then',
'echo "docker binary exists";',
'else',
'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
'fi',
].join(' ');
}
if (p === 'linux' || p === 'mac') {
return ['if [ -f dist/docker ]; then', 'echo "docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Test-Path -Path "dist/docker.exe") {',
'Write-Host "Skipping download, Docker binary exists"',
'return',
'} else {',
'& ".\\build\\download_docker_binary.ps1" -docker_version ' + binaryVersion + '',
'}}"',
].join(' ');
}
function shell_download_docker_compose_binary(p, a) {
var ps = { windows: 'win', darwin: 'mac' };
var as = { arm: 'armhf', arm64: 'aarch64' };
var ip = ps[p] || p;
var ia = as[a] || a;
var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsComposeVersion %>' : '<%= binaries.dockerLinuxComposeVersion %>';
return [
'if [ -f dist/docker-compose ] || [ -f dist/docker-compose.exe ]; then',
'echo "Docker Compose binary exists";',
'else',
'build/download_docker_compose_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
'fi',
].join(' ');
}
function shell_download_kompose_binary(p, a) {
var binaryVersion = '<%= binaries.komposeVersion %>';
if (p === 'linux' || p === 'darwin') {
return ['if [ -f dist/kompose ]; then', 'echo "kompose binary exists";', 'else', 'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Test-Path -Path "dist/kompose.exe") {',
'Write-Host "Skipping download, Kompose binary exists"',
'return',
'} else {',
'& ".\\build\\download_kompose_binary.ps1" -kompose_version ' + binaryVersion + '',
'}}"',
].join(' ');
}
return [
'if [ -f dist/kompose ] || [ -f dist/kompose.exe ]; then',
'echo "kompose binary exists";',
'else',
'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';',
'fi',
].join(' ');
}
function shell_download_kubectl_binary(p, a) {
var binaryVersion = '<%= binaries.kubectlVersion %>';
if (p === 'linux' || p === 'darwin') {
return ['if [ -f dist/kubectl ]; then', 'echo "kubectl binary exists";', 'else', 'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' ');
} else {
return [
'powershell -Command "& {if (Test-Path -Path "dist/kubectl.exe") {',
'Write-Host "Skipping download, Kubectl binary exists"',
'return',
'} else {',
'& ".\\build\\download_kubectl_binary.ps1" -kubectl_version ' + binaryVersion + '',
'}}"',
].join(' ');
}
return [
'if [ -f dist/kubectl ] || [ -f dist/kubectl.exe ]; then',
'echo "kubectl binary exists";',
'else',
'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';',
'fi',
].join(' ');
}

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.0.0",
"version": "2.1.0",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -21,7 +21,8 @@
"build:server": "grunt clean:server && grunt build:server",
"build:client": "grunt clean:client && grunt build:client",
"clean": "grunt clean:all",
"start": "grunt clean:all && grunt start",
"start": "grunt start",
"start:clean": "grunt clean:all && grunt start",
"start:localserver": "grunt start:localserver",
"start:server": "grunt clean:server && grunt start:server",
"start:client": "grunt clean:client && grunt start:client",
@@ -72,12 +73,14 @@
"babel-plugin-angularjs-annotate": "^0.10.0",
"bootbox": "^5.4.0",
"bootstrap": "^3.4.0",
"chardet": "^1.3.0",
"chart.js": "~2.6.0",
"codemirror": "~5.30.0",
"fast-json-patch": "^3.0.0-1",
"filesize": "~3.3.0",
"filesize-parser": "^1.5.0",
"jquery": "^3.5.1",
"js-base64": "^3.6.0",
"js-yaml": "^3.14.0",
"lodash-es": "^4.17.15",
"moment": "^2.21.0",

View File

@@ -1352,7 +1352,6 @@ angular-moment-picker@^0.10.2:
dependencies:
angular-mocks "1.6.1"
angular-sanitize "1.6.1"
lodash-es "^4.17.15"
angular-resource@1.8.0:
version "1.8.0"
@@ -2411,6 +2410,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chardet@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-1.3.0.tgz#a56ed2d9e4517a7128721340a0cb9a10a8fac238"
integrity sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw==
chart.js@~2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.6.0.tgz#308f9a4b0bfed5a154c14f5deb1d9470d22abe71"
@@ -6586,6 +6590,11 @@ js-base64@^2.1.9:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.1.tgz#c328374225d2e65569791ded73c258e2c59334c7"
integrity sha512-G5x2saUTupU9D/xBY9snJs3TxvwX8EkpLFiYlPpDt/VmMHOXprnSU1nxiTmFbijCX4BLF/cMRIfAcC5BiMYgFQ==
js-base64@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.6.0.tgz#773e1de628f4f298d65a7e9842c50244751f5756"
integrity sha512-wVdUBYQeY2gY73RIlPrysvpYx+2vheGo8Y1SNQv/BzHToWpAZzJU7Z6uheKMAe+GLSBig5/Ps2nxg/8tRB73xg==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"