Compare commits
31 Commits
feat/cd-18
...
fix-ce395-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59d4b90a0a | ||
|
|
81de55fedd | ||
|
|
84827b8782 | ||
|
|
a71e71f481 | ||
|
|
83f4c5ec0b | ||
|
|
41308d570d | ||
|
|
46ff8a01bc | ||
|
|
2b257d2785 | ||
|
|
da41dbb79a | ||
|
|
68d42617f2 | ||
|
|
8323e22309 | ||
|
|
20d4341170 | ||
|
|
832cafc933 | ||
|
|
f3c537ac2c | ||
|
|
958baf6283 | ||
|
|
08e392378e | ||
|
|
a2d9734b8b | ||
|
|
15aed9fc6f | ||
|
|
121d33538d | ||
|
|
7a03351df8 | ||
|
|
0c2987893d | ||
|
|
d1eddaa188 | ||
|
|
d336ada3c2 | ||
|
|
839198fbff | ||
|
|
486ffa5bbd | ||
|
|
4cd468ce21 | ||
|
|
cbd7fdc62e | ||
|
|
b9fe8009dd | ||
|
|
6a504e7134 | ||
|
|
51ba0876a5 | ||
|
|
769e6a4c6c |
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Custom.md
vendored
4
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask us a question about Portainer usage or deployment
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature/enhancement that should be added in Portainer
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
@@ -34,9 +34,9 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
||||
|
||||
## Getting help
|
||||
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
|
||||
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- FAQ: https://documentation.portainer.io
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
k8s "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/libcompose"
|
||||
@@ -73,7 +73,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager proxy.Manager) portainer.ComposeStackManager {
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
|
||||
if composeWrapper != nil {
|
||||
return composeWrapper
|
||||
@@ -96,6 +96,10 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if settings.UserSessionTimeout == "" {
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -133,7 +137,7 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
|
||||
|
||||
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
|
||||
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
|
||||
kubernetesSnapshotter := k8s.NewSnapshotter(kubernetesClientFactory)
|
||||
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
|
||||
|
||||
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter)
|
||||
if err != nil {
|
||||
@@ -387,10 +391,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, *proxyManager)
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
||||
|
||||
@@ -471,7 +475,7 @@ func main() {
|
||||
LDAPService: ldapService,
|
||||
OAuthService: oauthService,
|
||||
GitService: gitService,
|
||||
ProxyManager: *proxyManager,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
@@ -18,11 +16,11 @@ import (
|
||||
// ComposeWrapper is a wrapper for docker-compose binary
|
||||
type ComposeWrapper struct {
|
||||
binaryPath string
|
||||
proxyManager proxy.Manager
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeWrapper(binaryPath string, proxyManager proxy.Manager) *ComposeWrapper {
|
||||
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
|
||||
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
|
||||
return nil
|
||||
}
|
||||
@@ -33,6 +31,11 @@ func NewComposeWrapper(binaryPath string, proxyManager proxy.Manager) *ComposeWr
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -46,6 +49,10 @@ func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoi
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -56,42 +63,17 @@ func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpoint != nil {
|
||||
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
|
||||
|
||||
if endpoint.URL != "" {
|
||||
proxy, err := w.proxyManager.CreateAndRegisterComposeEndpointProxy(endpoint)
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Proxy Server: %v", proxy)
|
||||
server := http.Server{
|
||||
Handler: proxy,
|
||||
}
|
||||
|
||||
shutdownChan := make(chan error, 1)
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
go func() {
|
||||
log.Printf("Starting Proxy server on %s...\n", fmt.Sprintf("http://localhost:%d", port))
|
||||
// details are the same as for the `server.ListenAndServe()` section above
|
||||
err := server.Serve(listener)
|
||||
log.Printf("Proxy Server exited with '%v' error\n", err)
|
||||
|
||||
if err != http.ErrServerClosed {
|
||||
log.Printf("Put '%v' error returned by Proxy Server to shutdown channel\n", err)
|
||||
shutdownChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
defer server.Close()
|
||||
|
||||
options = append(options, "-H", fmt.Sprintf("http://localhost:%d", port))
|
||||
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
defer proxy.Close()
|
||||
|
||||
// options = addTLSConnectionOptions(options, endpoint)
|
||||
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
|
||||
}
|
||||
|
||||
args := append(options, command...)
|
||||
|
||||
@@ -119,46 +101,6 @@ func setComposeFile(stack *portainer.Stack) []string {
|
||||
return options
|
||||
}
|
||||
|
||||
func addTLSConnectionOptions(options []string, endpoint *portainer.Endpoint) []string {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
options = append(options, "--tls")
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
options = append(options, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
options = append(options, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func addConnectionOptions(options []string, endpoint *portainer.Endpoint) []string {
|
||||
if endpoint == nil {
|
||||
return options
|
||||
}
|
||||
|
||||
if endpoint.URL != "" {
|
||||
options = append(options, "-H", endpoint.URL)
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
options = append(options, "--tls")
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
options = append(options, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
options = append(options, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
|
||||
if stack == nil || stack.Name == "" {
|
||||
return options
|
||||
|
||||
@@ -42,7 +42,7 @@ func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w := NewComposeWrapper("")
|
||||
w := NewComposeWrapper("", nil)
|
||||
|
||||
err := w.Up(stack, endpoint)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,53 +85,6 @@ func Test_addProjectNameOption(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addConnectionOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint *portainer.Endpoint
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "should not add options if endpoint is missing",
|
||||
endpoint: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add hostname if endpoint doesn't have url",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should add hostname name option if endpoint has url",
|
||||
endpoint: &portainer.Endpoint{
|
||||
URL: "host:port",
|
||||
},
|
||||
expected: []string{"-H", "host:port"},
|
||||
},
|
||||
{
|
||||
name: "should add tls options if endpoint has tls setup",
|
||||
endpoint: &portainer.Endpoint{
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: true,
|
||||
TLSSkipVerify: false,
|
||||
TLSCACertPath: "ca-cert-path",
|
||||
TLSCertPath: "cert-path",
|
||||
TLSKeyPath: "key-path",
|
||||
},
|
||||
},
|
||||
expected: []string{"--tls", "--tlsverify", "--tlscacert", "ca-cert-path", "--tlscert", "cert-path", "--tlskey", "key-path"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := []string{"-a", "b"}
|
||||
result := addConnectionOptions(options, tt.endpoint)
|
||||
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addEnvFileOption(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
} else {
|
||||
args = append(args, "--tlscacert", "''")
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
|
||||
@@ -15,7 +15,6 @@ require (
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
@@ -23,22 +22,16 @@ require (
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.8
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
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/sirupsen/logrus v1.4.2 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
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
|
||||
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f // indirect
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24 // indirect
|
||||
google.golang.org/grpc v1.26.0 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
k8s.io/api v0.17.2
|
||||
|
||||
42
api/go.sum
42
api/go.sum
@@ -40,7 +40,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/containerd v1.3.1 h1:LdbWxLhkAIxGO7h3mATHkyav06WuDs/yTWxIljJOTks=
|
||||
github.com/containerd/containerd v1.3.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
@@ -76,13 +75,10 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
@@ -112,9 +108,6 @@ github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -148,6 +141,7 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -182,8 +176,6 @@ github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -215,8 +207,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
@@ -231,8 +225,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -249,8 +241,6 @@ github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQ
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
@@ -264,8 +254,6 @@ github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -285,7 +273,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
||||
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 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE=
|
||||
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
@@ -301,6 +288,7 @@ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
|
||||
@@ -324,6 +312,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
|
||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -347,15 +337,12 @@ golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f h1:6Sc1XOXTulBN6imkqo6XoAXDEzoQ4/ro6xy7Vn8+rOM=
|
||||
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -383,15 +370,9 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24 h1:wDju+RU97qa0FZT0QnZDg9Uc2dH0Ql513kFvHocz+WM=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -421,18 +402,29 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s=
|
||||
k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU=
|
||||
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
|
||||
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
|
||||
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI=
|
||||
k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
|
||||
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
|
||||
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
|
||||
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ=
|
||||
k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw=
|
||||
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
|
||||
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
|
||||
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
|
||||
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,7 +7,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"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
registry.Username = *payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != nil {
|
||||
if payload.Password != nil && *payload.Password != "" {
|
||||
registry.Password = *payload.Password
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -60,13 +61,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -89,6 +91,8 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -146,13 +150,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -185,6 +190,8 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -242,13 +249,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -271,6 +279,8 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -55,14 +56,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -85,6 +87,8 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -145,14 +149,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -185,6 +190,8 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -249,14 +256,15 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -279,6 +287,8 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
|
||||
@@ -78,6 +78,17 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdmin(userID portainer.UserID) (bool, error) {
|
||||
user, err := handler.DataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
|
||||
@@ -183,9 +183,20 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
}
|
||||
|
||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||
resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
var resourceControl *portainer.ResourceControl
|
||||
|
||||
err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
isAdmin, err := handler.userIsAdmin(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
|
||||
} else {
|
||||
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
}
|
||||
|
||||
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
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"
|
||||
@@ -135,6 +136,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
@@ -163,6 +167,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
|
||||
@@ -2,27 +2,28 @@ package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
|
||||
)
|
||||
|
||||
func (factory *ProxyFactory) NewDockerComposeProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
return factory.newDockerComposeLocalProxy(endpoint)
|
||||
}
|
||||
|
||||
return factory.newDockerComposeAgentProxy(endpoint)
|
||||
// ProxyServer provide an extedned proxy with a local server to forward requests
|
||||
type ProxyServer struct {
|
||||
server *http.Server
|
||||
Port int
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newDockerComposeLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
return &ProxyServer{
|
||||
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
@@ -43,69 +44,45 @@ func (factory *ProxyFactory) newDockerComposeLocalProxy(endpoint *portainer.Endp
|
||||
endpointURL.Scheme = "https"
|
||||
}
|
||||
|
||||
dockerComposeTransport := dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
proxy.Transport = dockerComposeTransport
|
||||
return proxy, nil
|
||||
|
||||
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||
|
||||
proxyServer := &ProxyServer{
|
||||
&http.Server{
|
||||
Handler: proxy,
|
||||
},
|
||||
0,
|
||||
}
|
||||
|
||||
return proxyServer, proxyServer.start()
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newDockerComposeAgentProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
func (proxy *ProxyServer) start() error {
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
endpointURL.Scheme = "http"
|
||||
httpTransport := &http.Transport{}
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
httpTransport.TLSClientConfig = config
|
||||
endpointURL.Scheme = "https"
|
||||
}
|
||||
|
||||
dockerComposeTransport := dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
proxy.Transport = dockerComposeTransport
|
||||
return proxy, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// // ServeHTTP is the http.Handler interface implementation
|
||||
// // for a local (Unix socket or Windows named pipe) Docker proxy.
|
||||
// func (proxy *dockerLocalProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// // Force URL/domain to http/unixsocket to be able to
|
||||
// // use http.transport RoundTrip to do the requests via the socket
|
||||
// r.URL.Scheme = "http"
|
||||
// r.URL.Host = "unixsocket"
|
||||
|
||||
// res, err := proxy.transport.ProxyDockerRequest(r)
|
||||
// if err != nil {
|
||||
// code := http.StatusInternalServerError
|
||||
// if res != nil && res.StatusCode != 0 {
|
||||
// code = res.StatusCode
|
||||
// }
|
||||
|
||||
// httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err)
|
||||
// return
|
||||
// }
|
||||
// defer res.Body.Close()
|
||||
|
||||
// for k, vv := range res.Header {
|
||||
// for _, v := range vv {
|
||||
// w.Header().Add(k, v)
|
||||
// }
|
||||
// }
|
||||
|
||||
// w.WriteHeader(res.StatusCode)
|
||||
|
||||
// if _, err := io.Copy(w, res.Body); err != nil {
|
||||
// log.Printf("proxy error: %s\n", err)
|
||||
// }
|
||||
// }
|
||||
// Close shuts down the server
|
||||
func (proxy *ProxyServer) Close() {
|
||||
if proxy.server != nil {
|
||||
proxy.server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,17 @@ import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
)
|
||||
|
||||
type (
|
||||
LocalTransport struct {
|
||||
httpTransport *http.Transport
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
EdgeTransport struct {
|
||||
httpTransport *http.Transport
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
)
|
||||
|
||||
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
|
||||
func NewLocalTransport() (*LocalTransport, error) {
|
||||
config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := &LocalTransport{
|
||||
httpTransport: &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
},
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *LocalTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
|
||||
return transport.httpTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
// 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{
|
||||
@@ -71,28 +38,3 @@ func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Respons
|
||||
|
||||
return transport.httpTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
|
||||
func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID) *EdgeTransport {
|
||||
transport := &EdgeTransport{
|
||||
httpTransport: &http.Transport{},
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
endpointIdentifier: endpointIdentifier,
|
||||
}
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *EdgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
|
||||
response, err := transport.httpTransport.RoundTrip(request)
|
||||
|
||||
if err == nil {
|
||||
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier)
|
||||
} else {
|
||||
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier)
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
@@ -43,25 +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
|
||||
}
|
||||
|
||||
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||
// 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) CreateAndRegisterComposeEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewDockerComposeProxy(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
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
|
||||
}
|
||||
@@ -73,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ type Server struct {
|
||||
LDAPService portainer.LDAPService
|
||||
OAuthService portainer.OAuthService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
ProxyManager proxy.Manager
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
@@ -84,7 +84,7 @@ func (server *Server) Start() error {
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.ProxyManager = &server.ProxyManager
|
||||
authHandler.ProxyManager = server.ProxyManager
|
||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||
authHandler.OAuthService = server.OAuthService
|
||||
|
||||
@@ -118,9 +118,10 @@ func (server *Server) Start() error {
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = &server.ProxyManager
|
||||
endpointHandler.ProxyManager = server.ProxyManager
|
||||
endpointHandler.SnapshotService = server.SnapshotService
|
||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
endpointHandler.ComposeStackManager = server.ComposeStackManager
|
||||
|
||||
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
|
||||
endpointEdgeHandler.DataStore = server.DataStore
|
||||
@@ -132,7 +133,7 @@ func (server *Server) Start() error {
|
||||
|
||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.DataStore = server.DataStore
|
||||
endpointProxyHandler.ProxyManager = &server.ProxyManager
|
||||
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
@@ -142,7 +143,7 @@ func (server *Server) Start() error {
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
registryHandler.DataStore = server.DataStore
|
||||
registryHandler.FileService = server.FileService
|
||||
registryHandler.ProxyManager = &server.ProxyManager
|
||||
registryHandler.ProxyManager = server.ProxyManager
|
||||
|
||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
@@ -6,6 +6,21 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
|
||||
// identifier and type parameters.
|
||||
func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl {
|
||||
return &portainer.ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: []portainer.UserResourceAccess{},
|
||||
TeamAccesses: []portainer.TeamResourceAccess{},
|
||||
AdministratorsOnly: true,
|
||||
Public: false,
|
||||
System: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
|
||||
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
|
||||
func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -554,6 +555,10 @@ type (
|
||||
Env []Pair `json:"Env"`
|
||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||
Status StackStatus `json:"Status"`
|
||||
CreationDate int64
|
||||
CreatedBy string
|
||||
UpdateDate int64
|
||||
UpdatedBy string
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
@@ -774,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
|
||||
}
|
||||
@@ -1122,6 +1128,8 @@ const (
|
||||
APIVersion = "2.0.1"
|
||||
// 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
|
||||
|
||||
@@ -4,100 +4,8 @@
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
<div class="settings">
|
||||
<span
|
||||
class="setting"
|
||||
ng-class="{ 'setting-active': $ctrl.columnVisibility.state.open }"
|
||||
uib-dropdown
|
||||
dropdown-append-to-body
|
||||
auto-close="disabled"
|
||||
is-open="$ctrl.columnVisibility.state.open"
|
||||
>
|
||||
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
Show / Hide Columns
|
||||
</div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_state"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.state.display"
|
||||
/>
|
||||
<label for="col_vis_state" ng-bind="$ctrl.columnVisibility.columns.state.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_actions"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.actions.display"
|
||||
/>
|
||||
<label for="col_vis_actions" ng-bind="$ctrl.columnVisibility.columns.actions.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_stack"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.stack.display"
|
||||
/>
|
||||
<label for="col_vis_stack" ng-bind="$ctrl.columnVisibility.columns.stack.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_image"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.image.display"
|
||||
/>
|
||||
<label for="col_vis_image" ng-bind="$ctrl.columnVisibility.columns.image.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_created"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.created.display"
|
||||
/>
|
||||
<label for="col_vis_created" ng-bind="$ctrl.columnVisibility.columns.created.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.showHostColumn">
|
||||
<input
|
||||
id="col_vis_host"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.host.display"
|
||||
/>
|
||||
<label for="col_vis_host" ng-bind="$ctrl.columnVisibility.columns.host.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_ports"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.ports.display"
|
||||
/>
|
||||
<label for="col_vis_ports" ng-bind="$ctrl.columnVisibility.columns.ports.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_ownership"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.ownership.display"
|
||||
/>
|
||||
<label for="col_vis_ownership" ng-bind="$ctrl.columnVisibility.columns.ownership.label"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
||||
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
|
||||
@@ -36,9 +36,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
};
|
||||
|
||||
this.columnVisibility = {
|
||||
state: {
|
||||
open: false,
|
||||
},
|
||||
columns: {
|
||||
state: {
|
||||
label: 'State',
|
||||
@@ -75,9 +72,11 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
},
|
||||
};
|
||||
|
||||
this.onColumnVisibilityChange = function (columnVisibility) {
|
||||
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
|
||||
};
|
||||
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
|
||||
function onColumnVisibilityChange(columns) {
|
||||
this.columnVisibility.columns = columns;
|
||||
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||
}
|
||||
|
||||
this.onSelectionChanged = function () {
|
||||
this.updateSelectionState();
|
||||
@@ -199,7 +198,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||
if (storedColumnVisibility !== null) {
|
||||
this.columnVisibility = storedColumnVisibility;
|
||||
this.columnVisibility.state.open = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -36,10 +36,13 @@
|
||||
<!-- don't use registry -->
|
||||
<div ng-if="!$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
|
||||
>Image
|
||||
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-left: 15px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When using advanced mode, image and repository <b>must be</b> publicly available.
|
||||
</p>
|
||||
</span>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ export function ImageViewModel(data) {
|
||||
this.Repository = data.Repository;
|
||||
this.Created = data.Created;
|
||||
this.Checked = false;
|
||||
|
||||
this.RepoTags = data.RepoTags;
|
||||
if (!this.RepoTags && data.RepoDigests) {
|
||||
this.RepoTags = [];
|
||||
@@ -21,6 +20,7 @@ export function ImageViewModel(data) {
|
||||
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
this.Labels = data.Labels;
|
||||
}
|
||||
|
||||
export function ImageBuildModel(data) {
|
||||
|
||||
@@ -16,4 +16,5 @@ export function ImageDetailsViewModel(data) {
|
||||
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
|
||||
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
|
||||
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
|
||||
this.Labels = data.ContainerConfig.Labels;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,17 @@
|
||||
<td>Build</td>
|
||||
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!(image.Labels | emptyobject)">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="(k, v) in image.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="image.Author">
|
||||
<td>Author</td>
|
||||
<td>{{ image.Author }}</td>
|
||||
|
||||
@@ -196,7 +196,8 @@
|
||||
<div class="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Restrict external access to the network
|
||||
Isolated network
|
||||
<portainer-tooltip position="bottom" message="An isolated network has no inbound or outbound communications."></portainer-tooltip>
|
||||
</label>
|
||||
<label name="ownership" class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="config.Internal" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html
|
||||
><html lang="en" ng-app="<%= name %>">
|
||||
><html lang="en" ng-app="<%= name %>" ng-strict-di>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Portainer</title>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th style="width: 55px;">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
@@ -142,8 +142,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||
<td colspan="5">
|
||||
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
|
||||
<td></td>
|
||||
<td colspan="4">
|
||||
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })">{{ app.Name }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
|
||||
>external</span
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
|
||||
bindings: {
|
||||
formValues: '=',
|
||||
isValid: '=',
|
||||
isCreation: '=',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import _ from 'lodash-es';
|
||||
import YAML from 'yaml';
|
||||
import { KubernetesConfigMap } 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 {
|
||||
/**
|
||||
@@ -16,7 +16,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;
|
||||
}
|
||||
|
||||
@@ -39,8 +55,16 @@ class KubernetesConfigMapConverter {
|
||||
const res = new KubernetesConfigMapCreatePayload();
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
||||
res.data = data.Data;
|
||||
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||
|
||||
_.forEach(data.Data, (entry) => {
|
||||
if (entry.IsBinary) {
|
||||
res.binaryData[entry.Key] = entry.Value;
|
||||
} else {
|
||||
res.data[entry.Key] = entry.Value;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -53,7 +77,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;
|
||||
}
|
||||
|
||||
@@ -63,18 +93,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
||||
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
|
||||
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
|
||||
@@ -20,7 +21,8 @@ class KubernetesNamespaceConverter {
|
||||
res.metadata.name = namespace.Name;
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
|
||||
if (namespace.ResourcePoolOwner) {
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner;
|
||||
const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,16 +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;
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
||||
res.stringData = secret.Data;
|
||||
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -19,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;
|
||||
}
|
||||
|
||||
@@ -31,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;
|
||||
}
|
||||
|
||||
@@ -40,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class KubernetesServiceConverter {
|
||||
payload.metadata.namespace = service.Namespace;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
|
||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.Application;
|
||||
payload.spec.ports = service.Ports;
|
||||
payload.spec.selector.app = service.ApplicationName;
|
||||
if (service.Headless) {
|
||||
|
||||
@@ -10,7 +10,11 @@ class KubernetesCommonHelper {
|
||||
}
|
||||
|
||||
static ownerToLabel(owner) {
|
||||
return _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
|
||||
let label = _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
|
||||
label = _.truncate(label, { length: 63, omission: '' });
|
||||
label = _.replace(label, /^[-_.]*/g, '');
|
||||
label = _.replace(label, /[-_.]*$/g, '');
|
||||
return label;
|
||||
}
|
||||
}
|
||||
export default KubernetesCommonHelper;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const _KubernetesConfigMap = Object.freeze({
|
||||
Namespace: '',
|
||||
Yaml: '',
|
||||
ConfigurationOwner: '',
|
||||
Data: {},
|
||||
Data: [],
|
||||
});
|
||||
|
||||
export class KubernetesConfigMap {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const KubernetesPortainerConfigurationOwnerLabel = 'io.portainer.kubernetes.configuration.owner';
|
||||
export const KubernetesPortainerConfigurationDataAnnotation = 'io.portainer.kubernetes.configuration.data';
|
||||
|
||||
/**
|
||||
* Configuration Model (Composite)
|
||||
|
||||
@@ -8,7 +8,7 @@ const _KubernetesApplicationSecret = Object.freeze({
|
||||
CreationDate: '',
|
||||
ConfigurationOwner: '',
|
||||
Yaml: '',
|
||||
Data: {},
|
||||
Data: [],
|
||||
});
|
||||
|
||||
export class KubernetesApplicationSecret {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
45
app/kubernetes/pod/payloads/create.js
Normal file
45
app/kubernetes/pod/payloads/create.js
Normal 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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -92,8 +92,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
|
||||
resource pool.
|
||||
</div>
|
||||
@@ -634,8 +634,8 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
|
||||
resource pool.
|
||||
</div>
|
||||
@@ -813,8 +813,8 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application would exceed available resources. Please review resource reservations or the instance count.
|
||||
</div>
|
||||
</div>
|
||||
@@ -930,8 +930,8 @@
|
||||
</table>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px;">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<i class="fa fa-exclamation-circle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
|
||||
</div>
|
||||
</div>
|
||||
@@ -1353,7 +1353,7 @@
|
||||
class="btn btn-primary"
|
||||
ng-model="publishedPort.Protocol"
|
||||
uib-btn-radio="'TCP'"
|
||||
ng-change="ctrl.onChangePortMappingContainerPort()"
|
||||
ng-change="ctrl.onChangePortProtocol($index)"
|
||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
|
||||
>TCP</label
|
||||
>
|
||||
@@ -1361,7 +1361,7 @@
|
||||
class="btn btn-primary"
|
||||
ng-model="publishedPort.Protocol"
|
||||
uib-btn-radio="'UDP'"
|
||||
ng-change="ctrl.onChangePortMappingContainerPort()"
|
||||
ng-change="ctrl.onChangePortProtocol($index)"
|
||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
|
||||
>UDP</label
|
||||
>
|
||||
|
||||
@@ -303,6 +303,9 @@ class KubernetesCreateApplicationController {
|
||||
const ingresses = this.filteredIngresses;
|
||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
||||
if (this.formValues.PublishedPorts.length) {
|
||||
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
||||
}
|
||||
this.formValues.PublishedPorts.push(p);
|
||||
}
|
||||
|
||||
@@ -335,6 +338,7 @@ class KubernetesCreateApplicationController {
|
||||
this.onChangePortMappingNodePort();
|
||||
this.onChangePortMappingIngressRoute();
|
||||
this.onChangePortMappingLoadBalancer();
|
||||
this.onChangePortProtocol();
|
||||
}
|
||||
|
||||
onChangePortMappingContainerPort() {
|
||||
@@ -403,6 +407,16 @@ class KubernetesCreateApplicationController {
|
||||
state.hasDuplicates = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region STATE VALIDATION FUNCTIONS */
|
||||
@@ -561,6 +575,10 @@ class KubernetesCreateApplicationController {
|
||||
return this.state.isEdit && !this.formValues.Placements[index].IsNew;
|
||||
}
|
||||
|
||||
isNewAndNotFirst(index) {
|
||||
return !this.state.isEdit && index !== 0;
|
||||
}
|
||||
|
||||
showPlacementPolicySection() {
|
||||
const placements = _.filter(this.formValues.Placements, { NeedsDeletion: false });
|
||||
return placements.length !== 0;
|
||||
@@ -600,8 +618,17 @@ class KubernetesCreateApplicationController {
|
||||
return this.state.isEdit && this.formValues.PublishedPorts.length > 0 && ports.length > 0;
|
||||
}
|
||||
|
||||
isEditLBWithPorts() {
|
||||
return this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER && _.filter(this.formValues.PublishedPorts, { IsNew: false }).length;
|
||||
}
|
||||
|
||||
isProtocolOptionDisabled(index, protocol) {
|
||||
return this.disableLoadBalancerEdit() || (this.isEditAndNotNewPublishedPort(index) && this.formValues.PublishedPorts[index].Protocol !== protocol);
|
||||
return (
|
||||
this.disableLoadBalancerEdit() ||
|
||||
(this.isEditAndNotNewPublishedPort(index) && this.formValues.PublishedPorts[index].Protocol !== protocol) ||
|
||||
(this.isEditLBWithPorts() && this.formValues.PublishedPorts[index].Protocol !== protocol) ||
|
||||
(this.isNewAndNotFirst(index) && this.formValues.PublishedPorts[index].Protocol !== protocol)
|
||||
);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
@@ -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>
|
||||
@@ -135,7 +131,13 @@
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading> <i class="fas fa-compress-arrows-alt space-right" aria-hidden="true"></i> Placement </uib-tab-heading>
|
||||
<uib-tab-heading>
|
||||
<i class="fas fa-compress-arrows-alt space-right" aria-hidden="true"></i> Placement
|
||||
<div ng-if="ctrl.state.placementWarning">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
warning
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<div class="small text-muted" style="padding: 20px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
The placement component helps you understand whether or not this application can be deployed on a specific node.
|
||||
|
||||
@@ -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;
|
||||
@@ -310,6 +306,7 @@ class KubernetesApplicationController {
|
||||
});
|
||||
|
||||
this.placements = computePlacements(nodes, this.application);
|
||||
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
} finally {
|
||||
@@ -335,9 +332,9 @@ class KubernetesApplicationController {
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
eventWarningCount: 0,
|
||||
placementWarning: false,
|
||||
expandedNote: false,
|
||||
useIngress: false,
|
||||
isAdmin: this.Authentication.isAdmin(),
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
||||
@@ -106,7 +106,21 @@
|
||||
</div>
|
||||
<!-- !type options -->
|
||||
|
||||
<kubernetes-configuration-data ng-if="ctrl.formValues" form-values="ctrl.formValues" is-valid="ctrl.state.isDataValid"></kubernetes-configuration-data>
|
||||
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET">
|
||||
Information
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.formValues.Type == ctrl.KubernetesConfigurationTypes.SECRET">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Creating a sensitive configuration will create a Kubernetes Secret of type <code>Opaque</code>. You can find more information about this in the
|
||||
<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"
|
||||
is-creation="true"
|
||||
></kubernetes-configuration-data>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Adding ingress controllers will allow users to expose application they deploy over a HTTP route.<br />
|
||||
Configuring ingress controllers will allow users to expose application they deploy over a HTTP route.<br />
|
||||
<p style="margin-top: 2px;">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Ingress classes must be manually specified for each controller you want to use in the cluster. Make sure that each controller is running inside your cluster.
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Ingress controller</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addIngressClass()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add ingress controller
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> configure ingress controller
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -58,19 +58,19 @@
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%;">
|
||||
<th style="width: 20px;">
|
||||
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
|
||||
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 55%;">
|
||||
<th style="width: 60%;">
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Storage
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th style="width: 35%;">
|
||||
<th style="width: 38%;">
|
||||
<a ng-click="$ctrl.changeOrderBy('Size')">
|
||||
Usage
|
||||
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
|
||||
@@ -96,8 +96,9 @@
|
||||
<td>{{ item.Size }}</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="vol in item.Volumes" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||
<td colspan="2">
|
||||
<a ui-sref="kubernetes.volumes.volume({ name: vol.PersistentVolumeClaim.Name, namespace: vol.PersistentVolumeClaim.Namespace })" style="margin-left: 25px;">
|
||||
<td></td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.volumes.volume({ name: vol.PersistentVolumeClaim.Name, namespace: vol.PersistentVolumeClaim.Namespace })">
|
||||
{{ vol.PersistentVolumeClaim.Name }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -37,6 +37,17 @@
|
||||
<td>Storage</td>
|
||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shared Access Policy</td>
|
||||
<td
|
||||
>{{ ctrl.state.volumeSharedAccessPolicy }}
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
ng-if="ctrl.state.volumeSharedAccessPolicyTooltip"
|
||||
message="{{ ctrl.state.volumeSharedAccessPolicyTooltip }}"
|
||||
></portainer-tooltip
|
||||
></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioner</td>
|
||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-' }}</td>
|
||||
|
||||
@@ -2,6 +2,7 @@ import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
|
||||
class KubernetesVolumeController {
|
||||
@@ -179,6 +180,8 @@ class KubernetesVolumeController {
|
||||
volumeSize: 0,
|
||||
volumeSizeUnit: 'GB',
|
||||
volumeSizeError: false,
|
||||
volumeSharedAccessPolicy: '',
|
||||
volumeSharedAccessPolicyTooltip: '',
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
||||
@@ -186,6 +189,16 @@ class KubernetesVolumeController {
|
||||
try {
|
||||
await this.getVolume();
|
||||
await this.getEvents();
|
||||
if (this.volume.PersistentVolumeClaim.StorageClass !== undefined) {
|
||||
this.state.volumeSharedAccessPolicy = this.volume.PersistentVolumeClaim.StorageClass.AccessModes[this.volume.PersistentVolumeClaim.StorageClass.AccessModes.length - 1];
|
||||
let policies = KubernetesStorageClassAccessPolicies();
|
||||
|
||||
policies.forEach((policy) => {
|
||||
if (policy.Name == this.state.volumeSharedAccessPolicy) {
|
||||
this.state.volumeSharedAccessPolicyTooltip = policy.Description;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default class DatatableColumnsVisibilityController {
|
||||
constructor() {
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.isOpen }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.state.isOpen">
|
||||
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
Show / Hide Columns
|
||||
</div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="(key, value) in $ctrl.columns">
|
||||
<input id="col_vis_{{::key}}" ng-change="$ctrl.onChange($ctrl.columns)" type="checkbox" ng-model="value.display" />
|
||||
<label for="col_vis_{{::key}}">{{ value.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.state.isOpen = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
@@ -0,0 +1,12 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import controller from './datatable-columns-visibility.controller';
|
||||
|
||||
angular.module('portainer.app').component('datatableColumnsVisibility', {
|
||||
templateUrl: './datatable-columns-visibility.html',
|
||||
controller,
|
||||
bindings: {
|
||||
columns: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
||||
refreshRate: '30',
|
||||
},
|
||||
};
|
||||
|
||||
this.resetSelectionState = function () {
|
||||
this.state.selectAll = false;
|
||||
this.state.selectedItems = [];
|
||||
@@ -158,6 +159,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
||||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
|
||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||
if (storedColumnVisibility !== null) {
|
||||
this.columnVisibility = storedColumnVisibility;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
<div class="settings">
|
||||
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
@@ -117,6 +118,20 @@
|
||||
</a>
|
||||
</th>
|
||||
<th>Control</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.CreationDate')">
|
||||
Created
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="$ctrl.columnVisibility.columns.updated.display">
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.UpdateDate')">
|
||||
Updated
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
||||
Ownership
|
||||
@@ -154,6 +169,14 @@
|
||||
</span>
|
||||
<span ng-if="!item.External">Total</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.CreationDate">{{ item.CreationDate | getisodatefromtimestamp }} {{ item.CreatedBy ? 'by ' + item.CreatedBy : '' }}</span>
|
||||
<span ng-if="!item.CreationDate"> - </span>
|
||||
</td>
|
||||
<td ng-if="$ctrl.columnVisibility.columns.updated.display">
|
||||
<span ng-if="item.UpdateDate">{{ item.UpdateDate | getisodatefromtimestamp }} {{ item.UpdatedBy ? 'by ' + item.UpdatedBy : '' }}</span>
|
||||
<span ng-if="!item.UpdateDate"> - </span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||
@@ -162,10 +185,10 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="6" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="4" class="text-center text-muted">No stack available.</td>
|
||||
<td colspan="6" class="text-center text-muted">No stack available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -15,6 +15,24 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
||||
},
|
||||
};
|
||||
|
||||
this.columnVisibility = {
|
||||
state: {
|
||||
open: false,
|
||||
},
|
||||
columns: {
|
||||
updated: {
|
||||
label: 'Updated',
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
|
||||
function onColumnVisibilityChange(columns) {
|
||||
this.columnVisibility.columns = columns;
|
||||
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not allow external items
|
||||
*/
|
||||
@@ -71,6 +89,11 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
||||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
|
||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||
if (storedColumnVisibility !== null) {
|
||||
this.columnVisibility = storedColumnVisibility;
|
||||
}
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { ExternalStackViewModel } from '@/portainer/models/stack';
|
||||
|
||||
angular.module('portainer.app').factory('StackHelper', [
|
||||
function StackHelperFactory() {
|
||||
'use strict';
|
||||
var helper = {};
|
||||
|
||||
helper.getExternalStackNamesFromContainers = function (containers) {
|
||||
var stackNames = [];
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var container = containers[i];
|
||||
if (!container.Labels || !container.Labels['com.docker.compose.project']) continue;
|
||||
var stackName = container.Labels['com.docker.compose.project'];
|
||||
stackNames.push(stackName);
|
||||
}
|
||||
|
||||
return _.uniq(stackNames);
|
||||
helper.getExternalStacksFromContainers = function (containers) {
|
||||
return getExternalStacksFromLabel(containers, 'com.docker.compose.project', 2);
|
||||
};
|
||||
|
||||
helper.getExternalStackNamesFromServices = function (services) {
|
||||
var stackNames = [];
|
||||
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
var service = services[i];
|
||||
if (!service.Labels || !service.Labels['com.docker.stack.namespace']) continue;
|
||||
|
||||
var stackName = service.Labels['com.docker.stack.namespace'];
|
||||
stackNames.push(stackName);
|
||||
}
|
||||
|
||||
return _.uniq(stackNames);
|
||||
helper.getExternalStacksFromServices = function (services) {
|
||||
return getExternalStacksFromLabel(services, 'com.docker.stack.namespace', 1);
|
||||
};
|
||||
|
||||
function getExternalStacksFromLabel(items, label, type) {
|
||||
return _.uniqBy(
|
||||
items.filter((item) => item.Labels && item.Labels[label]).map((item) => new ExternalStackViewModel(item.Labels[label], type, item.Created)),
|
||||
'Name'
|
||||
);
|
||||
}
|
||||
|
||||
return helper;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -13,11 +13,16 @@ export function StackViewModel(data) {
|
||||
}
|
||||
this.External = false;
|
||||
this.Status = data.Status;
|
||||
this.CreationDate = data.CreationDate;
|
||||
this.CreatedBy = data.CreatedBy;
|
||||
this.UpdateDate = data.UpdateDate;
|
||||
this.UpdatedBy = data.UpdatedBy;
|
||||
}
|
||||
|
||||
export function ExternalStackViewModel(name, type) {
|
||||
export function ExternalStackViewModel(name, type, creationDate) {
|
||||
this.Name = name;
|
||||
this.Type = type;
|
||||
this.External = true;
|
||||
this.Checked = false;
|
||||
this.CreationDate = creationDate;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
import { ExternalStackViewModel, StackViewModel } from '../../models/stack';
|
||||
import { StackViewModel } from '../../models/stack';
|
||||
|
||||
angular.module('portainer.app').factory('StackService', [
|
||||
'$q',
|
||||
@@ -121,13 +121,8 @@ angular.module('portainer.app').factory('StackService', [
|
||||
var deferred = $q.defer();
|
||||
|
||||
ServiceService.services()
|
||||
.then(function success(data) {
|
||||
var services = data;
|
||||
var stackNames = StackHelper.getExternalStackNamesFromServices(services);
|
||||
var stacks = stackNames.map(function (name) {
|
||||
return new ExternalStackViewModel(name, 1);
|
||||
});
|
||||
deferred.resolve(stacks);
|
||||
.then(function success(services) {
|
||||
deferred.resolve(StackHelper.getExternalStacksFromServices(services));
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve external stacks', err: err });
|
||||
@@ -140,13 +135,8 @@ angular.module('portainer.app').factory('StackService', [
|
||||
var deferred = $q.defer();
|
||||
|
||||
ContainerService.containers(1)
|
||||
.then(function success(data) {
|
||||
var containers = data;
|
||||
var stackNames = StackHelper.getExternalStackNamesFromContainers(containers);
|
||||
var stacks = stackNames.map(function (name) {
|
||||
return new ExternalStackViewModel(name, 2);
|
||||
});
|
||||
deferred.resolve(stacks);
|
||||
.then(function success(containers) {
|
||||
deferred.resolve(StackHelper.getExternalStacksFromContainers(containers));
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve external stacks', err: err });
|
||||
|
||||
@@ -179,7 +179,7 @@ angular.module('portainer.app').factory('StateManager', [
|
||||
}
|
||||
|
||||
var applicationState = LocalStorage.getApplicationState();
|
||||
if (applicationState) {
|
||||
if (applicationState && applicationState.validity) {
|
||||
var now = moment().unix();
|
||||
var cacheValidity = now - applicationState.validity;
|
||||
if (cacheValidity > APPLICATION_CACHE_VALIDITY) {
|
||||
|
||||
@@ -14,7 +14,8 @@ angular
|
||||
FormValidator,
|
||||
ResourceControlService,
|
||||
FormHelper,
|
||||
CustomTemplateService
|
||||
CustomTemplateService,
|
||||
EndpointProvider
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -166,6 +167,7 @@ angular
|
||||
|
||||
async function initView() {
|
||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
const endpointId = +$state.params.endpointId;
|
||||
$scope.state.StackType = 2;
|
||||
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER') {
|
||||
$scope.state.StackType = 1;
|
||||
@@ -177,6 +179,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();
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -39,14 +39,6 @@
|
||||
<div class="form-group">
|
||||
{{ stackName }}
|
||||
|
||||
<a
|
||||
ng-if="!state.externalStack && stackFileContent"
|
||||
class="btn btn-primary btn-xs"
|
||||
ui-sref="docker.templates.custom.new({fileContent: stackFileContent, type: stack.Type})"
|
||||
>
|
||||
Create template from stack
|
||||
</a>
|
||||
|
||||
<button
|
||||
authorization="PortainerStackUpdate"
|
||||
ng-if="!state.externalStack && stack.Status === 2"
|
||||
@@ -73,6 +65,15 @@
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>
|
||||
Delete this stack
|
||||
</button>
|
||||
|
||||
<button
|
||||
ng-if="!state.externalStack && stackFileContent"
|
||||
class="btn btn-primary btn-xs"
|
||||
ui-sref="docker.templates.custom.new({fileContent: stackFileContent, type: stack.Type})"
|
||||
>
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>
|
||||
Create template from stack
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !stack-details -->
|
||||
@@ -93,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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +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://download.docker.com/win/static/stable/x86_64/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"
|
||||
@@ -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}"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
param (
|
||||
[string]$docker_compose_version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop";
|
||||
|
||||
Invoke-WebRequest -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/$($docker_compose_version)/docker-compose-Windows-x86_64.exe"
|
||||
@@ -10,7 +10,9 @@ if [ "${PLATFORM}" == 'linux' ] && [ "${ARCH}" == 'amd64' ]; then
|
||||
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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
24
build/windows/Dockerfile
Normal 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"]
|
||||
@@ -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"]
|
||||
114
gruntfile.js
114
gruntfile.js
@@ -17,10 +17,11 @@ module.exports = function (grunt) {
|
||||
root: 'dist',
|
||||
distdir: 'dist/public',
|
||||
binaries: {
|
||||
dockerLinuxVersion: '18.09.3',
|
||||
dockerWindowsVersion: '17.09.0-ce',
|
||||
dockerComposeVersion: '1.27.4',
|
||||
komposeVersion: 'v1.21.0',
|
||||
dockerLinuxVersion: '19.03.13',
|
||||
dockerWindowsVersion: '19-03-12',
|
||||
dockerLinuxComposeVersion: '1.27.4',
|
||||
dockerWindowsComposeVersion: '1.28.0',
|
||||
komposeVersion: 'v1.22.0',
|
||||
kubectlVersion: 'v1.18.0',
|
||||
},
|
||||
config: gruntfile_cfg.config,
|
||||
@@ -164,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() {
|
||||
@@ -176,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(';');
|
||||
}
|
||||
|
||||
@@ -194,79 +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 %>';
|
||||
|
||||
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(' ');
|
||||
}
|
||||
|
||||
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(' ');
|
||||
}
|
||||
|
||||
function shell_download_docker_compose_binary(p, a) {
|
||||
var ps = { windows: 'win', darwin: 'mac' };
|
||||
var as = { arm: 'armhf', arm64: 'aarch64' };
|
||||
var ip = ps[p] === undefined ? p : ps[p];
|
||||
var ia = as[a] === undefined ? a : as[a];
|
||||
var binaryVersion = '<%= binaries.dockerComposeVersion %>';
|
||||
|
||||
if (p === 'linux' || p === 'mac') {
|
||||
return [
|
||||
'if [ -f dist/docker-compose ]; then',
|
||||
'echo "Docker Compose binary exists";',
|
||||
'else',
|
||||
'build/download_docker_compose_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
|
||||
'fi',
|
||||
].join(' ');
|
||||
}
|
||||
if (p === 'win') {
|
||||
return [
|
||||
'powershell -Command "& {if (Test-Path -Path "dist/docker-compose.exe") {',
|
||||
'Write-Host "Skipping download, Docker Compose binary exists"',
|
||||
'return',
|
||||
'} else {',
|
||||
'& ".\\build\\download_docker_compose_binary.ps1" -docker_compose_version ' + binaryVersion + '',
|
||||
'}}"',
|
||||
].join(' ');
|
||||
}
|
||||
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(' ');
|
||||
}
|
||||
|
||||
@@ -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,16 +73,21 @@
|
||||
"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",
|
||||
"file-system": "^2.2.2",
|
||||
"filesize": "~3.3.0",
|
||||
"filesize-parser": "^1.5.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jquery": "^3.5.1",
|
||||
"js-base64": "^3.6.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"lodash-es": "^4.17.15",
|
||||
"moment": "^2.21.0",
|
||||
"ng-file-upload": "~12.2.13",
|
||||
"source-map-loader": "^1.1.2",
|
||||
"spinkit": "^2.0.1",
|
||||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
|
||||
@@ -19,6 +19,11 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: ['source-map-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
|
||||
@@ -4,6 +4,7 @@ const commonConfig = require('./webpack.common.js');
|
||||
|
||||
module.exports = webpackMerge(commonConfig, {
|
||||
mode: 'development',
|
||||
devtool: 'eval-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user