Compare commits

..

5 Commits

Author SHA1 Message Date
yi-portainer
51295e481a * bump the APIVersion to 2.0.1 2021-01-07 14:28:52 +13:00
Yi Chen
b78d804881 Revert "chore(build): bump Kompose version (#4475)" (#4676)
This reverts commit 380f106571.

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2020-12-30 23:03:43 +13:00
Anthony Lapenna
51b72c12f9 fix(docker/stack-details): do not display editor tab for external stack (#4650) 2020-12-23 14:45:32 +13:00
Yi Chen
58c04bdbe3 + silently continue when downloading artifacts in windows (#4637) 2020-12-22 13:47:11 +13:00
cong meng
a6320d5222 fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)
* fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

* fix(frontend) rephrase comments (#4629)

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

Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2020-12-22 13:38:54 +13:00
183 changed files with 1183 additions and 2779 deletions

View File

@@ -1,10 +1,6 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--
@@ -13,7 +9,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/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
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
@@ -44,7 +40,6 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
- Browser:

View File

@@ -1,20 +1,17 @@
---
name: Question
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.slack.com/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Question**:
How can I deploy Portainer on... ?
---
name: Question
about: Ask us a question about Portainer usage or deployment
---
<!--
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/
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Question**:
How can I deploy Portainer on... ?

View File

@@ -1,34 +1,31 @@
---
name: Feature request
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.slack.com/
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
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
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.
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
---
<!--
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/
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
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
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.

1
.github/stale.yml vendored
View File

@@ -15,7 +15,6 @@ issues:
- kind/question
- kind/style
- kind/workaround
- kind/refactor
- bug/need-confirmation
- bug/confirmed
- status/discuss

View File

@@ -1,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -74,23 +74,3 @@ 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.
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## 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/>.

View File

@@ -28,15 +28,14 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
## Getting started
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://documentation.portainer.io)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
## Getting help
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
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
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/
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://documentation.portainer.io

View File

@@ -40,11 +40,18 @@ func (store *Store) Init() error {
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowHostNamespaceForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)

View File

@@ -1,51 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateEndpointSettingsToDB25() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for i := range endpoints {
endpoint := endpoints[i]
securitySettings := portainer.EndpointSecuritySettings{}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.DockerEnvironment {
securitySettings = portainer.EndpointSecuritySettings{
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
}
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
}
}
endpoint.SecuritySettings = securitySettings
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -342,13 +342,5 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
err := m.updateEndpointSettingsToDB25()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -4,7 +4,6 @@ import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"strings"
"github.com/boltdb/bolt"
)
@@ -48,8 +47,6 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
username = strings.ToLower(username)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
@@ -61,7 +58,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
return err
}
if strings.ToLower(u.Username) == username {
if u.Username == username {
user = &u
break
}
@@ -126,7 +123,6 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
user.Username = strings.ToLower(user.Username)
return internal.UpdateObject(service.db, BucketName, identifier, user)
}
@@ -137,7 +133,6 @@ func (service *Service) CreateUser(user *portainer.User) error {
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)
data, err := internal.MarshalObject(user)
if err != nil {

View File

@@ -6,7 +6,7 @@ import (
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
@@ -17,8 +17,6 @@ import (
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
@@ -73,12 +71,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
return store
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
if composeWrapper != nil {
return composeWrapper
}
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
@@ -96,10 +89,6 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
return nil, err
}
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
dataStore.Settings().UpdateSettings(settings)
}
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
if err != nil {
return nil, err
@@ -248,18 +237,6 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
@@ -309,18 +286,6 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
err := snapshotService.SnapshotEndpoint(endpoint)
@@ -415,10 +380,8 @@ func main() {
if err != nil {
log.Fatal(err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
@@ -485,29 +448,27 @@ func main() {
}
var server portainer.Server = &http.Server{
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
ReverseTunnelService: reverseTunnelService,
Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSL: *flags.SSL,
SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
}
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)

View File

@@ -1,132 +0,0 @@
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
}

View File

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

View File

@@ -1,143 +0,0 @@
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))
}
})
}
}

View File

@@ -134,8 +134,6 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
if !endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
} else {
args = append(args, "--tlscacert", "''")
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {

View File

@@ -1,24 +0,0 @@
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
}

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/edge"
@@ -440,18 +440,6 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
}
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
err := handler.DataStore.Endpoint().CreateEndpoint(endpoint)
if err != nil {
return err

View File

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

View File

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

View File

@@ -1,90 +0,0 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
type endpointSettingsUpdatePayload struct {
AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers"`
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures"`
}
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id/settings
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointSettingsUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
securitySettings := endpoint.SecuritySettings
if payload.AllowBindMountsForRegularUsers != nil {
securitySettings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
}
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
securitySettings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
}
if payload.AllowDeviceMappingForRegularUsers != nil {
securitySettings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
if payload.AllowHostNamespaceForRegularUsers != nil {
securitySettings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.AllowPrivilegedModeForRegularUsers != nil {
securitySettings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
}
if payload.AllowStackManagementForRegularUsers != nil {
securitySettings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
}
if payload.AllowVolumeBrowserForRegularUsers != nil {
securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
}
if payload.EnableHostManagementFeatures != nil {
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}
endpoint.SecuritySettings = securitySettings
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
}
return response.JSON(w, endpoint)
}

View File

@@ -27,7 +27,6 @@ type Handler struct {
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
ComposeStackManager portainer.ComposeStackManager
}
// NewHandler creates a handler to manage endpoint operations.
@@ -39,8 +38,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/endpoints",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/snapshot",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",

View File

@@ -7,7 +7,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
registry.Username = *payload.Username
}
if payload.Password != nil && *payload.Password != "" {
if payload.Password != nil {
registry.Password = *payload.Password
}

View File

@@ -10,11 +10,19 @@ import (
)
type publicSettingsResponse struct {
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
OAuthLoginURI string `json:"OAuthLoginURI"`
EnableTelemetry bool `json:"EnableTelemetry"`
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
OAuthLoginURI string `json:"OAuthLoginURI"`
EnableTelemetry bool `json:"EnableTelemetry"`
}
// GET request on /api/settings/public
@@ -25,10 +33,18 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
}
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,

View File

@@ -14,17 +14,25 @@ import (
)
type settingsUpdatePayload struct {
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
UserSessionTimeout *string
EnableTelemetry *bool
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
AllowHostNamespaceForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
AllowDeviceMappingForRegularUsers *bool
AllowStackManagementForRegularUsers *bool
AllowContainerCapabilitiesForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
UserSessionTimeout *string
EnableTelemetry *bool
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -99,10 +107,38 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.OAuthSettings.ClientSecret = clientSecret
}
if payload.AllowBindMountsForRegularUsers != nil {
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
}
if payload.AllowPrivilegedModeForRegularUsers != nil {
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
}
if payload.AllowVolumeBrowserForRegularUsers != nil {
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
}
if payload.EnableHostManagementFeatures != nil {
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}
if payload.EnableEdgeComputeFeatures != nil {
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.AllowHostNamespaceForRegularUsers != nil {
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.AllowStackManagementForRegularUsers != nil {
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
}
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {
@@ -122,6 +158,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
handler.JWTService.SetUserSessionDuration(userSessionDuration)
}
if payload.AllowDeviceMappingForRegularUsers != nil {
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
if payload.EnableTelemetry != nil {
settings.EnableTelemetry = *payload.EnableTelemetry
}

View File

@@ -7,12 +7,11 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -61,14 +60,13 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -91,8 +89,6 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -150,14 +146,13 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
@@ -190,8 +185,6 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -249,14 +242,13 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerComposeStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -279,8 +271,6 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -339,27 +329,31 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
// clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
securitySettings := &config.endpoint.SecuritySettings
if (!securitySettings.AllowBindMountsForRegularUsers ||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
if (!settings.AllowBindMountsForRegularUsers ||
!settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers ||
!settings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
err = handler.isValidStackFile(stackContent, securitySettings)
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}

View File

@@ -6,12 +6,11 @@ import (
"path"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
@@ -56,15 +55,14 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -87,8 +85,6 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -149,15 +145,14 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: payload.ComposeFilePathInRepository,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
@@ -190,8 +185,6 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -256,15 +249,14 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
ID: portainer.StackID(stackID),
Name: payload.Name,
Type: portainer.DockerSwarmStack,
SwarmID: payload.SwarmID,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ComposeFileDefaultName,
Env: payload.Env,
Status: portainer.StackStatusActive,
}
stackFolder := strconv.Itoa(int(stack.ID))
@@ -287,8 +279,6 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.CreatedBy = config.user.Username
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
@@ -344,12 +334,15 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
}
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
settings := &config.endpoint.SecuritySettings
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)

View File

@@ -7,7 +7,7 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -78,17 +78,6 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
func (handler *Handler) userIsAdmin(userID portainer.UserID) (bool, error) {
user, err := handler.DataStore.User().User(userID)
if err != nil {
return false, err
}
isAdmin := user.Role == portainer.AdministratorRole
return isAdmin, nil
}
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
isAdmin := user.Role == portainer.AdministratorRole

View File

@@ -46,14 +46,12 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
if !settings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
@@ -71,6 +69,13 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
@@ -124,7 +129,7 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return err
@@ -149,7 +154,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin
for key := range composeConfig.Services {
service := composeConfig.Services[key]
if !securitySettings.AllowBindMountsForRegularUsers {
if !settings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return errors.New("bind-mount disabled for non administrator users")
@@ -157,19 +162,19 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin
}
}
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
return errors.New("privileged mode disabled for non administrator users")
}
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users")
}
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
return errors.New("device mapping disabled for non administrator users")
}
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users")
}
}
@@ -178,20 +183,9 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin
}
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
var resourceControl *portainer.ResourceControl
resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
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)
err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
}

View File

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

View File

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

View File

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

View File

@@ -4,13 +4,12 @@ import (
"errors"
"net/http"
"strconv"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
@@ -136,9 +135,6 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
@@ -167,9 +163,6 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
return configErr
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
err = handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}

View File

@@ -9,7 +9,7 @@ import (
"net/http"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@@ -181,7 +181,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
}
if !isAdminOrEndpointAdmin {
securitySettings, err := transport.fetchEndpointSecuritySettings()
settings, err := transport.dataStore.Settings().Settings()
if err != nil {
return nil, err
}
@@ -197,23 +197,23 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
return nil, err
}
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
return forbiddenResponse, errors.New("forbidden to use privileged mode")
}
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
}
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
return forbiddenResponse, errors.New("forbidden to use device mapping")
}
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
return nil, errors.New("forbidden to use container capabilities")
}
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
return forbiddenResponse, errors.New("forbidden to use bind mounts")
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -111,7 +111,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
}
if !isAdminOrEndpointAdmin {
securitySettings, err := transport.fetchEndpointSecuritySettings()
settings, err := transport.dataStore.Settings().Settings()
if err != nil {
return nil, err
}
@@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
return nil, err
}
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
if mount.Type == "bind" {
return forbiddenResponse, errors.New("forbidden to use bind mounts")

View File

@@ -11,7 +11,7 @@ import (
"strings"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
@@ -407,12 +407,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
if tokenData.Role != portainer.AdministratorRole {
if volumeBrowseRestrictionCheck {
securitySettings, err := transport.fetchEndpointSecuritySettings()
settings, err := transport.dataStore.Settings().Settings()
if err != nil {
return nil, err
}
if !securitySettings.AllowVolumeBrowserForRegularUsers {
if !settings.AllowVolumeBrowserForRegularUsers {
return responseutils.WriteAccessDeniedResponse()
}
}
@@ -682,12 +682,3 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
return tokenData.Role == portainer.AdministratorRole, nil
}
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
if err != nil {
return nil, err
}
return &endpoint.SecuritySettings, nil
}

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package kubernetes
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/portainer/portainer/api/http/security"
@@ -14,16 +13,14 @@ import (
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
httpTransport *http.Transport
tokenManager *tokenManager
}
agentTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
}
edgeTransport struct {
@@ -53,11 +50,21 @@ func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
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))
return transport.httpTransport.RoundTrip(request)
@@ -78,11 +85,21 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
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)
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@@ -110,11 +127,21 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
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)
response, err := transport.httpTransport.RoundTrip(request)
@@ -127,27 +154,3 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
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
}

View File

@@ -1,7 +1,6 @@
package proxy
import (
"fmt"
"net/http"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
@@ -22,7 +21,6 @@ type (
proxyFactory *factory.ProxyFactory
endpointProxies cmap.ConcurrentMap
legacyExtensionProxies cmap.ConcurrentMap
k8sClientFactory *cli.ClientFactory
}
)
@@ -31,7 +29,6 @@ func NewManager(dataStore portainer.DataStore, signatureService portainer.Digita
return &Manager{
endpointProxies: cmap.New(),
legacyExtensionProxies: cmap.New(),
k8sClientFactory: kubernetesClientFactory,
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
}
}
@@ -44,19 +41,13 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
return nil, err
}
manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy)
manager.endpointProxies.Set(string(endpoint.ID), proxy)
return proxy, nil
}
// CreateComposeProxyServer creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
}
// GetEndpointProxy returns the proxy associated to a key
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
proxy, ok := manager.endpointProxies.Get(fmt.Sprint(endpoint.ID))
proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))
if !ok {
return nil
}
@@ -65,11 +56,8 @@ func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Hand
}
// 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) {
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
manager.k8sClientFactory.RemoveKubeClient(endpoint)
manager.endpointProxies.Remove(string(endpoint.ID))
}
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies

View File

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

View File

@@ -6,21 +6,6 @@ import (
"github.com/portainer/portainer/api"
)
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
// identifier and type parameters.
func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl {
return &portainer.ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: []portainer.UserResourceAccess{},
TeamAccesses: []portainer.TeamResourceAccess{},
AdministratorsOnly: true,
Public: false,
System: false,
}
}
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl {

View File

@@ -40,11 +40,6 @@ 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.
// 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) {

View File

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

View File

@@ -190,26 +190,24 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
SecuritySettings EndpointSecuritySettings
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -273,18 +271,6 @@ type (
// Deprecated
EndpointSyncJob struct{}
// EndpointSecuritySettings represents settings for an endpoint
EndpointSecuritySettings struct {
AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers"`
EnableHostManagementFeatures bool `json:"enableHostManagementFeatures"`
}
// EndpointType represents the type of an endpoint
EndpointType int
@@ -529,31 +515,29 @@ type (
// Settings represents the application settings
Settings struct {
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
// Deprecated fields v26
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
}
// SnapshotJob represents a scheduled job that can create endpoint snapshots
@@ -570,10 +554,6 @@ type (
Env []Pair `json:"Env"`
ResourceControl *ResourceControl `json:"ResourceControl"`
Status StackStatus `json:"Status"`
CreationDate int64
CreatedBy string
UpdateDate int64
UpdatedBy string
ProjectPath string
}
@@ -794,7 +774,6 @@ type (
// ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
}
@@ -1140,11 +1119,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.1.0"
APIVersion = "2.0.1"
// DBVersion is the version number of the Portainer database
DBVersion = 26
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
DBVersion = 25
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved

View File

@@ -1,3 +1,3 @@
<button type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
<button ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
<i style="margin: 0;" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
</button>

View File

@@ -625,6 +625,57 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
margin-left: 21px;
}
/* switch box */
:root {
--switch-size: 24px;
}
.switch input {
display: none;
}
.switch i,
.bootbox-form .checkbox i {
display: inline-block;
vertical-align: middle;
cursor: pointer;
padding-right: var(--switch-size);
transition: all ease 0.2s;
-webkit-transition: all ease 0.2s;
-moz-transition: all ease 0.2s;
-o-transition: all ease 0.2s;
border-radius: var(--switch-size);
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch i:before,
.bootbox-form .checkbox i:before {
display: block;
content: '';
width: var(--switch-size);
height: var(--switch-size);
border-radius: var(--switch-size);
background: white;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch :checked + i,
.bootbox-form .checkbox :checked ~ i {
padding-right: 0;
padding-left: var(--switch-size);
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
}
/* !switch box */
/* small switch box */
.switch.small {
--switch-size: 12px;
}
/* !small switch box */
.boxselector_wrapper {
display: flex;
flex-flow: row wrap;
@@ -876,27 +927,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
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*/
.modal-open {
padding-right: 0 !important;

View File

@@ -581,16 +581,6 @@ angular.module('portainer.docker', ['portainer.app']).config([
},
};
const dockerFeaturesConfiguration = {
name: 'docker.featuresConfiguration',
url: '/feat-config',
views: {
'content@': {
component: 'dockerFeaturesConfigurationView',
},
},
};
$stateRegistryProvider.register(configs);
$stateRegistryProvider.register(config);
$stateRegistryProvider.register(configCreation);
@@ -640,6 +630,5 @@ angular.module('portainer.docker', ['portainer.app']).config([
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeBrowse);
$stateRegistryProvider.register(volumeCreation);
$stateRegistryProvider.register(dockerFeaturesConfiguration);
},
]);

View File

@@ -4,8 +4,100 @@
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div class="settings">
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
<span
class="setting"
ng-class="{ 'setting-active': $ctrl.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 uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>

View File

@@ -36,6 +36,9 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
};
this.columnVisibility = {
state: {
open: false,
},
columns: {
state: {
label: 'State',
@@ -72,11 +75,9 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
},
};
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
function onColumnVisibilityChange(columns) {
this.columnVisibility.columns = columns;
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
}
this.onColumnVisibilityChange = function (columnVisibility) {
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
};
this.onSelectionChanged = function () {
this.updateSelectionState();
@@ -198,6 +199,7 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
if (storedColumnVisibility !== null) {
this.columnVisibility = storedColumnVisibility;
this.columnVisibility.state.open = false;
}
};
},

View File

@@ -66,13 +66,6 @@ angular.module('portainer.docker').controller('ServicesDatatableController', [
}
};
this.onDataRefresh = function () {
var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
if (storedExpandedItems !== null) {
this.expandItems(storedExpandedItems);
}
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();

View File

@@ -37,15 +37,7 @@
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
<a ui-sref="docker.swarm({endpointId: $ctrl.endpointId})" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.swarm'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement">
<a ui-sref="docker.host({endpointId: $ctrl.endpointId})" ui-sref-active="active">Host <span class="menu-icon fa fa-th fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="$ctrl.adminAccess && ['docker.featuresConfiguration', 'docker.host'].includes($ctrl.currentRouteName)">
<a ui-sref="docker.featuresConfiguration({endpointId: $ctrl.endpointId})" ui-sref-active="active">Setup</a>
</div>
</li>

View File

@@ -36,13 +36,10 @@
<!-- don't use registry -->
<div ng-if="!$ctrl.model.UseRegistry">
<div class="form-group">
<span class="small">
<p class="text-muted" style="margin-left: 15px;">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
When using advanced mode, image and repository <b>must be</b> publicly available.
</p>
</span>
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
>Image
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
</label>
<div ng-class="$ctrl.inputClass">
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
</div>

View File

@@ -4,6 +4,7 @@ export function ImageViewModel(data) {
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
if (!this.RepoTags && data.RepoDigests) {
this.RepoTags = [];
@@ -20,7 +21,6 @@ export function ImageViewModel(data) {
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
this.Labels = data.Labels;
}
export function ImageBuildModel(data) {

View File

@@ -16,5 +16,4 @@ export function ImageDetailsViewModel(data) {
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
this.Labels = data.ContainerConfig.Labels;
}

View File

@@ -27,9 +27,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'ModalService',
'RegistryService',
'SystemService',
'SettingsService',
'PluginService',
'HttpRequestHelper',
'endpoint',
function (
$q,
$scope,
@@ -53,9 +53,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
ModalService,
RegistryService,
SystemService,
SettingsService,
PluginService,
HttpRequestHelper,
endpoint
HttpRequestHelper
) {
$scope.create = create;
@@ -709,8 +709,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Notifications.error('Failure', err, 'Unable to retrieve engine details');
});
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
SettingsService.publicSettings()
.then(function success(data) {
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
});
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
$scope.availableLoggingDrivers = loggingDrivers;
@@ -927,11 +933,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
async function shouldShowDevices() {
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
}
async function checkIfContainerCapabilitiesEnabled() {
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
}
initView();

View File

@@ -22,7 +22,6 @@ angular.module('portainer.docker').controller('ContainerController', [
'HttpRequestHelper',
'Authentication',
'StateManager',
'endpoint',
function (
$q,
$scope,
@@ -42,8 +41,7 @@ angular.module('portainer.docker').controller('ContainerController', [
ImageService,
HttpRequestHelper,
Authentication,
StateManager,
endpoint
StateManager
) {
$scope.activityTime = 0;
$scope.portBindings = [];
@@ -99,13 +97,14 @@ angular.module('portainer.docker').controller('ContainerController', [
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
const autoRemove = $scope.container.HostConfig.AutoRemove;
const admin = Authentication.isAdmin();
const appState = StateManager.getState();
const {
allowContainerCapabilitiesForRegularUsers,
allowHostNamespaceForRegularUsers,
allowDeviceMappingForRegularUsers,
allowBindMountsForRegularUsers,
allowPrivilegedModeForRegularUsers,
} = endpoint.SecuritySettings;
} = appState.application;
const settingRestrictsRegularUsers =
!allowContainerCapabilitiesForRegularUsers ||

View File

@@ -17,7 +17,6 @@ angular.module('portainer.docker').controller('DashboardController', [
'EndpointProvider',
'StateManager',
'TagService',
'endpoint',
function (
$scope,
$q,
@@ -33,8 +32,7 @@ angular.module('portainer.docker').controller('DashboardController', [
Notifications,
EndpointProvider,
StateManager,
TagService,
endpoint
TagService
) {
$scope.dismissInformationPanel = function (id) {
StateManager.dismissInformationPanel(id);
@@ -91,8 +89,9 @@ angular.module('portainer.docker').controller('DashboardController', [
async function shouldShowStacks() {
const isAdmin = Authentication.isAdmin();
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers;
return isAdmin || allowStackManagementForRegularUsers;
}
initView();

View File

@@ -1,94 +0,0 @@
export default class DockerFeaturesConfigurationController {
/* @ngInject */
constructor($async, EndpointService, Notifications, StateManager) {
this.$async = $async;
this.EndpointService = EndpointService;
this.Notifications = Notifications;
this.StateManager = StateManager;
this.formValues = {
enableHostManagementFeatures: false,
allowVolumeBrowserForRegularUsers: false,
disableBindMountsForRegularUsers: false,
disablePrivilegedModeForRegularUsers: false,
disableHostNamespaceForRegularUsers: false,
disableStackManagementForRegularUsers: false,
disableDeviceMappingForRegularUsers: false,
disableContainerCapabilitiesForRegularUsers: false,
};
this.isAgent = false;
this.state = {
actionInProgress: false,
};
this.save = this.save.bind(this);
}
isContainerEditDisabled() {
const {
disableBindMountsForRegularUsers,
disableHostNamespaceForRegularUsers,
disablePrivilegedModeForRegularUsers,
disableDeviceMappingForRegularUsers,
disableContainerCapabilitiesForRegularUsers,
} = this.formValues;
return (
disableBindMountsForRegularUsers ||
disableHostNamespaceForRegularUsers ||
disablePrivilegedModeForRegularUsers ||
disableDeviceMappingForRegularUsers ||
disableContainerCapabilitiesForRegularUsers
);
}
async save() {
return this.$async(async () => {
try {
this.state.actionInProgress = true;
const securitySettings = {
enableHostManagementFeatures: this.formValues.enableHostManagementFeatures,
allowBindMountsForRegularUsers: !this.formValues.disableBindMountsForRegularUsers,
allowPrivilegedModeForRegularUsers: !this.formValues.disablePrivilegedModeForRegularUsers,
allowVolumeBrowserForRegularUsers: this.formValues.allowVolumeBrowserForRegularUsers,
allowHostNamespaceForRegularUsers: !this.formValues.disableHostNamespaceForRegularUsers,
allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers,
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
};
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
this.endpoint.SecuritySettings = securitySettings;
this.Notifications.success('Saved settings successfully');
} catch (e) {
this.Notifications.error('Failure', e, 'Failed saving settings');
}
this.state.actionInProgress = false;
});
}
checkAgent() {
const applicationState = this.StateManager.getState();
return applicationState.endpoint.mode.agentProxy;
}
$onInit() {
const securitySettings = this.endpoint.SecuritySettings;
const isAgent = this.checkAgent();
this.isAgent = isAgent;
this.formValues = {
enableHostManagementFeatures: isAgent && securitySettings.enableHostManagementFeatures,
allowVolumeBrowserForRegularUsers: isAgent && securitySettings.allowVolumeBrowserForRegularUsers,
disableBindMountsForRegularUsers: !securitySettings.allowBindMountsForRegularUsers,
disablePrivilegedModeForRegularUsers: !securitySettings.allowPrivilegedModeForRegularUsers,
disableHostNamespaceForRegularUsers: !securitySettings.allowHostNamespaceForRegularUsers,
disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers,
disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers,
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
};
}
}

View File

@@ -1,137 +0,0 @@
<rd-header>
<rd-header-title title-text="Docker features configuration"></rd-header-title>
<rd-header-content>Docker configuration</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="$ctrl.form">
<div class="col-sm-12 form-section-title">
Host and Filesystem
</div>
<div ng-if="!$ctrl.isAgent" class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
These features are only available for an Agent enabled endpoints.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.enableHostManagementFeatures"
name="enableHostManagementFeatures"
label="Enable host management features"
tooltip="Enable host management features: host system browsing and advanced host details."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.allowVolumeBrowserForRegularUsers"
name="allowVolumeBrowserForRegularUsers"
label="Enable volume management for non-administrators"
tooltip="When enabled, regular users will be able to use Portainer volume management features."
disabled="!$ctrl.isAgent"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<!-- security -->
<div class="col-sm-12 form-section-title">
Docker Security Settings
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableBindMountsForRegularUsers"
name="disableBindMountsForRegularUsers"
label="Disable bind mounts for non-administrators"
tooltip="When enabled, regular users will not be able to use bind mounts when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disablePrivilegedModeForRegularUsers"
name="disablePrivilegedModeForRegularUsers"
label="Disable privileged mode for non-administrators"
tooltip="When enabled, regular users will not be able to use privileged mode when creating containers."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableHostNamespaceForRegularUsers"
name="disableHostNamespaceForRegularUsers"
label="Disable the use of host PID 1 for non-administrators"
tooltip="Prevent users from accessing the host filesystem through the host PID namespace."
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableStackManagementForRegularUsers"
name="disableStackManagementForRegularUsers"
label="Disable the use of Stacks for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableDeviceMappingForRegularUsers"
name="disableDeviceMappingForRegularUsers"
label="Disable device mappings for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableContainerCapabilitiesForRegularUsers"
name="disableContainerCapabilitiesForRegularUsers"
label="Disable container capabilities for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
</span>
</div>
<!-- !security -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.save()" ng-disabled="$ctrl.state.actionInProgress" button-spinner="$ctrl.state.actionInProgress">
<span ng-hide="$ctrl.state.actionInProgress">Save configuration</span>
<span ng-show="$ctrl.state.actionInProgress">Saving...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@@ -1,11 +0,0 @@
import angular from 'angular';
import controller from './docker-features-configuration.controller';
angular.module('portainer.docker').component('dockerFeaturesConfigurationView', {
templateUrl: './docker-features-configuration.html',
controller,
bindings: {
endpoint: '<',
},
});

View File

@@ -29,7 +29,7 @@ angular.module('portainer.docker').controller('HostViewController', [
ctrl.state.isAdmin = Authentication.isAdmin();
var agentApiVersion = applicationState.endpoint.agentApiVersion;
ctrl.state.agentApiVersion = agentApiVersion;
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
$q.all({
version: SystemService.version(),

View File

@@ -1,7 +1,4 @@
angular.module('portainer.docker').component('hostView', {
templateUrl: './host-view.html',
controller: 'HostViewController',
bindings: {
endpoint: '<',
},
});

View File

@@ -128,17 +128,6 @@
<td>Build</td>
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
</tr>
<tr ng-if="!(image.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in image.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="image.Author">
<td>Author</td>
<td>{{ image.Author }}</td>

View File

@@ -19,7 +19,7 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ngf-accept="'application/x-tar,application/x-gzip'" ng-model="formValues.UploadFile"
<button class="btn btn-sm btn-primary" ngf-select ngf-min-size="10" ngf-accept="'application/x-tar,application/x-gzip'" ng-model="formValues.UploadFile"
>Select file</button
>
<span style="margin-left: 5px;">

View File

@@ -196,8 +196,7 @@
<div class="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Isolated network
<portainer-tooltip position="bottom" message="An isolated network has no inbound or outbound communications."></portainer-tooltip>
Restrict external access to the network
</label>
<label name="ownership" class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.Internal" />

View File

@@ -20,7 +20,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [
var applicationState = StateManager.getState();
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
ctrl.state.isAdmin = Authentication.isAdmin();
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent;

View File

@@ -1,7 +1,4 @@
angular.module('portainer.docker').component('nodeDetailsView', {
templateUrl: './node-details-view.html',
controller: 'NodeDetailsViewController',
bindings: {
endpoint: '<',
},
});

View File

@@ -30,9 +30,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'RegistryService',
'HttpRequestHelper',
'NodeService',
'SettingsService',
'WebhookService',
'EndpointProvider',
'endpoint',
function (
$q,
$scope,
@@ -56,9 +56,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
RegistryService,
HttpRequestHelper,
NodeService,
SettingsService,
WebhookService,
EndpointProvider,
endpoint
EndpointProvider
) {
$scope.formValues = {
Name: '',
@@ -593,9 +593,10 @@ angular.module('portainer.docker').controller('CreateServiceController', [
async function checkIfAllowedBindMounts() {
const isAdmin = Authentication.isAdmin();
const { allowBindMountsForRegularUsers } = endpoint.SecuritySettings;
const settings = await SettingsService.publicSettings();
const { AllowBindMountsForRegularUsers } = settings;
return isAdmin || allowBindMountsForRegularUsers;
return isAdmin || AllowBindMountsForRegularUsers;
}
},
]);

View File

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

View File

@@ -1,9 +1,6 @@
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12 small text-muted">
By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers (Linux) or
<code>C:\ProgramData\Docker\secrets\$SECRET_NAME</code> (Windows).</div
>
<div class="col-sm-12 small text-muted"> By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers. </div>
</div>
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">

View File

@@ -24,14 +24,7 @@
<tbody>
<tr ng-repeat="mount in service.ServiceMounts">
<td ng-if="isAdmin || allowBindMounts">
<select
name="mountType"
class="form-control"
ng-model="mount.Type"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
>
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating" disable-authorization="DockerServiceUpdate">
<option value="volume">Volume</option>
<option value="bind">Bind</option>
</select>
@@ -42,7 +35,6 @@
ng-model="mount.Source"
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
ng-if="mount.Type === 'volume'"
ng-change="updateMount(service, mount)"
disable-authorization="DockerServiceUpdate"
>
<option selected disabled hidden value="">Select a volume</option>

View File

@@ -34,6 +34,7 @@ angular.module('portainer.docker').controller('ServiceController', [
'SecretService',
'ImageService',
'SecretHelper',
'Service',
'ServiceHelper',
'LabelHelper',
'TaskService',
@@ -44,6 +45,7 @@ angular.module('portainer.docker').controller('ServiceController', [
'ModalService',
'PluginService',
'Authentication',
'SettingsService',
'VolumeService',
'ImageHelper',
'WebhookService',
@@ -51,7 +53,6 @@ angular.module('portainer.docker').controller('ServiceController', [
'clipboard',
'WebhookHelper',
'NetworkService',
'endpoint',
function (
$q,
$scope,
@@ -66,6 +67,7 @@ angular.module('portainer.docker').controller('ServiceController', [
SecretService,
ImageService,
SecretHelper,
Service,
ServiceHelper,
LabelHelper,
TaskService,
@@ -76,14 +78,14 @@ angular.module('portainer.docker').controller('ServiceController', [
ModalService,
PluginService,
Authentication,
SettingsService,
VolumeService,
ImageHelper,
WebhookService,
EndpointProvider,
clipboard,
WebhookHelper,
NetworkService,
endpoint
NetworkService
) {
$scope.state = {
updateInProgress: false,
@@ -664,6 +666,7 @@ angular.module('portainer.docker').controller('ServiceController', [
availableImages: ImageService.images(),
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
settings: SettingsService.publicSettings(),
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
});
})
@@ -674,7 +677,7 @@ angular.module('portainer.docker').controller('ServiceController', [
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
$scope.availableVolumes = data.volumes;
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
$scope.isAdmin = Authentication.isAdmin();
$scope.availableNetworks = data.availableNetworks;
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');

View File

@@ -13,13 +13,12 @@ angular.module('portainer.docker').controller('ServicesController', [
function getServices() {
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
return $q
.all({
services: ServiceService.services(),
tasks: TaskService.tasks(),
containers: agentProxy ? ContainerService.containers(1) : [],
nodes: NodeService.nodes(),
})
$q.all({
services: ServiceService.services(),
tasks: TaskService.tasks(),
containers: agentProxy ? ContainerService.containers(1) : [],
nodes: NodeService.nodes(),
})
.then(function success(data) {
var services = data.services;
var tasks = data.tasks;

View File

@@ -10,8 +10,7 @@ angular.module('portainer.docker').controller('VolumesController', [
'EndpointProvider',
'Authentication',
'ModalService',
'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService, endpoint) {
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService) {
$scope.removeAction = function (selectedItems) {
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
if (confirmed) {
@@ -76,7 +75,8 @@ angular.module('portainer.docker').controller('VolumesController', [
function initView() {
getVolumes();
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers);
$scope.showBrowseAction =
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
}
initView();

View File

@@ -205,7 +205,7 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.model.File">Select file</button>
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.model.File">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.model.File.name }}
<i class="fa fa-times red-icon" ng-if="!$ctrl.model.File" aria-hidden="true"></i>

View File

@@ -124,7 +124,7 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.StackFile">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.StackFile">
Select file
</button>
<span style="margin-left: 5px;">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html
><html lang="en" ng-app="<%= name %>" ng-strict-di>
><html lang="en" ng-app="<%= name %>">
<head>
<meta charset="utf-8" />
<title>Portainer</title>

View File

@@ -78,13 +78,6 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ImagePullPolicy')">
Image Pull Policy
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
@@ -117,7 +110,6 @@
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Image }}</td>
<td>{{ item.ImagePullPolicy }}</td>
<td
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
>

View File

@@ -71,7 +71,7 @@
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th style="width: 55px;">
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
@@ -142,9 +142,8 @@
</td>
</tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td></td>
<td colspan="4">
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })">{{ app.Name }}</a>
<td colspan="5">
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
>external</span
>

View File

@@ -118,7 +118,7 @@
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</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.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td>{{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td ng-if="$ctrl.isAdmin">
<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

View File

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

View File

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

View File

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

View File

@@ -59,9 +59,7 @@ class KubernetesApplicationConverter {
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name;
res.ResourcePool = data.metadata.namespace;
if (containers.length) {
res.Image = containers[0].image;
}
res.Image = containers[0].image;
res.CreationDate = data.metadata.creationTimestamp;
res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import * as JsonPatch from 'fast-json-patch';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
import {

View File

@@ -1,4 +1,3 @@
import _ from 'lodash-es';
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
@@ -21,8 +20,7 @@ class KubernetesNamespaceConverter {
res.metadata.name = namespace.Name;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
if (namespace.ResourcePoolOwner) {
const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner;
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner;
}
return res;
}

View File

@@ -1,30 +1,16 @@
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
import YAML from 'yaml';
import _ from 'lodash-es';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
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;
}
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
res.stringData = secret.Data;
return res;
}
@@ -33,19 +19,7 @@ class KubernetesSecretConverter {
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.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;
}
res.stringData = secret.Data;
return res;
}
@@ -57,21 +31,7 @@ class KubernetesSecretConverter {
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.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;
});
res.Data = payload.data;
return res;
}
@@ -80,7 +40,18 @@ class KubernetesSecretConverter {
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
res.Data = formValues.Data;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
return res;
}
}

View File

@@ -69,7 +69,7 @@ class KubernetesServiceConverter {
payload.metadata.namespace = service.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.Application;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
payload.spec.ports = service.Ports;
payload.spec.selector.app = service.ApplicationName;
if (service.Headless) {

View File

@@ -175,10 +175,7 @@ class KubernetesApplicationHelper {
item.OverridenKeys = _.map(keys, (k) => {
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
fvKey.Key = k.Key;
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) {
if (index < k.EnvCount) {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
} else {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;

View File

@@ -10,11 +10,7 @@ class KubernetesCommonHelper {
}
static ownerToLabel(owner) {
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;
return _.replace(owner, /[^-A-Za-z0-9_.]/g, '.');
}
}
export default KubernetesCommonHelper;

View File

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

View File

@@ -35,7 +35,6 @@ export class KubernetesApplicationFormValues {
}
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
NONE: 0,
ENVIRONMENT: 1,
FILESYSTEM: 2,
});

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More