Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9af9b70f3e | |||
| fa38af5d81 | |||
| 1b82b450d7 | |||
| b78d804881 | |||
| 51b72c12f9 | |||
| 58c04bdbe3 | |||
| a6320d5222 | |||
| cb4b4a43e6 | |||
| 1e5a1d5bdd | |||
| 5ed0d21c39 | |||
| 2972dbeafb |
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a bug report
|
about: Create a bug report
|
||||||
|
title: ''
|
||||||
|
labels: bug/need-confirmation, kind/bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -9,7 +13,7 @@ Thanks for reporting a bug for Portainer !
|
|||||||
|
|
||||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||||
|
|
||||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
|
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||||
|
|
||||||
Before opening a new issue, make sure that we do not have any duplicates
|
Before opening a new issue, make sure that we do not have any duplicates
|
||||||
already open. You can ensure this by searching the issue list for this
|
already open. You can ensure this by searching the issue list for this
|
||||||
@@ -40,6 +44,7 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
|
|||||||
|
|
||||||
- Portainer version:
|
- Portainer version:
|
||||||
- Docker version (managed by Portainer):
|
- Docker version (managed by Portainer):
|
||||||
|
- Kubernetes version (managed by Portainer):
|
||||||
- Platform (windows/linux):
|
- Platform (windows/linux):
|
||||||
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||||
- Browser:
|
- Browser:
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
---
|
---
|
||||||
name: Question
|
name: Question
|
||||||
about: Ask us a question about Portainer usage or deployment
|
about: Ask us a question about Portainer usage or deployment
|
||||||
|
title: ''
|
||||||
---
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
<!--
|
|
||||||
|
---
|
||||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
|
||||||
|
<!--
|
||||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
|
||||||
|
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
|
||||||
-->
|
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||||
|
|
||||||
**Question**:
|
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||||
How can I deploy Portainer on... ?
|
-->
|
||||||
|
|
||||||
|
**Question**:
|
||||||
|
How can I deploy Portainer on... ?
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest a feature/enhancement that should be added in Portainer
|
about: Suggest a feature/enhancement that should be added in Portainer
|
||||||
|
title: ''
|
||||||
---
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
<!--
|
|
||||||
|
---
|
||||||
Thanks for opening a feature request for Portainer !
|
|
||||||
|
<!--
|
||||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
|
||||||
|
Thanks for opening a feature request for Portainer !
|
||||||
Before opening a new issue, make sure that we do not have any duplicates
|
|
||||||
already open. You can ensure this by searching the issue list for this
|
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||||
repository. If there is a duplicate, please close your issue and add a comment
|
|
||||||
to the existing issue instead.
|
Before opening a new issue, make sure that we do not have any duplicates
|
||||||
|
already open. You can ensure this by searching the issue list for this
|
||||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
repository. If there is a duplicate, please close your issue and add a comment
|
||||||
-->
|
to the existing issue instead.
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
-->
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Additional context**
|
**Describe alternatives you've considered**
|
||||||
Add any other context or screenshots about the feature request here.
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ issues:
|
|||||||
- kind/question
|
- kind/question
|
||||||
- kind/style
|
- kind/style
|
||||||
- kind/workaround
|
- kind/workaround
|
||||||
|
- kind/refactor
|
||||||
- bug/need-confirmation
|
- bug/need-confirmation
|
||||||
- bug/confirmed
|
- bug/confirmed
|
||||||
- status/discuss
|
- status/discuss
|
||||||
|
|||||||
@@ -74,3 +74,23 @@ Our contribution process is described below. Some of the steps can be visualized
|
|||||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Build Portainer locally
|
||||||
|
|
||||||
|
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
||||||
|
|
||||||
|
Install dependencies with yarn:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build and run the project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Portainer can now be accessed at <http://localhost:9000>.
|
||||||
|
|
||||||
|
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
|||||||
|
|
||||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||||
- [Documentation](https://documentation.portainer.io)
|
- [Documentation](https://documentation.portainer.io)
|
||||||
|
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
|
||||||
|
|
||||||
## Getting help
|
## 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
|
- Issues: https://github.com/portainer/portainer/issues
|
||||||
- FAQ: https://documentation.portainer.io
|
- FAQ: https://documentation.portainer.io
|
||||||
|
|||||||
+39
-24
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt"
|
"github.com/portainer/portainer/api/bolt"
|
||||||
"github.com/portainer/portainer/api/chisel"
|
"github.com/portainer/portainer/api/chisel"
|
||||||
"github.com/portainer/portainer/api/cli"
|
"github.com/portainer/portainer/api/cli"
|
||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
"github.com/portainer/portainer/api/http"
|
"github.com/portainer/portainer/api/http"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/internal/snapshot"
|
"github.com/portainer/portainer/api/internal/snapshot"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
"github.com/portainer/portainer/api/kubernetes"
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
@@ -71,7 +73,12 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
|
|||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||||
|
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
|
||||||
|
if composeWrapper != nil {
|
||||||
|
return composeWrapper
|
||||||
|
}
|
||||||
|
|
||||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +96,10 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.UserSessionTimeout == "" {
|
||||||
|
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||||
|
dataStore.Settings().UpdateSettings(settings)
|
||||||
|
}
|
||||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -380,8 +391,10 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||||
|
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||||
|
|
||||||
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
||||||
|
|
||||||
@@ -448,27 +461,29 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var server portainer.Server = &http.Server{
|
var server portainer.Server = &http.Server{
|
||||||
ReverseTunnelService: reverseTunnelService,
|
ReverseTunnelService: reverseTunnelService,
|
||||||
Status: applicationStatus,
|
Status: applicationStatus,
|
||||||
BindAddress: *flags.Addr,
|
BindAddress: *flags.Addr,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
DataStore: dataStore,
|
DataStore: dataStore,
|
||||||
SwarmStackManager: swarmStackManager,
|
SwarmStackManager: swarmStackManager,
|
||||||
ComposeStackManager: composeStackManager,
|
ComposeStackManager: composeStackManager,
|
||||||
KubernetesDeployer: kubernetesDeployer,
|
KubernetesDeployer: kubernetesDeployer,
|
||||||
CryptoService: cryptoService,
|
CryptoService: cryptoService,
|
||||||
JWTService: jwtService,
|
JWTService: jwtService,
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
LDAPService: ldapService,
|
LDAPService: ldapService,
|
||||||
OAuthService: oauthService,
|
OAuthService: oauthService,
|
||||||
GitService: gitService,
|
GitService: gitService,
|
||||||
SignatureService: digitalSignatureService,
|
ProxyManager: proxyManager,
|
||||||
SnapshotService: snapshotService,
|
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||||
SSL: *flags.SSL,
|
SignatureService: digitalSignatureService,
|
||||||
SSLCert: *flags.SSLCert,
|
SnapshotService: snapshotService,
|
||||||
SSLKey: *flags.SSLKey,
|
SSL: *flags.SSL,
|
||||||
DockerClientFactory: dockerClientFactory,
|
SSLCert: *flags.SSLCert,
|
||||||
KubernetesClientFactory: kubernetesClientFactory,
|
SSLKey: *flags.SSLKey,
|
||||||
|
DockerClientFactory: dockerClientFactory,
|
||||||
|
KubernetesClientFactory: kubernetesClientFactory,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ComposeWrapper is a wrapper for docker-compose binary
|
||||||
|
type ComposeWrapper struct {
|
||||||
|
binaryPath string
|
||||||
|
proxyManager *proxy.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||||
|
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
|
||||||
|
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ComposeWrapper{
|
||||||
|
binaryPath: binaryPath,
|
||||||
|
proxyManager: proxyManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||||
|
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
|
||||||
|
return portainer.ComposeSyntaxMaxVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
|
||||||
|
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
|
||||||
|
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
|
||||||
|
if endpoint == nil {
|
||||||
|
return nil, errors.New("cannot call a compose command on an empty endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
program := programPath(w.binaryPath, "docker-compose")
|
||||||
|
|
||||||
|
options := setComposeFile(stack)
|
||||||
|
|
||||||
|
options = addProjectNameOption(options, stack)
|
||||||
|
options, err := addEnvFileOption(options, stack)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
|
||||||
|
|
||||||
|
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
args := append(options, command...)
|
||||||
|
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd := exec.Command(program, args...)
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return out, errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setComposeFile(stack *portainer.Stack) []string {
|
||||||
|
options := make([]string, 0)
|
||||||
|
|
||||||
|
if stack == nil || stack.EntryPoint == "" {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||||
|
options = append(options, "-f", composeFilePath)
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
|
||||||
|
if stack == nil || stack.Name == "" {
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, "-p", stack.Name)
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
|
||||||
|
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
envFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||||
|
|
||||||
|
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return options, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range stack.Env {
|
||||||
|
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||||
|
}
|
||||||
|
envfile.Close()
|
||||||
|
|
||||||
|
options = append(options, "--env-file", envFilePath)
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// +build integration
|
||||||
|
|
||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeFile = `version: "3.9"
|
||||||
|
services:
|
||||||
|
busybox:
|
||||||
|
image: "alpine:latest"
|
||||||
|
container_name: "compose_wrapper_test"`
|
||||||
|
const composedContainerName = "compose_wrapper_test"
|
||||||
|
|
||||||
|
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
composeFileName := "compose_wrapper_test.yml"
|
||||||
|
f, _ := os.Create(filepath.Join(dir, composeFileName))
|
||||||
|
f.WriteString(composeFile)
|
||||||
|
|
||||||
|
stack := &portainer.Stack{
|
||||||
|
ProjectPath: dir,
|
||||||
|
EntryPoint: composeFileName,
|
||||||
|
Name: "project-name",
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := &portainer.Endpoint{}
|
||||||
|
|
||||||
|
return stack, endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_UpAndDown(t *testing.T) {
|
||||||
|
|
||||||
|
stack, endpoint := setup(t)
|
||||||
|
|
||||||
|
w := NewComposeWrapper("", nil)
|
||||||
|
|
||||||
|
err := w.Up(stack, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error calling docker-compose up: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerExists(composedContainerName) == false {
|
||||||
|
t.Fatal("container should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.Down(stack, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error calling docker-compose down: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerExists(composedContainerName) {
|
||||||
|
t.Fatal("container should be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerExists(contaierName string) bool {
|
||||||
|
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to list containers: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(string(out), contaierName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_setComposeFile(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stack *portainer.Stack
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should return empty result if stack is missing",
|
||||||
|
stack: nil,
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return empty result if stack don't have entrypoint",
|
||||||
|
stack: &portainer.Stack{},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should allow file name and dir",
|
||||||
|
stack: &portainer.Stack{
|
||||||
|
ProjectPath: "dir",
|
||||||
|
EntryPoint: "file",
|
||||||
|
},
|
||||||
|
expected: []string{"-f", path.Join("dir", "file")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should allow file name only",
|
||||||
|
stack: &portainer.Stack{
|
||||||
|
EntryPoint: "file",
|
||||||
|
},
|
||||||
|
expected: []string{"-f", "file"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := setComposeFile(tt.stack)
|
||||||
|
assert.ElementsMatch(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_addProjectNameOption(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stack *portainer.Stack
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should not add project option if stack is missing",
|
||||||
|
stack: nil,
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not add project option if stack doesn't have name",
|
||||||
|
stack: &portainer.Stack{},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should add project name option if stack has a name",
|
||||||
|
stack: &portainer.Stack{
|
||||||
|
Name: "project-name",
|
||||||
|
},
|
||||||
|
expected: []string{"-p", "project-name"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
options := []string{"-a", "b"}
|
||||||
|
result := addProjectNameOption(options, tt.stack)
|
||||||
|
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_addEnvFileOption(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stack *portainer.Stack
|
||||||
|
expected []string
|
||||||
|
expectedContent string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should not add env file option if stack is missing",
|
||||||
|
stack: nil,
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not add env file option if stack doesn't have env variables",
|
||||||
|
stack: &portainer.Stack{},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should not add env file option if stack's env variables are empty",
|
||||||
|
stack: &portainer.Stack{
|
||||||
|
ProjectPath: dir,
|
||||||
|
Env: []portainer.Pair{},
|
||||||
|
},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should add env file option if stack has env variables",
|
||||||
|
stack: &portainer.Stack{
|
||||||
|
ProjectPath: dir,
|
||||||
|
Env: []portainer.Pair{
|
||||||
|
{Name: "var1", Value: "value1"},
|
||||||
|
{Name: "var2", Value: "value2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{"--env-file", path.Join(dir, "stack.env")},
|
||||||
|
expectedContent: "var1=value1\nvar2=value2\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
options := []string{"-a", "b"}
|
||||||
|
result, _ := addEnvFileOption(options, tt.stack)
|
||||||
|
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||||
|
|
||||||
|
if tt.expectedContent != "" {
|
||||||
|
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||||
|
content, _ := ioutil.ReadAll(f)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedContent, string(content))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,6 +134,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
|
|||||||
|
|
||||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||||
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||||
|
} else {
|
||||||
|
args = append(args, "--tlscacert", "''")
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func osProgram(program string) string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
program += ".exe"
|
||||||
|
}
|
||||||
|
return program
|
||||||
|
}
|
||||||
|
|
||||||
|
func programPath(rootPath, program string) string {
|
||||||
|
return filepath.Join(rootPath, osProgram(program))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBinaryPresent returns true if corresponding program exists on PATH
|
||||||
|
func IsBinaryPresent(program string) bool {
|
||||||
|
_, err := exec.LookPath(program)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_isBinaryPresent(t *testing.T) {
|
||||||
|
|
||||||
|
if !IsBinaryPresent("docker") {
|
||||||
|
t.Error("expect docker binary to exist on the path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
|
||||||
|
t.Error("expect binary with a random name to be missing on the path")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ require (
|
|||||||
github.com/portainer/libcompose v0.5.3
|
github.com/portainer/libcompose v0.5.3
|
||||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||||
|
github.com/stretchr/testify v1.6.1 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
|
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
|
|||||||
@@ -262,12 +262,15 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
|||||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||||
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
||||||
@@ -392,6 +395,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/errors"
|
"github.com/portainer/portainer/api/bolt/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideFields(endpoint)
|
hideFields(endpoint)
|
||||||
|
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||||
|
|
||||||
return response.JSON(w, endpoint)
|
return response.JSON(w, endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
|
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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 {
|
for idx := range paginatedEndpoints {
|
||||||
hideFields(&paginatedEndpoints[idx])
|
hideFields(&paginatedEndpoints[idx])
|
||||||
|
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Handler struct {
|
|||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
SnapshotService portainer.SnapshotService
|
SnapshotService portainer.SnapshotService
|
||||||
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage endpoint operations.
|
// NewHandler creates a handler to manage endpoint operations.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
|||||||
registry.Username = *payload.Username
|
registry.Username = *payload.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Password != nil {
|
if payload.Password != nil && *payload.Password != "" {
|
||||||
registry.Password = *payload.Password
|
registry.Password = *payload.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
@@ -60,13 +61,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
|||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
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()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: payload.ComposeFilePathInRepository,
|
EntryPoint: payload.ComposeFilePathInRepository,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
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()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||||
@@ -347,7 +357,6 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
|||||||
!isAdminOrEndpointAdmin {
|
!isAdminOrEndpointAdmin {
|
||||||
|
|
||||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||||
|
|
||||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
@@ -55,14 +56,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
|||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
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()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: payload.ComposeFilePathInRepository,
|
EntryPoint: payload.ComposeFilePathInRepository,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
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()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
)
|
)
|
||||||
@@ -78,6 +78,17 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
|||||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
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) {
|
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||||
isAdmin := user.Role == portainer.AdministratorRole
|
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 {
|
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 {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,5 +155,6 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
|
|||||||
if stack.Type == portainer.DockerSwarmStack {
|
if stack.Type == portainer.DockerSwarmStack {
|
||||||
return handler.SwarmStackManager.Remove(stack, endpoint)
|
return handler.SwarmStackManager.Remove(stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.ComposeStackManager.Down(stack, endpoint)
|
return handler.ComposeStackManager.Down(stack, endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// POST request on /api/stacks/:id/stop
|
// POST request on /api/stacks/:id/stop
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -135,6 +136,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
|||||||
return configErr
|
return configErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.UpdateDate = time.Now().Unix()
|
||||||
|
stack.UpdatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.deployComposeStack(config)
|
err = handler.deployComposeStack(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
@@ -163,6 +167,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
|||||||
return configErr
|
return configErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.UpdateDate = time.Now().Unix()
|
||||||
|
stack.UpdatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.deploySwarmStack(config)
|
err = handler.deploySwarmStack(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/crypto"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProxyServer provide an extedned proxy with a local server to forward requests
|
||||||
|
type ProxyServer struct {
|
||||||
|
server *http.Server
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||||
|
return &ProxyServer{
|
||||||
|
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL, err := url.Parse(endpoint.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL.Scheme = "http"
|
||||||
|
httpTransport := &http.Transport{}
|
||||||
|
|
||||||
|
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||||
|
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpTransport.TLSClientConfig = config
|
||||||
|
endpointURL.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||||
|
|
||||||
|
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||||
|
|
||||||
|
proxyServer := &ProxyServer{
|
||||||
|
&http.Server{
|
||||||
|
Handler: proxy,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyServer, proxyServer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ProxyServer) start() error {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.Port = listener.Addr().(*net.TCPAddr).Port
|
||||||
|
go func() {
|
||||||
|
proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port)
|
||||||
|
log.Printf("Starting Proxy server on %s...\n", proxyHost)
|
||||||
|
|
||||||
|
err := proxy.server.Serve(listener)
|
||||||
|
log.Printf("Exiting Proxy server %s\n", proxyHost)
|
||||||
|
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the server
|
||||||
|
func (proxy *ProxyServer) Close() {
|
||||||
|
if proxy.server != nil {
|
||||||
|
proxy.server.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package dockercompose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
|
||||||
|
AgentTransport struct {
|
||||||
|
httpTransport *http.Transport
|
||||||
|
signatureService portainer.DigitalSignatureService
|
||||||
|
endpointIdentifier portainer.EndpointID
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||||
|
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
|
||||||
|
transport := &AgentTransport{
|
||||||
|
httpTransport: httpTransport,
|
||||||
|
signatureService: signatureService,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
|
||||||
|
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||||
|
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
|
|
||||||
|
return transport.httpTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package kubernetes
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
@@ -13,14 +14,16 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
localTransport struct {
|
localTransport struct {
|
||||||
httpTransport *http.Transport
|
httpTransport *http.Transport
|
||||||
tokenManager *tokenManager
|
tokenManager *tokenManager
|
||||||
|
endpointIdentifier portainer.EndpointID
|
||||||
}
|
}
|
||||||
|
|
||||||
agentTransport struct {
|
agentTransport struct {
|
||||||
httpTransport *http.Transport
|
httpTransport *http.Transport
|
||||||
tokenManager *tokenManager
|
tokenManager *tokenManager
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
endpointIdentifier portainer.EndpointID
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeTransport struct {
|
edgeTransport struct {
|
||||||
@@ -50,21 +53,11 @@ func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
|
|||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var token string
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
|
||||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
|
||||||
} else {
|
|
||||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
|
||||||
return transport.httpTransport.RoundTrip(request)
|
return transport.httpTransport.RoundTrip(request)
|
||||||
@@ -85,21 +78,11 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
|
|||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var token string
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
|
||||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
|
||||||
} else {
|
|
||||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||||
|
|
||||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||||
@@ -127,21 +110,11 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
|
|||||||
|
|
||||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var token string
|
|
||||||
if tokenData.Role == portainer.AdministratorRole {
|
|
||||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
|
||||||
} else {
|
|
||||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||||
|
|
||||||
response, err := transport.httpTransport.RoundTrip(request)
|
response, err := transport.httpTransport.RoundTrip(request)
|
||||||
@@ -154,3 +127,27 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
|
|||||||
|
|
||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRoundTripToken(
|
||||||
|
request *http.Request,
|
||||||
|
tokenManager *tokenManager,
|
||||||
|
endpointIdentifier portainer.EndpointID,
|
||||||
|
) (string, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
token = tokenManager.getAdminServiceAccountToken()
|
||||||
|
} else {
|
||||||
|
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed retrieving service account token: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
@@ -21,6 +22,7 @@ type (
|
|||||||
proxyFactory *factory.ProxyFactory
|
proxyFactory *factory.ProxyFactory
|
||||||
endpointProxies cmap.ConcurrentMap
|
endpointProxies cmap.ConcurrentMap
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
|
k8sClientFactory *cli.ClientFactory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ func NewManager(dataStore portainer.DataStore, signatureService portainer.Digita
|
|||||||
return &Manager{
|
return &Manager{
|
||||||
endpointProxies: cmap.New(),
|
endpointProxies: cmap.New(),
|
||||||
legacyExtensionProxies: cmap.New(),
|
legacyExtensionProxies: cmap.New(),
|
||||||
|
k8sClientFactory: kubernetesClientFactory,
|
||||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,13 +44,19 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateComposeProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||||
|
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||||
|
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
|
||||||
|
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
// GetEndpointProxy returns the proxy associated to a key
|
// GetEndpointProxy returns the proxy associated to a key
|
||||||
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
|
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 {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -56,8 +65,11 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteEndpointProxy deletes the proxy associated to a key
|
// DeleteEndpointProxy deletes the proxy associated to a key
|
||||||
|
// and cleans the k8s endpoint client cache. DeleteEndpointProxy
|
||||||
|
// is currently only called for edge connection clean up.
|
||||||
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
||||||
manager.endpointProxies.Remove(string(endpoint.ID))
|
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
|
||||||
|
manager.k8sClientFactory.RemoveKubeClient(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||||
|
|||||||
+31
-29
@@ -39,39 +39,41 @@ import (
|
|||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server implements the portainer.Server interface
|
// Server implements the portainer.Server interface
|
||||||
type Server struct {
|
type Server struct {
|
||||||
BindAddress string
|
BindAddress string
|
||||||
AssetsPath string
|
AssetsPath string
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
SnapshotService portainer.SnapshotService
|
SnapshotService portainer.SnapshotService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
DataStore portainer.DataStore
|
DataStore portainer.DataStore
|
||||||
GitService portainer.GitService
|
GitService portainer.GitService
|
||||||
JWTService portainer.JWTService
|
JWTService portainer.JWTService
|
||||||
LDAPService portainer.LDAPService
|
LDAPService portainer.LDAPService
|
||||||
OAuthService portainer.OAuthService
|
OAuthService portainer.OAuthService
|
||||||
SwarmStackManager portainer.SwarmStackManager
|
SwarmStackManager portainer.SwarmStackManager
|
||||||
Handler *handler.Handler
|
ProxyManager *proxy.Manager
|
||||||
SSL bool
|
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
SSLCert string
|
Handler *handler.Handler
|
||||||
SSLKey string
|
SSL bool
|
||||||
DockerClientFactory *docker.ClientFactory
|
SSLCert string
|
||||||
KubernetesClientFactory *cli.ClientFactory
|
SSLKey string
|
||||||
KubernetesDeployer portainer.KubernetesDeployer
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
KubernetesClientFactory *cli.ClientFactory
|
||||||
|
KubernetesDeployer portainer.KubernetesDeployer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (server *Server) Start() error {
|
func (server *Server) Start() error {
|
||||||
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
kubernetesTokenCacheManager := server.KubernetesTokenCacheManager
|
||||||
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
|
|
||||||
|
|
||||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ func (server *Server) Start() error {
|
|||||||
authHandler.CryptoService = server.CryptoService
|
authHandler.CryptoService = server.CryptoService
|
||||||
authHandler.JWTService = server.JWTService
|
authHandler.JWTService = server.JWTService
|
||||||
authHandler.LDAPService = server.LDAPService
|
authHandler.LDAPService = server.LDAPService
|
||||||
authHandler.ProxyManager = proxyManager
|
authHandler.ProxyManager = server.ProxyManager
|
||||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||||
authHandler.OAuthService = server.OAuthService
|
authHandler.OAuthService = server.OAuthService
|
||||||
|
|
||||||
@@ -116,10 +118,10 @@ func (server *Server) Start() error {
|
|||||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||||
endpointHandler.DataStore = server.DataStore
|
endpointHandler.DataStore = server.DataStore
|
||||||
endpointHandler.FileService = server.FileService
|
endpointHandler.FileService = server.FileService
|
||||||
endpointHandler.ProxyManager = proxyManager
|
endpointHandler.ProxyManager = server.ProxyManager
|
||||||
endpointHandler.SnapshotService = server.SnapshotService
|
endpointHandler.SnapshotService = server.SnapshotService
|
||||||
endpointHandler.ProxyManager = proxyManager
|
|
||||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
endpointHandler.ComposeStackManager = server.ComposeStackManager
|
||||||
|
|
||||||
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
|
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
|
||||||
endpointEdgeHandler.DataStore = server.DataStore
|
endpointEdgeHandler.DataStore = server.DataStore
|
||||||
@@ -131,7 +133,7 @@ func (server *Server) Start() error {
|
|||||||
|
|
||||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||||
endpointProxyHandler.DataStore = server.DataStore
|
endpointProxyHandler.DataStore = server.DataStore
|
||||||
endpointProxyHandler.ProxyManager = proxyManager
|
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||||
@@ -141,7 +143,7 @@ func (server *Server) Start() error {
|
|||||||
var registryHandler = registries.NewHandler(requestBouncer)
|
var registryHandler = registries.NewHandler(requestBouncer)
|
||||||
registryHandler.DataStore = server.DataStore
|
registryHandler.DataStore = server.DataStore
|
||||||
registryHandler.FileService = server.FileService
|
registryHandler.FileService = server.FileService
|
||||||
registryHandler.ProxyManager = proxyManager
|
registryHandler.ProxyManager = server.ProxyManager
|
||||||
|
|
||||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||||
resourceControlHandler.DataStore = server.DataStore
|
resourceControlHandler.DataStore = server.DataStore
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ import (
|
|||||||
"github.com/portainer/portainer/api"
|
"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
|
// 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.
|
// 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 {
|
func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl {
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the cached kube client so a new one can be created
|
||||||
|
func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) {
|
||||||
|
factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID)))
|
||||||
|
}
|
||||||
|
|
||||||
// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found.
|
// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found.
|
||||||
// If no client is registered, it will create a new client, register it, and returns it.
|
// If no client is registered, it will create a new client, register it, and returns it.
|
||||||
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import (
|
|||||||
"github.com/portainer/libcompose/lookup"
|
"github.com/portainer/libcompose/lookup"
|
||||||
"github.com/portainer/libcompose/project"
|
"github.com/portainer/libcompose/project"
|
||||||
"github.com/portainer/libcompose/project/options"
|
"github.com/portainer/libcompose/project/options"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dockerClientVersion = "1.24"
|
dockerClientVersion = "1.24"
|
||||||
|
composeSyntaxMaxVersion = "2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ComposeStackManager represents a service for managing compose stacks.
|
// ComposeStackManager represents a service for managing compose stacks.
|
||||||
@@ -58,6 +59,11 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
|
|||||||
return client.NewDefaultFactory(clientOpts)
|
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)
|
// Up will deploy a compose stack (equivalent of docker-compose up)
|
||||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
|
||||||
|
|||||||
+27
-19
@@ -190,24 +190,25 @@ type (
|
|||||||
// Endpoint represents a Docker endpoint with all the info required
|
// Endpoint represents a Docker endpoint with all the info required
|
||||||
// to connect to it
|
// to connect to it
|
||||||
Endpoint struct {
|
Endpoint struct {
|
||||||
ID EndpointID `json:"Id"`
|
ID EndpointID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Type EndpointType `json:"Type"`
|
Type EndpointType `json:"Type"`
|
||||||
URL string `json:"URL"`
|
URL string `json:"URL"`
|
||||||
GroupID EndpointGroupID `json:"GroupId"`
|
GroupID EndpointGroupID `json:"GroupId"`
|
||||||
PublicURL string `json:"PublicURL"`
|
PublicURL string `json:"PublicURL"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
Extensions []EndpointExtension `json:"Extensions"`
|
Extensions []EndpointExtension `json:"Extensions"`
|
||||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||||
TagIDs []TagID `json:"TagIds"`
|
TagIDs []TagID `json:"TagIds"`
|
||||||
Status EndpointStatus `json:"Status"`
|
Status EndpointStatus `json:"Status"`
|
||||||
Snapshots []DockerSnapshot `json:"Snapshots"`
|
Snapshots []DockerSnapshot `json:"Snapshots"`
|
||||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
EdgeID string `json:"EdgeID,omitempty"`
|
EdgeID string `json:"EdgeID,omitempty"`
|
||||||
EdgeKey string `json:"EdgeKey"`
|
EdgeKey string `json:"EdgeKey"`
|
||||||
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
|
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
|
||||||
Kubernetes KubernetesData `json:"Kubernetes"`
|
Kubernetes KubernetesData `json:"Kubernetes"`
|
||||||
|
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
@@ -554,6 +555,10 @@ type (
|
|||||||
Env []Pair `json:"Env"`
|
Env []Pair `json:"Env"`
|
||||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||||
Status StackStatus `json:"Status"`
|
Status StackStatus `json:"Status"`
|
||||||
|
CreationDate int64
|
||||||
|
CreatedBy string
|
||||||
|
UpdateDate int64
|
||||||
|
UpdatedBy string
|
||||||
ProjectPath string
|
ProjectPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,6 +779,7 @@ type (
|
|||||||
|
|
||||||
// ComposeStackManager represents a service to manage Compose stacks
|
// ComposeStackManager represents a service to manage Compose stacks
|
||||||
ComposeStackManager interface {
|
ComposeStackManager interface {
|
||||||
|
ComposeSyntaxMaxVersion() string
|
||||||
Up(stack *Stack, endpoint *Endpoint) error
|
Up(stack *Stack, endpoint *Endpoint) error
|
||||||
Down(stack *Stack, endpoint *Endpoint) error
|
Down(stack *Stack, endpoint *Endpoint) error
|
||||||
}
|
}
|
||||||
@@ -1119,9 +1125,11 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "2.0.0"
|
APIVersion = "2.1.0"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 25
|
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 represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
|||||||
@@ -927,6 +927,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.striketext:before,
|
||||||
|
.striketext:after {
|
||||||
|
background-color: #777777;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 1px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.striketext:before {
|
||||||
|
right: 0.5em;
|
||||||
|
margin-left: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.striketext:after {
|
||||||
|
left: 0.5em;
|
||||||
|
margin-right: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
/*bootbox override*/
|
/*bootbox override*/
|
||||||
.modal-open {
|
.modal-open {
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
|
|||||||
@@ -4,100 +4,8 @@
|
|||||||
<div class="toolBar">
|
<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="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<span
|
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
||||||
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>
|
|
||||||
<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 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>
|
<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>
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
|||||||
+5
-7
@@ -36,9 +36,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.columnVisibility = {
|
this.columnVisibility = {
|
||||||
state: {
|
|
||||||
open: false,
|
|
||||||
},
|
|
||||||
columns: {
|
columns: {
|
||||||
state: {
|
state: {
|
||||||
label: 'State',
|
label: 'State',
|
||||||
@@ -75,9 +72,11 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onColumnVisibilityChange = function (columnVisibility) {
|
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
|
||||||
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
|
function onColumnVisibilityChange(columns) {
|
||||||
};
|
this.columnVisibility.columns = columns;
|
||||||
|
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
this.onSelectionChanged = function () {
|
this.onSelectionChanged = function () {
|
||||||
this.updateSelectionState();
|
this.updateSelectionState();
|
||||||
@@ -199,7 +198,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
|||||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||||
if (storedColumnVisibility !== null) {
|
if (storedColumnVisibility !== null) {
|
||||||
this.columnVisibility = storedColumnVisibility;
|
this.columnVisibility = storedColumnVisibility;
|
||||||
this.columnVisibility.state.open = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,10 +36,13 @@
|
|||||||
<!-- don't use registry -->
|
<!-- don't use registry -->
|
||||||
<div ng-if="!$ctrl.model.UseRegistry">
|
<div ng-if="!$ctrl.model.UseRegistry">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
|
<span class="small">
|
||||||
>Image
|
<p class="text-muted" style="margin-left: 15px;">
|
||||||
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
|
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
</label>
|
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">
|
<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 />
|
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export function ImageViewModel(data) {
|
|||||||
this.Repository = data.Repository;
|
this.Repository = data.Repository;
|
||||||
this.Created = data.Created;
|
this.Created = data.Created;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
|
|
||||||
this.RepoTags = data.RepoTags;
|
this.RepoTags = data.RepoTags;
|
||||||
if (!this.RepoTags && data.RepoDigests) {
|
if (!this.RepoTags && data.RepoDigests) {
|
||||||
this.RepoTags = [];
|
this.RepoTags = [];
|
||||||
@@ -21,6 +20,7 @@ export function ImageViewModel(data) {
|
|||||||
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||||
this.NodeName = data.Portainer.Agent.NodeName;
|
this.NodeName = data.Portainer.Agent.NodeName;
|
||||||
}
|
}
|
||||||
|
this.Labels = data.Labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageBuildModel(data) {
|
export function ImageBuildModel(data) {
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ export function ImageDetailsViewModel(data) {
|
|||||||
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
|
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
|
||||||
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
|
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
|
||||||
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
|
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
|
||||||
|
this.Labels = data.ContainerConfig.Labels;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,17 @@
|
|||||||
<td>Build</td>
|
<td>Build</td>
|
||||||
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
|
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
|
||||||
</tr>
|
</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">
|
<tr ng-if="image.Author">
|
||||||
<td>Author</td>
|
<td>Author</td>
|
||||||
<td>{{ image.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="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="ownership" class="control-label text-left">
|
<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>
|
||||||
<label name="ownership" class="switch" style="margin-left: 20px;">
|
<label name="ownership" class="switch" style="margin-left: 20px;">
|
||||||
<input type="checkbox" ng-model="config.Internal" />
|
<input type="checkbox" ng-model="config.Internal" />
|
||||||
|
|||||||
@@ -309,8 +309,8 @@
|
|||||||
<!-- volume-type -->
|
<!-- volume-type -->
|
||||||
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||||
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
<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="'volume'" ng-click="volume.Source = null">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="'bind'" ng-click="volume.Source = null">Bind</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
|
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
|
||||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
ng-model="volume.Source"
|
ng-model="volume.Source"
|
||||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </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
-1
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html
|
<!DOCTYPE html
|
||||||
><html lang="en" ng-app="<%= name %>">
|
><html lang="en" ng-app="<%= name %>" ng-strict-di>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Portainer</title>
|
<title>Portainer</title>
|
||||||
|
|||||||
+4
-3
@@ -71,7 +71,7 @@
|
|||||||
<table class="table table-hover nowrap-cells">
|
<table class="table table-hover nowrap-cells">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th style="width: 55px;">
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
@@ -142,8 +142,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
<td colspan="5">
|
<td></td>
|
||||||
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
|
<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)"
|
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
|
||||||
>external</span
|
>external</span
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-1
@@ -118,7 +118,7 @@
|
|||||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||||
</td>
|
</td>
|
||||||
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
|
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
|
||||||
<td>{{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
|
<td>{{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
|
||||||
<td ng-if="$ctrl.isAdmin">
|
<td ng-if="$ctrl.isAdmin">
|
||||||
<a ng-if="$ctrl.canManageAccess(item)" ui-sref="kubernetes.resourcePools.resourcePool.access({id: item.Namespace.Name})">
|
<a ng-if="$ctrl.canManageAccess(item)" ui-sref="kubernetes.resourcePools.resourcePool.access({id: item.Namespace.Name})">
|
||||||
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||||
|
|||||||
+11
-4
@@ -3,13 +3,13 @@
|
|||||||
Data
|
Data
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="$ctrl.isCreation">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<p>
|
<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
|
<i class="fa fa-list-ol space-right" aria-hidden="true"></i> Advanced mode
|
||||||
</a>
|
</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
|
<i class="fa fa-edit space-right" aria-hidden="true"></i> Simple mode
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -80,6 +80,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="form-group" ng-if="$ctrl.formValues.IsSimple">
|
||||||
<div class="col-sm-1"></div>
|
<div class="col-sm-1"></div>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ angular.module('portainer.kubernetes').component('kubernetesConfigurationData',
|
|||||||
bindings: {
|
bindings: {
|
||||||
formValues: '=',
|
formValues: '=',
|
||||||
isValid: '=',
|
isValid: '=',
|
||||||
|
isCreation: '=',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+31
-5
@@ -1,7 +1,10 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
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 KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
|
|
||||||
class KubernetesConfigurationDataController {
|
class KubernetesConfigurationDataController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -12,6 +15,8 @@ class KubernetesConfigurationDataController {
|
|||||||
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
|
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
|
||||||
this.onFileLoad = this.onFileLoad.bind(this);
|
this.onFileLoad = this.onFileLoad.bind(this);
|
||||||
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
|
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
|
||||||
|
this.showSimpleMode = this.showSimpleMode.bind(this);
|
||||||
|
this.showAdvancedMode = this.showAdvancedMode.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeKey() {
|
onChangeKey() {
|
||||||
@@ -20,7 +25,7 @@ class KubernetesConfigurationDataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addEntry() {
|
addEntry() {
|
||||||
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
|
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEntry(index) {
|
removeEntry(index) {
|
||||||
@@ -37,9 +42,20 @@ class KubernetesConfigurationDataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onFileLoadAsync(event) {
|
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.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.formValues.Data.push(entry);
|
||||||
this.onChangeKey();
|
this.onChangeKey();
|
||||||
}
|
}
|
||||||
@@ -53,10 +69,20 @@ class KubernetesConfigurationDataController {
|
|||||||
const temporaryFileReader = new FileReader();
|
const temporaryFileReader = new FileReader();
|
||||||
temporaryFileReader.fileName = file.name;
|
temporaryFileReader.fileName = file.name;
|
||||||
temporaryFileReader.onload = this.onFileLoad;
|
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() {
|
$onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
duplicateKeys: {},
|
duplicateKeys: {},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import YAML from 'yaml';
|
|
||||||
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
|
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
|
||||||
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
||||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||||
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
|
|
||||||
class KubernetesConfigMapConverter {
|
class KubernetesConfigMapConverter {
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +16,23 @@ class KubernetesConfigMapConverter {
|
|||||||
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||||
res.CreationDate = data.metadata.creationTimestamp;
|
res.CreationDate = data.metadata.creationTimestamp;
|
||||||
res.Yaml = yaml ? yaml.data : '';
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +55,16 @@ class KubernetesConfigMapConverter {
|
|||||||
const res = new KubernetesConfigMapCreatePayload();
|
const res = new KubernetesConfigMapCreatePayload();
|
||||||
res.metadata.name = data.Name;
|
res.metadata.name = data.Name;
|
||||||
res.metadata.namespace = data.Namespace;
|
res.metadata.namespace = data.Namespace;
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.data = data.Data;
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +77,13 @@ class KubernetesConfigMapConverter {
|
|||||||
res.metadata.name = data.Name;
|
res.metadata.name = data.Name;
|
||||||
res.metadata.namespace = data.Namespace;
|
res.metadata.namespace = data.Namespace;
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,18 +93,7 @@ class KubernetesConfigMapConverter {
|
|||||||
res.Name = formValues.Name;
|
res.Name = formValues.Name;
|
||||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||||
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
||||||
if (formValues.IsSimple) {
|
res.Data = formValues.Data;
|
||||||
res.Data = _.reduce(
|
|
||||||
formValues.Data,
|
|
||||||
(acc, entry) => {
|
|
||||||
acc[entry.Key] = entry.Value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.Data = YAML.parse(formValues.DataYaml);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import _ from 'lodash-es';
|
||||||
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
|
|
||||||
class KubernetesConfigurationConverter {
|
class KubernetesConfigurationConverter {
|
||||||
@@ -9,7 +10,9 @@ class KubernetesConfigurationConverter {
|
|||||||
res.Namespace = secret.Namespace;
|
res.Namespace = secret.Namespace;
|
||||||
res.CreationDate = secret.CreationDate;
|
res.CreationDate = secret.CreationDate;
|
||||||
res.Yaml = secret.Yaml;
|
res.Yaml = secret.Yaml;
|
||||||
res.Data = secret.Data;
|
_.forEach(secret.Data, (entry) => {
|
||||||
|
res.Data[entry.Key] = entry.Value;
|
||||||
|
});
|
||||||
res.ConfigurationOwner = secret.ConfigurationOwner;
|
res.ConfigurationOwner = secret.ConfigurationOwner;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -22,7 +25,9 @@ class KubernetesConfigurationConverter {
|
|||||||
res.Namespace = configMap.Namespace;
|
res.Namespace = configMap.Namespace;
|
||||||
res.CreationDate = configMap.CreationDate;
|
res.CreationDate = configMap.CreationDate;
|
||||||
res.Yaml = configMap.Yaml;
|
res.Yaml = configMap.Yaml;
|
||||||
res.Data = configMap.Data;
|
_.forEach(configMap.Data, (entry) => {
|
||||||
|
res.Data[entry.Key] = entry.Value;
|
||||||
|
});
|
||||||
res.ConfigurationOwner = configMap.ConfigurationOwner;
|
res.ConfigurationOwner = configMap.ConfigurationOwner;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as JsonPatch from 'fast-json-patch';
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
||||||
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
|
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import _ from 'lodash-es';
|
||||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||||
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
|
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
|
||||||
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
|
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
|
||||||
@@ -20,7 +21,8 @@ class KubernetesNamespaceConverter {
|
|||||||
res.metadata.name = namespace.Name;
|
res.metadata.name = namespace.Name;
|
||||||
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
|
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
|
||||||
if (namespace.ResourcePoolOwner) {
|
if (namespace.ResourcePoolOwner) {
|
||||||
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner;
|
const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' });
|
||||||
|
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
|
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
|
||||||
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
||||||
import YAML from 'yaml';
|
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||||
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
|
|
||||||
class KubernetesSecretConverter {
|
class KubernetesSecretConverter {
|
||||||
static createPayload(secret) {
|
static createPayload(secret) {
|
||||||
const res = new KubernetesSecretCreatePayload();
|
const res = new KubernetesSecretCreatePayload();
|
||||||
res.metadata.name = secret.Name;
|
res.metadata.name = secret.Name;
|
||||||
res.metadata.namespace = secret.Namespace;
|
res.metadata.namespace = secret.Namespace;
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||||
res.stringData = secret.Data;
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +33,19 @@ class KubernetesSecretConverter {
|
|||||||
res.metadata.name = secret.Name;
|
res.metadata.name = secret.Name;
|
||||||
res.metadata.namespace = secret.Namespace;
|
res.metadata.namespace = secret.Namespace;
|
||||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +57,21 @@ class KubernetesSecretConverter {
|
|||||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||||
res.CreationDate = payload.metadata.creationTimestamp;
|
res.CreationDate = payload.metadata.creationTimestamp;
|
||||||
res.Yaml = yaml ? yaml.data : '';
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,18 +80,7 @@ class KubernetesSecretConverter {
|
|||||||
res.Name = formValues.Name;
|
res.Name = formValues.Name;
|
||||||
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
res.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||||
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
res.ConfigurationOwner = formValues.ConfigurationOwner;
|
||||||
if (formValues.IsSimple) {
|
res.Data = formValues.Data;
|
||||||
res.Data = _.reduce(
|
|
||||||
formValues.Data,
|
|
||||||
(acc, entry) => {
|
|
||||||
acc[entry.Key] = entry.Value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.Data = YAML.parse(formValues.DataYaml);
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class KubernetesServiceConverter {
|
|||||||
payload.metadata.namespace = service.Namespace;
|
payload.metadata.namespace = service.Namespace;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
|
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
|
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.ports = service.Ports;
|
||||||
payload.spec.selector.app = service.ApplicationName;
|
payload.spec.selector.app = service.ApplicationName;
|
||||||
if (service.Headless) {
|
if (service.Headless) {
|
||||||
|
|||||||
@@ -175,7 +175,10 @@ class KubernetesApplicationHelper {
|
|||||||
item.OverridenKeys = _.map(keys, (k) => {
|
item.OverridenKeys = _.map(keys, (k) => {
|
||||||
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
||||||
fvKey.Key = k.Key;
|
fvKey.Key = k.Key;
|
||||||
if (index < k.EnvCount) {
|
if (!k.Count) {
|
||||||
|
// !k.Count indicates k.Key is new added to the configuration and has not been loaded to the application yet
|
||||||
|
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.NONE;
|
||||||
|
} else if (index < k.EnvCount) {
|
||||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
|
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
|
||||||
} else {
|
} else {
|
||||||
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;
|
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ class KubernetesCommonHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ownerToLabel(owner) {
|
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;
|
export default KubernetesCommonHelper;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
|
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
|
||||||
class KubernetesConfigurationHelper {
|
class KubernetesConfigurationHelper {
|
||||||
static getUsingApplications(config, applications) {
|
static getUsingApplications(config, applications) {
|
||||||
@@ -21,6 +23,10 @@ class KubernetesConfigurationHelper {
|
|||||||
return _.startsWith(config.Name, 'default-token-');
|
return _.startsWith(config.Name, 'default-token-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isBinary(encoding) {
|
||||||
|
return encoding !== '' && !_.includes(encoding, 'ISO') && !_.includes(encoding, 'UTF');
|
||||||
|
}
|
||||||
|
|
||||||
static setConfigurationUsed(config) {
|
static setConfigurationUsed(config) {
|
||||||
config.Used = config.Applications && config.Applications.length !== 0;
|
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) {
|
static isExternalConfiguration(configuration) {
|
||||||
return !configuration.ConfigurationOwner;
|
return !configuration.ConfigurationOwner;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export class KubernetesApplicationFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
|
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
ENVIRONMENT: 1,
|
ENVIRONMENT: 1,
|
||||||
FILESYSTEM: 2,
|
FILESYSTEM: 2,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const _KubernetesConfigMap = Object.freeze({
|
|||||||
Namespace: '',
|
Namespace: '',
|
||||||
Yaml: '',
|
Yaml: '',
|
||||||
ConfigurationOwner: '',
|
ConfigurationOwner: '',
|
||||||
Data: {},
|
Data: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesConfigMap {
|
export class KubernetesConfigMap {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloa
|
|||||||
const _KubernetesConfigMapCreatePayload = Object.freeze({
|
const _KubernetesConfigMapCreatePayload = Object.freeze({
|
||||||
metadata: new KubernetesCommonMetadataPayload(),
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
data: {},
|
data: {},
|
||||||
|
binaryData: {},
|
||||||
});
|
});
|
||||||
export class KubernetesConfigMapCreatePayload {
|
export class KubernetesConfigMapCreatePayload {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -19,6 +20,7 @@ export class KubernetesConfigMapCreatePayload {
|
|||||||
const _KubernetesConfigMapUpdatePayload = Object.freeze({
|
const _KubernetesConfigMapUpdatePayload = Object.freeze({
|
||||||
metadata: new KubernetesCommonMetadataPayload(),
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
data: {},
|
data: {},
|
||||||
|
binaryData: {},
|
||||||
});
|
});
|
||||||
export class KubernetesConfigMapUpdatePayload {
|
export class KubernetesConfigMapUpdatePayload {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -20,16 +20,14 @@ export class KubernetesConfigurationFormValues {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const _KubernetesConfigurationFormValuesEntry = Object.freeze({
|
||||||
* KubernetesConfigurationEntry Model
|
|
||||||
*/
|
|
||||||
const _KubernetesConfigurationFormValuesDataEntry = Object.freeze({
|
|
||||||
Key: '',
|
Key: '',
|
||||||
Value: '',
|
Value: '',
|
||||||
|
IsBinary: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesConfigurationFormValuesDataEntry {
|
export class KubernetesConfigurationFormValuesEntry {
|
||||||
constructor() {
|
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 KubernetesPortainerConfigurationOwnerLabel = 'io.portainer.kubernetes.configuration.owner';
|
||||||
|
export const KubernetesPortainerConfigurationDataAnnotation = 'io.portainer.kubernetes.configuration.data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration Model (Composite)
|
* Configuration Model (Composite)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const _KubernetesApplicationSecret = Object.freeze({
|
|||||||
CreationDate: '',
|
CreationDate: '',
|
||||||
ConfigurationOwner: '',
|
ConfigurationOwner: '',
|
||||||
Yaml: '',
|
Yaml: '',
|
||||||
Data: {},
|
Data: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationSecret {
|
export class KubernetesApplicationSecret {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const _KubernetesSecretCreatePayload = Object.freeze({
|
|||||||
metadata: new KubernetesCommonMetadataPayload(),
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
type: 'Opaque',
|
type: 'Opaque',
|
||||||
data: {},
|
data: {},
|
||||||
|
stringData: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesSecretCreatePayload {
|
export class KubernetesSecretCreatePayload {
|
||||||
@@ -22,6 +23,7 @@ const _KubernetesSecretUpdatePayload = Object.freeze({
|
|||||||
metadata: new KubernetesCommonMetadataPayload(),
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
type: 'Opaque',
|
type: 'Opaque',
|
||||||
data: {},
|
data: {},
|
||||||
|
stringData: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesSecretUpdatePayload {
|
export class KubernetesSecretUpdatePayload {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
import _ from 'lodash-es';
|
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) {
|
function computeStatus(statuses) {
|
||||||
const containerStatuses = _.map(statuses, 'state');
|
const containerStatuses = _.map(statuses, 'state');
|
||||||
@@ -104,4 +115,48 @@ export default class KubernetesPodConverter {
|
|||||||
res.Tolerations = computeTolerations(data.spec.tolerations);
|
res.Tolerations = computeTolerations(data.spec.tolerations);
|
||||||
return res;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
|
import KubernetesPodConverter from './converter';
|
||||||
|
|
||||||
class KubernetesPodService {
|
class KubernetesPodService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -13,6 +14,7 @@ class KubernetesPodService {
|
|||||||
this.getAllAsync = this.getAllAsync.bind(this);
|
this.getAllAsync = this.getAllAsync.bind(this);
|
||||||
this.logsAsync = this.logsAsync.bind(this);
|
this.logsAsync = this.logsAsync.bind(this);
|
||||||
this.deleteAsync = this.deleteAsync.bind(this);
|
this.deleteAsync = this.deleteAsync.bind(this);
|
||||||
|
this.patchAsync = this.patchAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAsync(namespace, name) {
|
async getAsync(namespace, name) {
|
||||||
@@ -74,6 +76,29 @@ class KubernetesPodService {
|
|||||||
return this.$async(this.logsAsync, namespace, podName, containerName);
|
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
|
* DELETE
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
|
|||||||
create: { method: 'POST' },
|
create: { method: 'POST' },
|
||||||
update: { method: 'PUT' },
|
update: { method: 'PUT' },
|
||||||
delete: { method: 'DELETE' },
|
delete: { method: 'DELETE' },
|
||||||
|
patch: {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
},
|
||||||
logs: {
|
logs: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: { action: 'log' },
|
params: { action: 'log' },
|
||||||
|
|||||||
@@ -28,8 +28,19 @@ class KubernetesConfigMapService {
|
|||||||
this.KubernetesConfigMaps(namespace).get(params).$promise,
|
this.KubernetesConfigMaps(namespace).get(params).$promise,
|
||||||
this.KubernetesConfigMaps(namespace).getYaml(params).$promise,
|
this.KubernetesConfigMaps(namespace).getYaml(params).$promise,
|
||||||
]);
|
]);
|
||||||
const configMap = KubernetesConfigMapConverter.apiToConfigMap(rawPromise.value, yamlPromise.value);
|
|
||||||
return configMap;
|
if (_.get(rawPromise, 'reason.status') == 404 && _.get(yamlPromise, 'reason.status') == 404) {
|
||||||
|
return KubernetesConfigMapConverter.defaultConfigMap(namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saving binary data to 'data' field in configMap Object is not allowed by kubernetes and getYaml() may get
|
||||||
|
// an error. We should keep binary data to 'binaryData' field instead of 'data'. Before that, we
|
||||||
|
// use response from get() and ignore 500 error as a workaround.
|
||||||
|
if (rawPromise.value) {
|
||||||
|
return KubernetesConfigMapConverter.apiToConfigMap(rawPromise.value, yamlPromise.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PortainerError('Unable to retrieve config map ', name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 404) {
|
if (err.status === 404) {
|
||||||
return KubernetesConfigMapConverter.defaultConfigMap(namespace, name);
|
return KubernetesConfigMapConverter.defaultConfigMap(namespace, name);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<information-panel title-text="Advanced deployment">
|
||||||
|
<span class="small">
|
||||||
|
<p class="text-muted">
|
||||||
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy"> <i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment </button>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
||||||
@@ -5,19 +5,7 @@
|
|||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<div ng-if="ctrl.state.viewReady">
|
||||||
<information-panel title-text="Advanced deployment" ng-if="ctrl.state.isAdmin">
|
<div ng-if="ctrl.state.isAdmin" ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
|
||||||
<span class="small">
|
|
||||||
<p class="text-muted">
|
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
|
||||||
As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy">
|
|
||||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
</information-panel>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require('../../templates/advancedDeploymentPanel.html');
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||||
|
|||||||
@@ -92,8 +92,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||||
<div class="col-sm-12 small text-warning">
|
<div class="col-sm-12 small text-danger">
|
||||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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
|
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.
|
resource pool.
|
||||||
</div>
|
</div>
|
||||||
@@ -634,8 +634,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-danger">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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
|
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.
|
resource pool.
|
||||||
</div>
|
</div>
|
||||||
@@ -813,8 +813,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
|
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-danger">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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.
|
This application would exceed available resources. Please review resource reservations or the instance count.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -930,8 +930,8 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px;">
|
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px;">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-danger">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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.
|
This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1299,7 +1299,7 @@
|
|||||||
ng-min="1"
|
ng-min="1"
|
||||||
ng-max="65535"
|
ng-max="65535"
|
||||||
ng-required="!publishedPort.NeedsDeletion"
|
ng-required="!publishedPort.NeedsDeletion"
|
||||||
ng-change="ctrl.onChangePortMappingLoadBalancerPort()"
|
ng-change="ctrl.onChangePortMappingLoadBalancer()"
|
||||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1353,7 +1353,7 @@
|
|||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
ng-model="publishedPort.Protocol"
|
ng-model="publishedPort.Protocol"
|
||||||
uib-btn-radio="'TCP'"
|
uib-btn-radio="'TCP'"
|
||||||
ng-change="ctrl.onChangePortMappingContainerPort()"
|
ng-change="ctrl.onChangePortProtocol($index)"
|
||||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
|
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
|
||||||
>TCP</label
|
>TCP</label
|
||||||
>
|
>
|
||||||
@@ -1361,7 +1361,7 @@
|
|||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
ng-model="publishedPort.Protocol"
|
ng-model="publishedPort.Protocol"
|
||||||
uib-btn-radio="'UDP'"
|
uib-btn-radio="'UDP'"
|
||||||
ng-change="ctrl.onChangePortMappingContainerPort()"
|
ng-change="ctrl.onChangePortProtocol($index)"
|
||||||
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
|
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
|
||||||
>UDP</label
|
>UDP</label
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -303,6 +303,9 @@ class KubernetesCreateApplicationController {
|
|||||||
const ingresses = this.filteredIngresses;
|
const ingresses = this.filteredIngresses;
|
||||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : 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);
|
this.formValues.PublishedPorts.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +338,7 @@ class KubernetesCreateApplicationController {
|
|||||||
this.onChangePortMappingNodePort();
|
this.onChangePortMappingNodePort();
|
||||||
this.onChangePortMappingIngressRoute();
|
this.onChangePortMappingIngressRoute();
|
||||||
this.onChangePortMappingLoadBalancer();
|
this.onChangePortMappingLoadBalancer();
|
||||||
|
this.onChangePortProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangePortMappingContainerPort() {
|
onChangePortMappingContainerPort() {
|
||||||
@@ -403,6 +407,17 @@ class KubernetesCreateApplicationController {
|
|||||||
state.hasDuplicates = false;
|
state.hasDuplicates = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangePortProtocol(index) {
|
||||||
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||||
|
const newPorts = _.filter(this.formValues.PublishedPorts, { IsNew: true });
|
||||||
|
_.forEach(newPorts, (port) => {
|
||||||
|
port.Protocol = index ? this.formValues.PublishedPorts[index].Protocol : newPorts[0].Protocol;
|
||||||
|
});
|
||||||
|
this.onChangePortMappingLoadBalancer();
|
||||||
|
}
|
||||||
|
this.onChangePortMappingContainerPort();
|
||||||
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region STATE VALIDATION FUNCTIONS */
|
/* #region STATE VALIDATION FUNCTIONS */
|
||||||
@@ -561,6 +576,10 @@ class KubernetesCreateApplicationController {
|
|||||||
return this.state.isEdit && !this.formValues.Placements[index].IsNew;
|
return this.state.isEdit && !this.formValues.Placements[index].IsNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isNewAndNotFirst(index) {
|
||||||
|
return !this.state.isEdit && index !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
showPlacementPolicySection() {
|
showPlacementPolicySection() {
|
||||||
const placements = _.filter(this.formValues.Placements, { NeedsDeletion: false });
|
const placements = _.filter(this.formValues.Placements, { NeedsDeletion: false });
|
||||||
return placements.length !== 0;
|
return placements.length !== 0;
|
||||||
@@ -600,8 +619,17 @@ class KubernetesCreateApplicationController {
|
|||||||
return this.state.isEdit && this.formValues.PublishedPorts.length > 0 && ports.length > 0;
|
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) {
|
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 */
|
/* #endregion */
|
||||||
|
|||||||
@@ -55,9 +55,7 @@
|
|||||||
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
|
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
|
||||||
<td>
|
<td>
|
||||||
<div>Resource reservations</div>
|
<div>Resource reservations</div>
|
||||||
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small">
|
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small"> per instance </div>
|
||||||
per instance
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
|
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
|
||||||
@@ -133,7 +131,13 @@
|
|||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
<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;">
|
<div class="small text-muted" style="padding: 20px;">
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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.
|
The placement component helps you understand whether or not this application can be deployed on a specific node.
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ class KubernetesApplicationController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.placements = computePlacements(nodes, this.application);
|
this.placements = computePlacements(nodes, this.application);
|
||||||
|
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -331,6 +332,7 @@ class KubernetesApplicationController {
|
|||||||
name: this.$transition$.params().name,
|
name: this.$transition$.params().name,
|
||||||
},
|
},
|
||||||
eventWarningCount: 0,
|
eventWarningCount: 0,
|
||||||
|
placementWarning: false,
|
||||||
expandedNote: false,
|
expandedNote: false,
|
||||||
useIngress: false,
|
useIngress: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<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="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<kubernetes-configurations-datatable
|
<kubernetes-configurations-datatable
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
require('../../templates/advancedDeploymentPanel.html');
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
|
|
||||||
class KubernetesConfigurationsController {
|
class KubernetesConfigurationsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService, ModalService) {
|
constructor($async, $state, Notifications, Authentication, KubernetesConfigurationService, KubernetesApplicationService, ModalService) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
this.Authentication = Authentication;
|
||||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
@@ -93,6 +96,7 @@ class KubernetesConfigurationsController {
|
|||||||
configurationsLoading: true,
|
configurationsLoading: true,
|
||||||
applicationsLoading: true,
|
applicationsLoading: true,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
|
isAdmin: this.Authentication.isAdmin(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.getApplications();
|
await this.getApplications();
|
||||||
|
|||||||
@@ -106,7 +106,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- !type options -->
|
<!-- !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 -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
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 { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
|
|
||||||
class KubernetesCreateConfigurationController {
|
class KubernetesCreateConfigurationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -41,6 +42,9 @@ class KubernetesCreateConfigurationController {
|
|||||||
try {
|
try {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
|
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
|
||||||
|
if (!this.formValues.IsSimple) {
|
||||||
|
this.formValues.Data = KubernetesConfigurationHelper.parseYaml(this.formValues);
|
||||||
|
}
|
||||||
await this.KubernetesConfigurationService.create(this.formValues);
|
await this.KubernetesConfigurationService.create(this.formValues);
|
||||||
this.Notifications.success('Configuration succesfully created');
|
this.Notifications.success('Configuration succesfully created');
|
||||||
this.$state.go('kubernetes.configurations');
|
this.$state.go('kubernetes.configurations');
|
||||||
@@ -76,7 +80,7 @@ class KubernetesCreateConfigurationController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.formValues = new KubernetesConfigurationFormValues();
|
this.formValues = new KubernetesConfigurationFormValues();
|
||||||
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
|
this.formValues.Data.push(new KubernetesConfigurationFormValuesEntry());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||||
|
|||||||
@@ -77,7 +77,12 @@
|
|||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form ng-if="!ctrl.isSystemNamespace()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
<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 -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import angular from 'angular';
|
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 { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||||
|
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ class KubernetesConfigurationController {
|
|||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
KubernetesConfigurationService,
|
KubernetesConfigurationService,
|
||||||
|
KubernetesConfigMapService,
|
||||||
|
KubernetesSecretService,
|
||||||
KubernetesResourcePoolService,
|
KubernetesResourcePoolService,
|
||||||
ModalService,
|
ModalService,
|
||||||
KubernetesApplicationService,
|
KubernetesApplicationService,
|
||||||
@@ -32,6 +35,8 @@ class KubernetesConfigurationController {
|
|||||||
this.KubernetesEventService = KubernetesEventService;
|
this.KubernetesEventService = KubernetesEventService;
|
||||||
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
|
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
|
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||||
|
this.KubernetesSecretService = KubernetesSecretService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.getConfigurationAsync = this.getConfigurationAsync.bind(this);
|
this.getConfigurationAsync = this.getConfigurationAsync.bind(this);
|
||||||
@@ -126,7 +131,18 @@ class KubernetesConfigurationController {
|
|||||||
this.state.configurationLoading = true;
|
this.state.configurationLoading = true;
|
||||||
const name = this.$transition$.params().name;
|
const name = this.$transition$.params().name;
|
||||||
const namespace = this.$transition$.params().namespace;
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
|
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -211,20 +227,6 @@ class KubernetesConfigurationController {
|
|||||||
await this.getConfiguration();
|
await this.getConfiguration();
|
||||||
await this.getApplications(this.configuration.Namespace);
|
await this.getApplications(this.configuration.Namespace);
|
||||||
await this.getEvents(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();
|
await this.getConfigurations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<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;">
|
<p style="margin-top: 2px;">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<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.
|
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">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">Ingress controller</label>
|
<label class="control-label text-left">Ingress controller</label>
|
||||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addIngressClass()">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -58,19 +58,19 @@
|
|||||||
<table class="table table-hover nowrap-cells">
|
<table class="table table-hover nowrap-cells">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 10%;">
|
<th style="width: 20px;">
|
||||||
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
|
<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>
|
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 55%;">
|
<th style="width: 60%;">
|
||||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
Storage
|
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-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>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th style="width: 35%;">
|
<th style="width: 38%;">
|
||||||
<a ng-click="$ctrl.changeOrderBy('Size')">
|
<a ng-click="$ctrl.changeOrderBy('Size')">
|
||||||
Usage
|
Usage
|
||||||
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
|
<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>
|
<td>{{ item.Size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="vol in item.Volumes" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="vol in item.Volumes" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
<td colspan="2">
|
<td></td>
|
||||||
<a ui-sref="kubernetes.volumes.volume({ name: vol.PersistentVolumeClaim.Name, namespace: vol.PersistentVolumeClaim.Namespace })" style="margin-left: 25px;">
|
<td>
|
||||||
|
<a ui-sref="kubernetes.volumes.volume({ name: vol.PersistentVolumeClaim.Name, namespace: vol.PersistentVolumeClaim.Namespace })">
|
||||||
{{ vol.PersistentVolumeClaim.Name }}
|
{{ vol.PersistentVolumeClaim.Name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -37,6 +37,17 @@
|
|||||||
<td>Storage</td>
|
<td>Storage</td>
|
||||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
|
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>Provisioner</td>
|
<td>Provisioner</td>
|
||||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.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 _ from 'lodash-es';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
|
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
|
|
||||||
class KubernetesVolumeController {
|
class KubernetesVolumeController {
|
||||||
@@ -179,6 +180,8 @@ class KubernetesVolumeController {
|
|||||||
volumeSize: 0,
|
volumeSize: 0,
|
||||||
volumeSizeUnit: 'GB',
|
volumeSizeUnit: 'GB',
|
||||||
volumeSizeError: false,
|
volumeSizeError: false,
|
||||||
|
volumeSharedAccessPolicy: '',
|
||||||
|
volumeSharedAccessPolicyTooltip: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
||||||
@@ -186,6 +189,16 @@ class KubernetesVolumeController {
|
|||||||
try {
|
try {
|
||||||
await this.getVolume();
|
await this.getVolume();
|
||||||
await this.getEvents();
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
<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="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require('../../templates/advancedDeploymentPanel.html');
|
||||||
|
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
@@ -39,10 +41,22 @@ function computeSize(volumes) {
|
|||||||
|
|
||||||
class KubernetesVolumesController {
|
class KubernetesVolumesController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, ModalService, LocalStorage, EndpointProvider, KubernetesStorageService, KubernetesVolumeService, KubernetesApplicationService) {
|
constructor(
|
||||||
|
$async,
|
||||||
|
$state,
|
||||||
|
Notifications,
|
||||||
|
Authentication,
|
||||||
|
ModalService,
|
||||||
|
LocalStorage,
|
||||||
|
EndpointProvider,
|
||||||
|
KubernetesStorageService,
|
||||||
|
KubernetesVolumeService,
|
||||||
|
KubernetesApplicationService
|
||||||
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
this.Authentication = Authentication;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
this.EndpointProvider = EndpointProvider;
|
this.EndpointProvider = EndpointProvider;
|
||||||
@@ -117,6 +131,7 @@ class KubernetesVolumesController {
|
|||||||
currentName: this.$state.$current.name,
|
currentName: this.$state.$current.name,
|
||||||
endpointId: this.EndpointProvider.endpointID(),
|
endpointId: this.EndpointProvider.endpointID(),
|
||||||
activeTab: this.LocalStorage.getActiveTab('volumes'),
|
activeTab: this.LocalStorage.getActiveTab('volumes'),
|
||||||
|
isAdmin: this.Authentication.isAdmin(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.getVolumes();
|
await this.getVolumes();
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
export default class DatatableColumnsVisibilityController {
|
||||||
|
constructor() {
|
||||||
|
this.state = {
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -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',
|
refreshRate: '30',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.resetSelectionState = function () {
|
this.resetSelectionState = function () {
|
||||||
this.state.selectAll = false;
|
this.state.selectAll = false;
|
||||||
this.state.selectedItems = [];
|
this.state.selectedItems = [];
|
||||||
@@ -158,6 +159,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
|||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
}
|
}
|
||||||
this.onSettingsRepeaterChange();
|
this.onSettingsRepeaterChange();
|
||||||
|
|
||||||
|
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||||
|
if (storedColumnVisibility !== null) {
|
||||||
|
this.columnVisibility = storedColumnVisibility;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="toolBar">
|
<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="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
<div class="settings">
|
<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 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>
|
<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>
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
@@ -117,6 +118,20 @@
|
|||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>Control</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>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
||||||
Ownership
|
Ownership
|
||||||
@@ -154,6 +169,14 @@
|
|||||||
</span>
|
</span>
|
||||||
<span ng-if="!item.External">Total</span>
|
<span ng-if="!item.External">Total</span>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
@@ -162,10 +185,10 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<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>
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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
|
* Do not allow external items
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +89,11 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
|||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
}
|
}
|
||||||
this.onSettingsRepeaterChange();
|
this.onSettingsRepeaterChange();
|
||||||
|
|
||||||
|
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||||
|
if (storedColumnVisibility !== null) {
|
||||||
|
this.columnVisibility = storedColumnVisibility;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
|
import { ExternalStackViewModel } from '@/portainer/models/stack';
|
||||||
|
|
||||||
angular.module('portainer.app').factory('StackHelper', [
|
angular.module('portainer.app').factory('StackHelper', [
|
||||||
function StackHelperFactory() {
|
function StackHelperFactory() {
|
||||||
'use strict';
|
'use strict';
|
||||||
var helper = {};
|
var helper = {};
|
||||||
|
|
||||||
helper.getExternalStackNamesFromContainers = function (containers) {
|
helper.getExternalStacksFromContainers = function (containers) {
|
||||||
var stackNames = [];
|
return getExternalStacksFromLabel(containers, 'com.docker.compose.project', 2);
|
||||||
|
|
||||||
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.getExternalStackNamesFromServices = function (services) {
|
helper.getExternalStacksFromServices = function (services) {
|
||||||
var stackNames = [];
|
return getExternalStacksFromLabel(services, 'com.docker.stack.namespace', 1);
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
return helper;
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -13,11 +13,16 @@ export function StackViewModel(data) {
|
|||||||
}
|
}
|
||||||
this.External = false;
|
this.External = false;
|
||||||
this.Status = data.Status;
|
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.Name = name;
|
||||||
this.Type = type;
|
this.Type = type;
|
||||||
this.External = true;
|
this.External = true;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
|
this.CreationDate = creationDate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { ExternalStackViewModel, StackViewModel } from '../../models/stack';
|
import { StackViewModel } from '../../models/stack';
|
||||||
|
|
||||||
angular.module('portainer.app').factory('StackService', [
|
angular.module('portainer.app').factory('StackService', [
|
||||||
'$q',
|
'$q',
|
||||||
@@ -121,13 +121,8 @@ angular.module('portainer.app').factory('StackService', [
|
|||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
ServiceService.services()
|
ServiceService.services()
|
||||||
.then(function success(data) {
|
.then(function success(services) {
|
||||||
var services = data;
|
deferred.resolve(StackHelper.getExternalStacksFromServices(services));
|
||||||
var stackNames = StackHelper.getExternalStackNamesFromServices(services);
|
|
||||||
var stacks = stackNames.map(function (name) {
|
|
||||||
return new ExternalStackViewModel(name, 1);
|
|
||||||
});
|
|
||||||
deferred.resolve(stacks);
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to retrieve external stacks', err: 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();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
ContainerService.containers(1)
|
ContainerService.containers(1)
|
||||||
.then(function success(data) {
|
.then(function success(containers) {
|
||||||
var containers = data;
|
deferred.resolve(StackHelper.getExternalStacksFromContainers(containers));
|
||||||
var stackNames = StackHelper.getExternalStackNamesFromContainers(containers);
|
|
||||||
var stacks = stackNames.map(function (name) {
|
|
||||||
return new ExternalStackViewModel(name, 2);
|
|
||||||
});
|
|
||||||
deferred.resolve(stacks);
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to retrieve external stacks', err: err });
|
deferred.reject({ msg: 'Unable to retrieve external stacks', err: err });
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user