Compare commits

..

2 Commits

Author SHA1 Message Date
xAt0mZ
70ba10d992 fix(app): EndpointProvider fallback on URL EndpointID when no endpoint is selected 2021-03-02 19:51:15 +01:00
Dmitry Salakhov
f443dd113d fix windows build 2021-02-25 11:53:04 +13:00
198 changed files with 2237 additions and 3467 deletions

View File

@@ -5,8 +5,7 @@
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"corejs": "2"
"useBuiltIns": "entry"
}
]
]

View File

@@ -1,32 +0,0 @@
# Open Source License Attribution
This application uses Open Source components. You can find the source
code of their open source projects along with license information below.
We acknowledge and are grateful to these developers for their contributions
to open source.
### [angular-json-tree](https://github.com/awendland/angular-json-tree)
by [Alex Wendland](https://github.com/awendland) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [caniuse-db](https://github.com/Fyrd/caniuse)
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [caniuse-lite](https://github.com/ben-eb/caniuse-lite)
by [caniuse.com](caniuse.com) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
### [spdx-exceptions](https://github.com/jslicense/spdx-exceptions.json)
by Kyle Mitchell using [SPDX](https://spdx.dev/) from Linux Foundation licensed under [CC BY 3.0 License](https://creativecommons.org/licenses/by/3.0/)
### [fontawesome-free](https://github.com/FortAwesome/Font-Awesome) Icons
by [Fort Awesome](https://fortawesome.com/) is licensed under [CC BY 4.0 License](https://creativecommons.org/licenses/by/4.0/)
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@@ -127,9 +127,3 @@ When adding a new route to an existing handler use the following as a template (
```
explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation]
## Licensing
See the [LICENSE](https://github.com/portainer/portainer/blob/develop/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.

View File

@@ -65,4 +65,8 @@ Portainer supports "Current - 2 docker versions only. Prior versions may operate
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains code from open source projects. See [ATTRIBUTIONS.md](./ATTRIBUTIONS.md) for a list.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@@ -1,40 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/internal/stackutils"
)
func (m *Migrator) updateStackResourceControlToDB27() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resource := range resourceControls {
if resource.Type != portainer.StackResourceControl {
continue
}
stackName := resource.ResourceID
stack, err := m.stackService.StackByName(stackName)
if err != nil {
if err == errors.ErrObjectNotFound {
continue
}
return err
}
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
if err != nil {
return err
}
}
return nil
}

View File

@@ -2,7 +2,7 @@ package migrator
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
@@ -350,13 +350,5 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
err := m.updateStackResourceControlToDB27()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -1,9 +1,7 @@
package team
import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
@@ -60,7 +58,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
return err
}
if strings.EqualFold(t.Name, name) {
if t.Name == name {
team = &t
break
}

View File

@@ -1,11 +1,10 @@
package user
import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"strings"
"github.com/boltdb/bolt"
)
@@ -62,7 +61,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
return err
}
if strings.EqualFold(u.Username, username) {
if strings.ToLower(u.Username) == username {
user = &u
break
}

View File

@@ -33,12 +33,12 @@ func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatalf("failed parsing flags: %v", err)
log.Fatal(err)
}
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatalf("failed validating flags:%v", err)
log.Fatal(err)
}
return flags
}
@@ -46,7 +46,7 @@ func initCLI() *portainer.CLIFlags {
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatalf("failed creating file service: %v", err)
log.Fatal(err)
}
return fileService
}
@@ -54,33 +54,33 @@ func initFileService(dataStorePath string) portainer.FileService {
func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatalf("failed creating data store: %v", err)
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatalf("failed opening store: %v", err)
log.Fatal(err)
}
err = store.Init()
if err != nil {
log.Fatalf("failed initializing data store: %v", err)
log.Fatal(err)
}
err = store.MigrateData()
if err != nil {
log.Fatalf("failed migration: %v", err)
log.Fatal(err)
}
return store
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, proxyManager)
if err != nil {
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
if composeWrapper != nil {
return composeWrapper
}
return composeWrapper
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
@@ -211,7 +211,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
log.Fatalf("failed checking for existing key pair: %v", err)
log.Fatal(err)
}
if existingKeyPair {
@@ -359,7 +359,7 @@ func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatalf("failed getting admin user: %v", err)
log.Fatal(err)
}
if len(users) == 0 {
@@ -382,7 +382,7 @@ func main() {
jwtService, err := initJWTService(dataStore)
if err != nil {
log.Fatalf("failed initializing JWT service: %v", err)
log.Fatal(err)
}
ldapService := initLDAPService()
@@ -397,14 +397,14 @@ func main() {
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatalf("failed initializing key pai: %v", err)
log.Fatal(err)
}
reverseTunnelService := chisel.NewService(dataStore)
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
log.Fatalf("failed getting instance id: %v", err)
log.Fatal(err)
}
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
@@ -412,13 +412,13 @@ func main() {
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
if err != nil {
log.Fatalf("failed initializing snapshot service: %v", err)
log.Fatal(err)
}
snapshotService.Start()
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err)
log.Fatal(err)
}
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
@@ -430,31 +430,31 @@ func main() {
if dataStore.IsNew() {
err = updateSettingsFromFlags(dataStore, flags)
if err != nil {
log.Fatalf("failed updating settings from flags: %v", err)
log.Fatal(err)
}
}
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService)
if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err)
log.Fatal(err)
}
applicationStatus := initStatus(flags)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
log.Fatalf("failed initializing endpoint: %v", err)
log.Fatal(err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
if err != nil {
log.Fatalf("failed getting admin password file: %v", err)
log.Fatal(err)
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
log.Fatalf("failed hashing admin password: %v", err)
log.Fatal(err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -463,7 +463,7 @@ func main() {
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatalf("failed getting admin user: %v", err)
log.Fatal(err)
}
if len(users) == 0 {
@@ -475,7 +475,7 @@ func main() {
}
err := dataStore.User().CreateUser(user)
if err != nil {
log.Fatalf("failed creating admin user: %v", err)
log.Fatal(err)
}
} else {
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
@@ -486,7 +486,7 @@ func main() {
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatalf("failed starting tunnel server: %v", err)
log.Fatal(err)
}
var server portainer.Server = &http.Server{
@@ -518,6 +518,6 @@ func main() {
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
err = server.Start()
if err != nil {
log.Fatalf("failed starting server: %v", err)
log.Fatal(err)
}
}

View File

@@ -118,7 +118,6 @@ func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
snapshot.NodeCount = len(nodes)
return nil
}

View File

@@ -1,118 +0,0 @@
package exec
import (
"fmt"
"os"
"path"
"strings"
wrapper "github.com/portainer/docker-compose-wrapper"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
)
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
wrapper *wrapper.ComposeWrapper
proxyManager *proxy.Manager
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
wrap, err := wrapper.NewComposeWrapper(binaryPath)
if err != nil {
return nil, err
}
return &ComposeStackManager{
wrapper: wrap,
proxyManager: proxyManager,
}, nil
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (w *ComposeStackManager) NormalizeStackName(name string) string {
return name
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
func (w *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return portainer.ComposeSyntaxMaxVersion
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (w *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return err
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Up(filePath, url, stack.Name, envFilePath)
return err
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
func (w *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := w.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
filePath := stackFilePath(stack)
_, err = w.wrapper.Down(filePath, url, stack.Name)
return err
}
func stackFilePath(stack *portainer.Stack) string {
return path.Join(stack.ProjectPath, stack.EntryPoint)
}
func (w *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return "", nil, nil
}
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {
if stack.Env == nil || len(stack.Env) == 0 {
return "", 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 "", err
}
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return envFilePath, nil
}

View File

@@ -1,112 +0,0 @@
package exec
import (
"io/ioutil"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_stackFilePath(t *testing.T) {
tests := []struct {
name string
stack *portainer.Stack
expected string
}{
// {
// name: "should return empty result if stack is missing",
// stack: nil,
// expected: "",
// },
// {
// name: "should return empty result if stack don't have entrypoint",
// stack: &portainer.Stack{},
// expected: "",
// },
{
name: "should allow file name and dir",
stack: &portainer.Stack{
ProjectPath: "dir",
EntryPoint: "file",
},
expected: path.Join("dir", "file"),
},
{
name: "should allow file name only",
stack: &portainer.Stack{
EntryPoint: "file",
},
expected: "file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stackFilePath(tt.stack)
assert.Equal(t, tt.expected, result)
})
}
}
func Test_createEnvFile(t *testing.T) {
dir := t.TempDir()
tests := []struct {
name string
stack *portainer.Stack
expected string
expectedFile bool
}{
// {
// name: "should not add env file option if stack is missing",
// stack: nil,
// expected: "",
// },
{
name: "should not add env file option if stack doesn't have env variables",
stack: &portainer.Stack{
ProjectPath: dir,
},
expected: "",
},
{
name: "should not add env file option if stack's env variables are empty",
stack: &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{},
},
expected: "",
},
{
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: "var1=value1\nvar2=value2\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, _ := createEnvFile(tt.stack)
if tt.expected != "" {
assert.Equal(t, path.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
assert.Equal(t, tt.expected, string(content))
} else {
assert.Equal(t, "", result)
}
})
}
}

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

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

View File

@@ -1,4 +1,5 @@
// +build integration
package exec
import (
@@ -32,9 +33,7 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
Name: "project-name",
}
endpoint := &portainer.Endpoint{
URL: "unix://",
}
endpoint := &portainer.Endpoint{}
return stack, endpoint
}
@@ -43,17 +42,14 @@ func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w, err := NewComposeStackManager("", nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
w := NewComposeWrapper("", nil)
err = w.Up(stack, endpoint)
err := w.Up(stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
if !containerExists(composedContainerName) {
if containerExists(composedContainerName) == false {
t.Fatal("container should exist")
}
@@ -67,13 +63,13 @@ func Test_UpAndDown(t *testing.T) {
}
}
func containerExists(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
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), containerName)
return strings.Contains(string(out), contaierName)
}

View File

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

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

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

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

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

View File

@@ -25,11 +25,10 @@ require (
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/portainer/docker-compose-wrapper v0.0.0-20210222063404-03b2b09b0a19
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.7.0
github.com/stretchr/testify v1.6.1
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

@@ -226,10 +226,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210214130111-fb78615516aa h1:68p+i58sm7uvX5MG7PEDvyxQuUc4W/+tuqTktz/e23s=
github.com/portainer/docker-compose-wrapper v0.0.0-20210214130111-fb78615516aa/go.mod h1:No8p8iZt9N2HOtDS9aWkh1ILxmQVoOTZZjHGlOij/ec=
github.com/portainer/docker-compose-wrapper v0.0.0-20210222063404-03b2b09b0a19 h1:w+r2ay77hw8q/nsXR59QCZ2V4oN7et2fTJNz1SORw5c=
github.com/portainer/docker-compose-wrapper v0.0.0-20210222063404-03b2b09b0a19/go.mod h1:No8p8iZt9N2HOtDS9aWkh1ILxmQVoOTZZjHGlOij/ec=
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
@@ -271,8 +267,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=

View File

@@ -3,12 +3,11 @@ package endpointproxy
import (
"errors"
"fmt"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"net/http"
@@ -67,15 +66,9 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID)
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
if isKubernetesRequest(strings.TrimPrefix(r.URL.String(), requestPrefix)) {
requestPrefix = fmt.Sprintf("/%d", endpointID)
}
requestPrefix = fmt.Sprintf("/%d", endpointID)
}
http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r)
return nil
}
func isKubernetesRequest(requestURL string) bool {
return strings.HasPrefix(requestURL, "/api")
}

View File

@@ -1,140 +0,0 @@
package endpoints
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/endpointutils"
)
type dockerhubStatusResponse struct {
Remaining int `json:"remaining"`
Limit int `json:"limit"`
}
// GET request on /api/endpoints/{id}/dockerhub/status
func (handler *Handler) endpointDockerhubStatus(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}
}
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}
}
if !endpointutils.IsLocalEndpoint(endpoint) {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment type", errors.New("Invalid environment type")}
}
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
httpClient := client.NewHTTPClient()
token, err := getDockerHubToken(httpClient, dockerhub)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub token from DockerHub", err}
}
resp, err := getDockerHubLimits(httpClient, token)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub rate limits from DockerHub", err}
}
return response.JSON(w, resp)
}
func getDockerHubToken(httpClient *client.HTTPClient, dockerhub *portainer.DockerHub) (string, error) {
type dockerhubTokenResponse struct {
Token string `json:"token"`
}
requestURL := "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull"
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return "", err
}
if dockerhub.Authentication {
req.SetBasicAuth(dockerhub.Username, dockerhub.Password)
}
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New("failed fetching dockerhub token")
}
var data dockerhubTokenResponse
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return "", err
}
return data.Token, nil
}
func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) {
requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed fetching dockerhub limits")
}
rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit")
rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining")
return &dockerhubStatusResponse{
Limit: rateLimit,
Remaining: rateLimitRemaining,
}, nil
}
func parseRateLimitHeader(headers http.Header, headerKey string) (int, error) {
headerValue := headers.Get(headerKey)
if headerValue == "" {
return 0, fmt.Errorf("Missing %s header", headerKey)
}
matches := strings.Split(headerValue, ";")
value, err := strconv.Atoi(matches[0])
if err != nil {
return 0, err
}
return value, nil
}

View File

@@ -66,11 +66,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from 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}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
@@ -113,9 +108,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()
if paginatedEndpoints[idx].EdgeCheckinInterval == 0 {
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
}
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))

View File

@@ -25,8 +25,6 @@ type endpointSettingsUpdatePayload struct {
AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers" example:"true"`
// Whether non-administrator should be able to use container capabilities
AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"`
// Whether non-administrator should be able to use sysctl settings
AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"`
// Whether host management features are enabled
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"`
}
@@ -99,10 +97,6 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
}
if payload.AllowSysctlSettingForRegularUsers != nil {
securitySettings.AllowSysctlSettingForRegularUsers = *payload.AllowSysctlSettingForRegularUsers
}
if payload.EnableHostManagementFeatures != nil {
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"net/http"
"strconv"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -101,13 +100,11 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
} else if agentPlatform == portainer.AgentPlatformKubernetes {
endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment
}
}
endpoint.LastCheckInDate = time.Now().Unix()
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
}
}
settings, err := handler.DataStore.Settings().Settings()

View File

@@ -51,8 +51,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/dockerhub",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}/extensions",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/extensions/{extensionType}",

View File

@@ -26,8 +26,6 @@ type registryCreatePayload struct {
Password string `example:"registry_password"`
// Gitlab specific details, required when type = 4
Gitlab portainer.GitlabRegistryData
// Quay specific details, required when type = 1
Quay portainer.QuayRegistryData
}
func (payload *registryCreatePayload) Validate(r *http.Request) error {
@@ -76,7 +74,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Gitlab: payload.Gitlab,
Quay: payload.Quay,
}
err = handler.DataStore.Registry().CreateRegistry(registry)

View File

@@ -24,7 +24,6 @@ type registryUpdatePayload struct {
Password *string `example:"registry_password"`
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
Quay *portainer.QuayRegistryData
}
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
@@ -111,10 +110,6 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
registry.TeamAccessPolicies = payload.TeamAccessPolicies
}
if payload.Quay != nil {
registry.Quay = *payload.Quay
}
err = handler.DataStore.Registry().UpdateRegistry(registry.ID, registry)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}

View File

@@ -2,10 +2,11 @@ package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
@@ -16,6 +17,13 @@ import (
"github.com/portainer/portainer/api/http/security"
)
// this is coming from libcompose
// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120
func normalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
type composeStackFromFileContentPayload struct {
// Name of the stack
Name string `example:"myStack" validate:"required"`
@@ -29,7 +37,7 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid stack name")
}
payload.Name = normalizeStackName(payload.Name)
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
}
@@ -40,18 +48,18 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
var payload composeStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -69,7 +77,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Compose file on disk", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
}
stack.ProjectPath = projectPath
@@ -83,14 +91,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
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{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
@@ -122,14 +130,16 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid stack name")
}
payload.Name = normalizeStackName(payload.Name)
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
return nil
}
@@ -137,21 +147,18 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
var payload composeStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
if payload.ComposeFilePathInRepository == "" {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -183,7 +190,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
err = handler.cloneGitRepository(gitCloneParams)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
@@ -193,14 +200,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
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{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
@@ -213,44 +220,44 @@ type composeStackFromFileUploadPayload struct {
Env []portainer.Pair
}
func decodeRequestForm(r *http.Request) (*composeStackFromFileUploadPayload, error) {
payload := &composeStackFromFileUploadPayload{}
func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return nil, errors.New("Invalid stack name")
return errors.New("Invalid stack name")
}
payload.Name = name
payload.Name = normalizeStackName(name)
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return nil, errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.StackFileContent = composeFileContent
var env []portainer.Pair
err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true)
if err != nil {
return nil, errors.New("Invalid Env parameter")
return errors.New("Invalid Env parameter")
}
payload.Env = env
return payload, nil
return nil
}
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload, err := decodeRequestForm(r)
payload := &composeStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -268,7 +275,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Compose file on disk", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
}
stack.ProjectPath = projectPath
@@ -282,14 +289,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
err = handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
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{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
}
doCleanUp = false
@@ -308,23 +315,23 @@ type composeStackDeploymentConfig struct {
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
if err != nil {
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve DockerHub details from the database", Err: err}
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err}
}
registries, err := handler.DataStore.Registry().Registries()
if err != nil {
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve registries from the database", Err: err}
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
config := &composeStackDeploymentConfig{
@@ -356,7 +363,6 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowSysctlSettingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {

View File

@@ -2,10 +2,10 @@ package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
@@ -47,13 +47,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -148,13 +150,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -253,13 +257,15 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, payload.Name) {
return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View File

@@ -1,17 +1,13 @@
package stacks
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -28,7 +24,6 @@ type Handler struct {
requestBouncer *security.RequestBouncer
*mux.Router
DataStore portainer.DataStore
DockerClientFactory *docker.ClientFactory
FileService portainer.FileService
GitService portainer.GitService
SwarmStackManager portainer.SwarmStackManager
@@ -108,50 +103,3 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return false, err
}
for _, stack := range stacks {
if strings.EqualFold(stack.Name, name) && (stackID == 0 || stackID != stack.ID) && stack.EndpointID == endpoint.ID {
return false, nil
}
}
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
if err != nil {
return false, err
}
defer dockerClient.Close()
if swarmMode {
services, err := dockerClient.ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
return false, err
}
for _, service := range services {
serviceNS, ok := service.Spec.Labels["com.docker.stack.namespace"]
if ok && serviceNS == name {
return false, nil
}
}
}
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
return false, err
}
for _, container := range containers {
containerNS, ok := container.Labels["com.docker.compose.project"]
if ok && containerNS == name {
return false, nil
}
}
return true, nil
}

View File

@@ -15,7 +15,6 @@ import (
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/stackutils"
)
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
@@ -192,10 +191,6 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin
return errors.New("device mapping disabled for non administrator users")
}
if !securitySettings.AllowSysctlSettingForRegularUsers && service.Sysctls != nil && len(service.Sysctls) > 0 {
return errors.New("sysctl setting disabled for non administrator users")
}
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users")
}
@@ -213,9 +208,9 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
}
if isAdmin {
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
} else {
resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID)
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
}
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)

View File

@@ -12,7 +12,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackDelete
@@ -83,7 +82,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -11,7 +11,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type stackFileResponse struct {
@@ -58,7 +57,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -10,7 +10,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackInspect
@@ -57,7 +56,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -2,7 +2,6 @@ package stacks
import (
"errors"
"fmt"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -12,7 +11,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type stackMigratePayload struct {
@@ -78,7 +76,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
@@ -124,16 +122,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
stack.Name = payload.Name
}
isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
migrationError := handler.migrateStack(r, stack, targetEndpoint)
if migrationError != nil {
return migrationError

View File

@@ -2,13 +2,11 @@ package stacks
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -59,16 +57,7 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -11,7 +11,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackStop
@@ -57,7 +56,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -14,7 +14,6 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type updateComposeStackPayload struct {
@@ -100,7 +99,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View File

@@ -5,12 +5,10 @@ import (
"net/http"
"strings"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
)
const (
@@ -31,23 +29,6 @@ type (
}
)
func getUniqueElements(items string) []string {
result := []string{}
seen := make(map[string]struct{})
for _, item := range strings.Split(items, ",") {
v := strings.TrimSpace(item)
if v == "" {
continue
}
if _, ok := seen[v]; !ok {
result = append(result, v)
seen[v] = struct{}{}
}
}
return result
}
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType)
@@ -64,12 +45,12 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
userNames := make([]string, 0)
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
teamNames = getUniqueElements(concatenatedTeamNames)
teamNames = strings.Split(concatenatedTeamNames, ",")
}
if labelsObject[resourceLabelForPortainerUserResourceControl] != nil {
concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string)
userNames = getUniqueElements(concatenatedUserNames)
userNames = strings.Split(concatenatedUserNames, ",")
}
if len(teamNames) > 0 || len(userNames) > 0 {
@@ -136,17 +117,17 @@ func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resour
switch resourceType {
case portainer.ContainerResourceControl:
return getInheritedResourceControlFromContainerLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls)
case portainer.NetworkResourceControl:
return getInheritedResourceControlFromNetworkLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls)
case portainer.VolumeResourceControl:
return getInheritedResourceControlFromVolumeLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls)
case portainer.ServiceResourceControl:
return getInheritedResourceControlFromServiceLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls)
case portainer.ConfigResourceControl:
return getInheritedResourceControlFromConfigLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls)
case portainer.SecretResourceControl:
return getInheritedResourceControlFromSecretLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls)
}
return nil, nil
@@ -292,9 +273,8 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
@@ -302,9 +282,8 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
@@ -317,20 +296,6 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
return nil, nil
}
func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string {
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != "" {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName]
return stackutils.ResourceControlID(endpointID, stackName)
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != "" {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName]
return stackutils.ResourceControlID(endpointID, stackName)
}
return ""
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
if object["Portainer"] == nil {
object["Portainer"] = make(map[string]interface{})

View File

@@ -1,63 +0,0 @@
package docker
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getUniqueElements(t *testing.T) {
cases := []struct {
description string
input string
expected []string
}{
{
description: "no items padded",
input: " ",
expected: []string{},
},
{
description: "single item",
input: "a",
expected: []string{"a"},
},
{
description: "single item padded",
input: " a ",
expected: []string{"a"},
},
{
description: "multiple items",
input: "a,b",
expected: []string{"a", "b"},
},
{
description: "multiple items padded",
input: " a , b ",
expected: []string{"a", "b"},
},
{
description: "multiple items with empty values",
input: " a , ,b ",
expected: []string{"a", "b"},
},
{
description: "duplicates",
input: " a , a ",
expected: []string{"a"},
},
{
description: "mix with duplicates",
input: " a ,b, a ",
expected: []string{"a", "b"},
},
}
for _, tt := range cases {
t.Run(tt.description, func(t *testing.T) {
result := getUniqueElements(tt.input)
assert.ElementsMatch(t, result, tt.expected)
})
}
}

View File

@@ -15,15 +15,15 @@ const (
configObjectIdentifier = "ID"
)
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, endpointID portainer.EndpointID, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID)
if err != nil {
return nil, err
}
stackResourceID := getStackResourceIDFromLabels(config.Spec.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -58,7 +58,7 @@ func (transport *Transport) configListOperation(response *http.Response, executo
func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error {
// ConfigInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -19,7 +19,7 @@ const (
containerObjectIdentifier = "Id"
)
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
if err != nil {
return nil, err
@@ -33,9 +33,14 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
}
}
stackResourceID := getStackResourceIDFromLabels(container.Config.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
if composeStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -77,7 +82,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec
func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error {
//ContainerInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}
@@ -152,13 +157,12 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
type PartialContainer struct {
HostConfig struct {
Privileged bool `json:"Privileged"`
PidMode string `json:"PidMode"`
Devices []interface{} `json:"Devices"`
Sysctls map[string]interface{} `json:"Sysctls"`
CapAdd []string `json:"CapAdd"`
CapDrop []string `json:"CapDrop"`
Binds []string `json:"Binds"`
Privileged bool `json:"Privileged"`
PidMode string `json:"PidMode"`
Devices []interface{} `json:"Devices"`
CapAdd []string `json:"CapAdd"`
CapDrop []string `json:"CapDrop"`
Binds []string `json:"Binds"`
} `json:"HostConfig"`
}
@@ -205,10 +209,6 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
return forbiddenResponse, errors.New("forbidden to use device mapping")
}
if !securitySettings.AllowSysctlSettingForRegularUsers && len(partialContainer.HostConfig.Sysctls) > 0 {
return forbiddenResponse, errors.New("forbidden to use sysctl settings")
}
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
return nil, errors.New("forbidden to use container capabilities")
}

View File

@@ -4,12 +4,11 @@ import (
"context"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -19,15 +18,15 @@ const (
networkObjectName = "Name"
)
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, endpointID portainer.EndpointID, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{})
if err != nil {
return nil, err
}
stackResourceID := getStackResourceIDFromLabels(network.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -62,7 +61,7 @@ func (transport *Transport) networkListOperation(response *http.Response, execut
func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error {
// NetworkInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -15,15 +15,15 @@ const (
secretObjectIdentifier = "ID"
)
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, endpointID portainer.EndpointID, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID)
if err != nil {
return nil, err
}
stackResourceID := getStackResourceIDFromLabels(secret.Spec.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -58,7 +58,7 @@ func (transport *Transport) secretListOperation(response *http.Response, executo
func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error {
// SecretInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -20,15 +20,15 @@ const (
serviceObjectIdentifier = "ID"
)
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
if err != nil {
return nil, err
}
stackResourceID := getStackResourceIDFromLabels(service.Spec.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -63,7 +63,7 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
//ServiceInspect response is a JSON object
//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -11,7 +11,7 @@ import (
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
// SwarmInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -1,11 +1,9 @@
package docker
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"path"
@@ -165,21 +163,6 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
// volume browser request
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
case strings.HasPrefix(requestPath, "/dockerhub"):
dockerhub, err := transport.dataStore.DockerHub().DockerHub()
if err != nil {
return nil, err
}
newBody, err := json.Marshal(dockerhub)
if err != nil {
return nil, err
}
r.Method = http.MethodPost
r.Body = ioutil.NopCloser(bytes.NewReader(newBody))
r.ContentLength = int64(len(newBody))
}
return transport.executeDockerRequest(r)
@@ -530,7 +513,7 @@ func (transport *Transport) interceptAndRewriteRequest(request *http.Request, op
// https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate
// https://docs.docker.com/engine/api/v1.37/#operation/ConfigCreate
func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -8,7 +8,7 @@ import (
"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"
@@ -18,15 +18,15 @@ const (
volumeObjectIdentifier = "ID"
)
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, endpointID portainer.EndpointID, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
volume, err := dockerClient.VolumeInspect(context.Background(), volumeID)
if err != nil {
return nil, err
}
stackResourceID := getStackResourceIDFromLabels(volume.Labels, endpointID)
if stackResourceID != "" {
return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName]
if swarmStackName != "" {
return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
@@ -37,7 +37,7 @@ func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, en
func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error {
// VolumeList response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}
@@ -76,7 +76,7 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
// VolumeInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}
@@ -142,7 +142,7 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt
}
func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
responseObject, err := responseutils.GetResponseAsJSONObject(response)
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
if err != nil {
return err
}

View File

@@ -49,18 +49,13 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
proxyServer := &ProxyServer{
server: &http.Server{
&http.Server{
Handler: proxy,
},
Port: 0,
0,
}
err = proxyServer.start()
if err != nil {
return nil, err
}
return proxyServer, err
return proxyServer, proxyServer.start()
}
func (proxy *ProxyServer) start() error {
@@ -77,7 +72,7 @@ func (proxy *ProxyServer) start() error {
err := proxy.server.Serve(listener)
log.Printf("Exiting Proxy server %s\n", proxyHost)
if err != nil && err != http.ErrServerClosed {
if err != http.ErrServerClosed {
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
}
}()

View File

@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
endpointURL.Scheme = "http"
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, tokenManager)
proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint.ID, tokenManager)
return proxy, nil
}
@@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
}
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager)
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager)
return proxy, nil
}

View File

@@ -1,14 +1,10 @@
package kubernetes
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/portainer/portainer/api/http/security"
@@ -24,7 +20,6 @@ type (
}
agentTransport struct {
dataStore portainer.DataStore
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
@@ -32,7 +27,6 @@ type (
}
edgeTransport struct {
dataStore portainer.DataStore
httpTransport *http.Transport
tokenManager *tokenManager
reverseTunnelService portainer.ReverseTunnelService
@@ -70,9 +64,8 @@ func (transport *localTransport) RoundTrip(request *http.Request) (*http.Respons
}
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewAgentTransport(datastore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport {
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport {
transport := &agentTransport{
dataStore: datastore,
httpTransport: &http.Transport{
TLSClientConfig: tlsConfig,
},
@@ -92,10 +85,6 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
decorateAgentRequest(request, transport.dataStore)
}
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
@@ -107,10 +96,9 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons
return transport.httpTransport.RoundTrip(request)
}
// NewEdgeTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
func NewEdgeTransport(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport {
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport {
transport := &edgeTransport{
dataStore: datastore,
httpTransport: &http.Transport{},
tokenManager: tokenManager,
reverseTunnelService: reverseTunnelService,
@@ -129,10 +117,6 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
if strings.HasPrefix(request.URL.Path, "/v2") {
decorateAgentRequest(request, transport.dataStore)
}
response, err := transport.httpTransport.RoundTrip(request)
if err == nil {
@@ -167,33 +151,3 @@ func getRoundTripToken(
return token, nil
}
func decorateAgentRequest(r *http.Request, dataStore portainer.DataStore) error {
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
switch {
case strings.HasPrefix(requestPath, "/dockerhub"):
decorateAgentDockerHubRequest(r, dataStore)
}
return nil
}
func decorateAgentDockerHubRequest(r *http.Request, dataStore portainer.DataStore) error {
dockerhub, err := dataStore.DockerHub().DockerHub()
if err != nil {
return err
}
newBody, err := json.Marshal(dockerhub)
if err != nil {
return err
}
r.Method = http.MethodPost
r.Body = ioutil.NopCloser(bytes.NewReader(newBody))
r.ContentLength = int64(len(newBody))
return nil
}

View File

@@ -10,8 +10,8 @@ import (
"strconv"
)
// GetResponseAsJSONObject returns the response content as a generic JSON object
func GetResponseAsJSONObject(response *http.Response) (map[string]interface{}, error) {
// GetResponseAsJSONOBject returns the response content as a generic JSON object
func GetResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
responseData, err := getResponseBodyAsGenericJSON(response)
if err != nil {
return nil, err

View File

@@ -157,7 +157,6 @@ func (server *Server) Start() error {
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore
stackHandler.DockerClientFactory = server.DockerClientFactory
stackHandler.FileService = server.FileService
stackHandler.SwarmStackManager = server.SwarmStackManager
stackHandler.ComposeStackManager = server.ComposeStackManager

View File

@@ -3,8 +3,7 @@ package authorization
import (
"strconv"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api"
)
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
@@ -111,7 +110,7 @@ func NewRestrictedResourceControl(resourceIdentifier string, resourceType portai
func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack {
for idx, stack := range stacks {
resourceControl := GetResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, resourceControls)
resourceControl := GetResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
stacks[idx].ResourceControl = resourceControl
}

View File

@@ -1,11 +0,0 @@
package endpointutils
import (
"strings"
portainer "github.com/portainer/portainer/api"
)
func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
}

View File

@@ -1,12 +0,0 @@
package stackutils
import (
"fmt"
portainer "github.com/portainer/portainer/api"
)
// ResourceControlID returns the stack resource control id
func ResourceControlID(endpointID portainer.EndpointID, name string) string {
return fmt.Sprintf("%d_%s", endpointID, name)
}

View File

@@ -5,8 +5,6 @@ import (
"fmt"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/portainer/libcompose/config"
"github.com/portainer/libcompose/docker"
@@ -66,14 +64,6 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
return composeSyntaxMaxVersion
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
// this is coming from libcompose
// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
// Up will deploy a compose stack (equivalent of docker-compose up)
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {

View File

@@ -119,7 +119,6 @@ type (
ImageCount int `json:"ImageCount"`
ServiceCount int `json:"ServiceCount"`
StackCount int `json:"StackCount"`
NodeCount int `json:"NodeCount"`
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
}
@@ -250,8 +249,6 @@ type (
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion" example:"3.8"`
// Endpoint specific security settings
SecuritySettings EndpointSecuritySettings
// LastCheckInDate mark last check-in date on checkin
LastCheckInDate int64
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -322,29 +319,27 @@ type (
// EndpointSecuritySettings represents settings for an endpoint
EndpointSecuritySettings struct {
// Whether non-administrator should be able to use bind mounts when creating containers
AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers" example:"false"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers" example:"false"`
// Whether non-administrator should be able to use privileged mode when creating containers
AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers" example:"false"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers" example:"false"`
// Whether non-administrator should be able to browse volumes
AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers" example:"true"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers" example:""`
// Whether non-administrator should be able to use the host pid
AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers" example:"true"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers" example:""`
// Whether non-administrator should be able to use device mapping
AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers" example:"true"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers" example:""`
// Whether non-administrator should be able to manage stacks
AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers" example:"true"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers" example:""`
// Whether non-administrator should be able to use container capabilities
AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"`
// Whether non-administrator should be able to use sysctl settings
AllowSysctlSettingForRegularUsers bool `json:"AllowSysctlSettingForRegularUsers" example:"true"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers" example:""`
// Whether host management features are enabled
EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures" example:""`
}
// EndpointType represents the type of an endpoint
EndpointType int
// EndpointRelation represents a endpoint relation object
// EndpointRelation represnts a endpoint relation object
EndpointRelation struct {
EndpointID EndpointID
EdgeStacks map[EdgeStackID]bool
@@ -381,12 +376,6 @@ type (
ProjectPath string `json:"ProjectPath"`
}
// QuayRegistryData represents data required for Quay registry to work
QuayRegistryData struct {
UseOrganisation bool `json:"UseOrganisation"`
OrganisationName string `json:"OrganisationName"`
}
// JobType represents a job type
JobType int
@@ -516,7 +505,6 @@ type (
Password string `json:"Password,omitempty" example:"registry_password"`
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
Gitlab GitlabRegistryData `json:"Gitlab"`
Quay QuayRegistryData `json:"Quay"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
@@ -975,7 +963,6 @@ type (
// ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
NormalizeStackName(name string) string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
}
@@ -1195,7 +1182,7 @@ type (
DeleteResourceControl(ID ResourceControlID) error
}
// ReverseTunnelService represents a service used to manage reverse tunnel connections.
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
ReverseTunnelService interface {
StartTunnelServer(addr, port string, snapshotService SnapshotService) error
GenerateEdgeKey(url, host string, endpointIdentifier int) string
@@ -1237,7 +1224,7 @@ type (
GetNextIdentifier() int
}
// SnapshotService represents a service for managing endpoint snapshots
// StackService represents a service for managing endpoint snapshots
SnapshotService interface {
Start()
SetSnapshotInterval(snapshotInterval string) error
@@ -1325,7 +1312,7 @@ const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.2.0"
// DBVersion is the version number of the Portainer database
DBVersion = 27
DBVersion = 26
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
@@ -1560,7 +1547,6 @@ const (
EdgeAgentActive string = "ACTIVE"
)
// represents an authorization type
const (
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
OperationDockerContainerList Authorization = "DockerContainerList"

View File

@@ -1,13 +0,0 @@
import angular from 'angular';
angular.module('portainer.agent').factory('AgentDockerhub', AgentDockerhub);
function AgentDockerhub($resource, API_ENDPOINT_ENDPOINTS) {
return $resource(
`${API_ENDPOINT_ENDPOINTS}/:endpointId/:endpointType/v2/dockerhub`,
{},
{
limits: { method: 'GET' },
}
);
}

View File

@@ -3,18 +3,17 @@
Container capabilities
</div>
<div class="form-group">
<div ng-repeat="cap in $ctrl.capabilities" class="col-xs-12 col-sm-6 col-md-4">
<div class="row">
<div class="col-xs-8">
<label for="capability" class="control-label text-left" style="display: flex; padding: 0;">
{{ cap.capability }}
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
</label>
</div>
<div class="col-xs-4">
<label class="switch"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
</div>
<div ng-repeat="cap in $ctrl.capabilities">
<div class="col-xs-8 col-sm-3 col-md-2">
<label for="capability" class="control-label text-left">
{{ cap.capability }}
<portainer-tooltip position="bottom" message="{{ cap.description }}"></portainer-tooltip>
</label>
</div>
<div class="col-xs-4 col-sm-2 col-md-1">
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="capability" ng-model="cap.allowed" /><i></i> </label>
</div>
<div class="col-xs-0 col-sm-1 col-md-1"> </div>
</div>
</div>
</form>

View File

@@ -176,13 +176,6 @@
Created
</a>
</th>
<th ng-show="$ctrl.columnVisibility.columns.ip.display">
<a ng-click="$ctrl.changeOrderBy('IP')">
IP Address
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">
<a ng-click="$ctrl.changeOrderBy('NodeName')">
Host
@@ -257,7 +250,6 @@
<td ng-show="$ctrl.columnVisibility.columns.created.display">
{{ item.Created | getisodatefromtimestamp }}
</td>
<td ng-show="$ctrl.columnVisibility.columns.ip.display">{{ item.IP ? item.IP : '-' }}</td>
<td ng-if="$ctrl.showHostColumn" ng-show="$ctrl.columnVisibility.columns.host.display">{{ item.NodeName ? item.NodeName : '-' }}</td>
<td ng-show="$ctrl.columnVisibility.columns.ports.display">
<a

View File

@@ -57,10 +57,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
label: 'Created',
display: true,
},
ip: {
label: 'IP Address',
display: true,
},
host: {
label: 'Host',
display: true,

View File

@@ -1,31 +0,0 @@
export default class porImageRegistryContainerController {
/* @ngInject */
constructor(EndpointHelper, DockerHubService, Notifications) {
this.EndpointHelper = EndpointHelper;
this.DockerHubService = DockerHubService;
this.Notifications = Notifications;
this.pullRateLimits = null;
}
$onChanges({ isDockerHubRegistry }) {
if (isDockerHubRegistry && isDockerHubRegistry.currentValue) {
this.fetchRateLimits();
}
}
async fetchRateLimits() {
this.pullRateLimits = null;
if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) {
try {
this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint);
this.setValidity(this.pullRateLimits.remaining >= 0);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed loading DockerHub pull rate limits', e);
}
} else {
this.setValidity(true);
}
}
}

View File

@@ -1,34 +0,0 @@
<div class="form-group" ng-if="$ctrl.isDockerHubRegistry && $ctrl.pullRateLimits">
<div class="col-sm-12 small">
<div ng-if="$ctrl.pullRateLimits.remaining > 0" class="text-muted">
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
<span ng-if="$ctrl.isAuthenticated">
You are currently using a free account to pull images from DockerHub and will be limited to 200 pulls every 6 hours. Remaining pulls:
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
<span ng-if="!$ctrl.isAuthenticated">
<span ng-if="$ctrl.isAdmin">
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. You can configure DockerHub authentication in
the
<a ui-sref="portainer.registries">Registries View</a>. Remaining pulls:
<span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
<span ng-if="!$ctrl.isAdmin">
You are currently using an anonymous account to pull images from DockerHub and will be limited to 100 pulls every 6 hours. Contact your administrator to configure
DockerHub authentication. Remaining pulls: <span style="font-weight: bold;">{{ $ctrl.pullRateLimits.remaining }}/{{ $ctrl.pullRateLimits.limit }}</span>
</span>
</span>
</div>
<div ng-if="$ctrl.pullRateLimits.remaining <= 0" class="text-warning">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span ng-if="$ctrl.isAuthenticated">
Your authorized pull count quota as a free user is now exceeded.
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
</span>
<span ng-if="!$ctrl.isAuthenticated">
Your authorized pull count quota as an anonymous user is now exceeded.
<span ng-transclude="rateLimitExceeded">You will not be able to pull any image from the DockerHub registry.</span>
</span>
</div>
</div>
</div>

View File

@@ -1,18 +0,0 @@
import angular from 'angular';
import controller from './por-image-registry-rate-limits.controller';
angular.module('portainer.docker').component('porImageRegistryRateLimits', {
bindings: {
endpoint: '<',
setValidity: '<',
isAdmin: '<',
isDockerHubRegistry: '<',
isAuthenticated: '<',
},
controller,
transclude: {
rateLimitExceeded: '?porImageRegistryRateLimitExceeded',
},
templateUrl: './por-image-registry-rate-limits.html',
});

View File

@@ -1,97 +0,0 @@
<!-- use registry -->
<div ng-if="$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
Registry
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
<div class="input-group">
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
<input
type="text"
class="form-control"
aria-describedby="registry-name"
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.model.Image"
name="image_name"
placeholder="e.g. myImage:myTag"
ng-change="$ctrl.onImageChange()"
required
/>
<span ng-if="$ctrl.isDockerHubRegistry()" class="input-group-btn">
<a
href="https://hub.docker.com/search?type=image&q={{ $ctrl.model.Image | trimshasum | trimversiontag }}"
class="btn btn-default"
title="Search image on Docker Hub"
target="_blank"
>
<i class="fab fa-docker"></i> Search
</a>
</span>
</div>
</div>
</div>
<!-- ! use registry -->
<!-- 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>
<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>
</div>
</div>
<!-- ! don't use registry -->
<!-- info message -->
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="$ctrl.form.image_name.$error">
<p ng-message="required">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span>
</p>
</div>
</div>
</div>
<!-- ! info message -->
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
</a>
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
</a>
</p>
</div>
</div>
<div ng-transclude></div>
<por-image-registry-rate-limits
ng-show="$ctrl.checkRateLimits"
is-docker-hub-registry="$ctrl.isDockerHubRegistry()"
endpoint="$ctrl.endpoint"
set-validity="$ctrl.setValidity"
is-authenticated="$ctrl.model.Registry.Authentication"
is-admin="$ctrl.isAdmin"
>
</por-image-registry-rate-limits>
</div>

View File

@@ -1,5 +1,5 @@
angular.module('portainer.docker').component('porImageRegistry', {
templateUrl: './por-image-registry.html',
templateUrl: './porImageRegistry.html',
controller: 'porImageRegistryController',
bindings: {
model: '=', // must be of type PorImageRegistryModel
@@ -7,14 +7,9 @@ angular.module('portainer.docker').component('porImageRegistry', {
autoComplete: '<',
labelClass: '@',
inputClass: '@',
endpoint: '<',
isAdmin: '<',
checkRateLimits: '<',
onImageChange: '&',
setValidity: '<',
},
require: {
form: '^form',
},
transclude: true,
});

View File

@@ -0,0 +1,83 @@
<!-- use registry -->
<div ng-if="$ctrl.model.UseRegistry">
<div class="form-group">
<label for="image_registry" class="control-label text-left" ng-class="$ctrl.labelClass">
Registry
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.availableRegistries track by registry.Name"
ng-model="$ctrl.model.Registry"
id="image_registry"
selected-item-id="ctrl.selectedItemId"
class="form-control"
></select>
</div>
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>
<div ng-class="$ctrl.inputClass" class="margin-sm-top">
<div class="input-group">
<span class="input-group-addon" id="registry-name">{{ $ctrl.displayedRegistryURL() }}</span>
<input
type="text"
class="form-control"
aria-describedby="registry-name"
uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.model.Image"
name="image_name"
placeholder="e.g. myImage:myTag"
ng-change="$ctrl.onImageChange()"
required
/>
<span ng-if="$ctrl.displayedRegistryURL() === 'docker.io'" class="input-group-btn">
<a href="https://hub.docker.com/search?type=image&q={{$ctrl.model.Image | trimshasum | trimversiontag}}"
class="btn btn-default"
title="Search image on Docker Hub"
target="_blank">
<i class="fab fa-docker"></i> Search
</a>
</span>
</div>
</div>
</div>
</div>
<!-- ! use registry -->
<!-- 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>
<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>
</div>
</div>
<!-- ! don't use registry -->
<!-- info message -->
<div class="form-group" ng-show="$ctrl.form.image_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="$ctrl.form.image_name.$error">
<p ng-message="required"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image name is required.
<span ng-if="$ctrl.canPull">Tag must be specified otherwise Portainer will pull all tags associated to the image.</span></p
>
</div>
</div>
</div>
<!-- ! info message -->
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="!$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = true;">
<i class="fa fa-database space-right" aria-hidden="true"></i> Simple mode
</a>
<a class="small interactive" ng-if="$ctrl.model.UseRegistry" ng-click="$ctrl.model.UseRegistry = false;">
<i class="fa fa-globe space-right" aria-hidden="true"></i> Advanced mode
</a>
</p>
</div>
</div>

View File

@@ -48,11 +48,7 @@ class porImageRegistryController {
this.availableImages = images;
}
isDockerHubRegistry() {
return this.model.UseRegistry && this.model.Registry.Name === 'DockerHub';
}
async onRegistryChange() {
onRegistryChange() {
this.prepareAutocomplete();
if (this.model.Registry.Type === RegistryTypes.GITLAB && this.model.Image) {
this.model.Image = _.replace(this.model.Image, this.model.Registry.Gitlab.ProjectPath, '');

View File

@@ -5,7 +5,7 @@ const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655
function parsePort(port) {
if (portPattern.test(port)) {
return parseInt(port, 10);
return parseInt(port);
} else {
return 0;
}
@@ -211,14 +211,14 @@ angular.module('portainer.docker').factory('ContainerHelper', [
_.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => {
// Sort by host port
const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => {
return parseInt(_.split(portKey, '/')[0], 10);
return parseInt(_.split(portKey, '/')[0]);
});
let previousHostPort = -1;
let previousContainerPort = -1;
_.forEach(sortedPortBindingKeys, (portKey) => {
const portKeySplit = _.split(portKey, '/');
const containerPort = parseInt(portKeySplit[0], 10);
const containerPort = parseInt(portKeySplit[0]);
const portBinding = portBindings[portKey][0];
portBindings[portKey].shift();
const hostPort = parsePort(portBinding.HostPort);
@@ -259,10 +259,6 @@ angular.module('portainer.docker').factory('ContainerHelper', [
return bindings;
};
helper.getContainerNames = function (containers) {
return _.map(_.flatten(_.map(containers, 'Names')), (name) => _.trimStart(name, '/'));
};
return helper;
},
]);

View File

@@ -40,10 +40,6 @@ angular.module('portainer.docker').factory('ImageHelper', [
if (registry.Registry.Type === RegistryTypes.GITLAB) {
const slash = _.startsWith(registry.Image, ':') ? '' : '/';
fullImageName = registry.Registry.URL + '/' + registry.Registry.Gitlab.ProjectPath + slash + registry.Image;
} else if (registry.Registry.Type === RegistryTypes.QUAY) {
const name = registry.Registry.Quay.UseOrganisation ? registry.Registry.Quay.OrganisationName : registry.Registry.Username;
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + name + '/' + registry.Image;
} else {
const url = registry.Registry.URL ? registry.Registry.URL + '/' : '';
fullImageName = url + registry.Image;

View File

@@ -5,11 +5,9 @@ import angular from 'angular';
class CreateConfigController {
/* @ngInject */
constructor($async, $state, $transition$, $window, ModalService, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
constructor($async, $state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
this.$state = $state;
this.$transition$ = $transition$;
this.$window = $window;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.ConfigService = ConfigService;
this.Authentication = Authentication;
@@ -26,7 +24,6 @@ class CreateConfigController {
this.state = {
formValidationError: '',
isEditorDirty: false,
};
this.editorUpdate = this.editorUpdate.bind(this);
@@ -34,12 +31,6 @@ class CreateConfigController {
}
async $onInit() {
this.$window.onbeforeunload = () => {
if (this.formValues.displayCodeEditor && this.formValues.ConfigContent && this.state.isEditorDirty) {
return '';
}
};
if (!this.$transition$.params().id) {
this.formValues.displayCodeEditor = true;
return;
@@ -62,12 +53,6 @@ class CreateConfigController {
}
}
async uiCanExit() {
if (this.formValues.displayCodeEditor && this.formValues.ConfigContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
addLabel() {
this.formValues.Labels.push({ name: '', value: '' });
}
@@ -137,7 +122,6 @@ class CreateConfigController {
const userId = userDetails.ID;
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
this.Notifications.success('Config successfully created');
this.state.isEditorDirty = false;
this.$state.go('docker.configs', {}, { reload: true });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create config');
@@ -146,7 +130,6 @@ class CreateConfigController {
editorUpdate(cm) {
this.formValues.ConfigContent = cm.getValue();
this.state.isEditorDirty = true;
}
}

View File

@@ -58,7 +58,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
endpoint
) {
$scope.create = create;
$scope.endpoint = endpoint;
$scope.formValues = {
alwaysPull: true,
@@ -80,7 +79,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
EntrypointMode: 'default',
NodeName: null,
capabilities: [],
Sysctls: [],
LogDriverName: '',
LogDriverOpts: [],
RegistryModel: new PorImageRegistryModel(),
@@ -92,7 +90,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
formValidationError: '',
actionInProgress: false,
mode: '',
pullImageValidity: true,
};
$scope.refreshSlider = function () {
@@ -106,14 +103,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.formValues.EntrypointMode = 'default';
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
if (!validity) {
$scope.formValues.alwaysPull = false;
}
$scope.state.pullImageValidity = validity;
}
$scope.config = {
Image: '',
Env: [],
@@ -137,7 +126,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Devices: [],
CapAdd: [],
CapDrop: [],
Sysctls: {},
},
NetworkingConfig: {
EndpointsConfig: {},
@@ -193,14 +181,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices.splice(index, 1);
};
$scope.addSysctl = function () {
$scope.formValues.Sysctls.push({ name: '', value: '' });
};
$scope.removeSysctl = function (index) {
$scope.formValues.Sysctls.splice(index, 1);
};
$scope.addLogDriverOpt = function () {
$scope.formValues.LogDriverOpts.push({ name: '', value: '' });
};
@@ -354,16 +334,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
config.HostConfig.Devices = path;
}
function prepareSysctls(config) {
var sysctls = {};
$scope.formValues.Sysctls.forEach(function (sysctl) {
if (sysctl.name && sysctl.value) {
sysctls[sysctl.name] = sysctl.value;
}
});
config.HostConfig.Sysctls = sysctls;
}
function prepareResources(config) {
// Memory Limit - Round to 0.125
if ($scope.formValues.MemoryLimit >= 0) {
@@ -432,7 +402,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
prepareResources(config);
prepareLogDriver(config);
prepareCapabilities(config);
prepareSysctls(config);
return config;
}
@@ -578,14 +547,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.config.HostConfig.Devices = path;
}
function loadFromContainerSysctls() {
for (var s in $scope.config.HostConfig.Sysctls) {
if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) {
$scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] });
}
}
}
function loadFromContainerImageConfig() {
RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image)
.then((model) => {
@@ -661,7 +622,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
loadFromContainerImageConfig(d);
loadFromContainerResources(d);
loadFromContainerCapabilities(d);
loadFromContainerSysctls(d);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve container');
@@ -686,7 +646,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
$scope.isAdmin = Authentication.isAdmin();
$scope.showDeviceMapping = await shouldShowDevices();
$scope.showSysctls = await shouldShowSysctls();
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled();
$scope.isAdminOrEndpointAdmin = Authentication.isAdmin();
@@ -971,12 +930,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
}
async function shouldShowSysctls() {
const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application;
return allowSysctlSettingForRegularUsers || Authentication.isAdmin();
}
async function checkIfContainerCapabilitiesEnabled() {
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
}

View File

@@ -31,10 +31,10 @@
</div>
<div ng-if="!formValues.RegistryModel.Registry && fromContainer">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
<span class="small text-danger" style="margin-left: 5px;">
The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that
registry first.
</span>
<span class="small text-danger" style="margin-left: 5px;"
>The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register
that registry first.</span
>
</div>
<div ng-if="formValues.RegistryModel.Registry || !fromContainer">
<!-- image-and-registry -->
@@ -45,30 +45,23 @@
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="formValues.alwaysPull"
on-image-change="onImageNameChange()"
set-validity="setPullImageValidity"
>
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip
position="bottom"
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull" ng-disabled="!state.pullImageValidity" /><i></i>
</label>
</div>
</div>
<!-- !always-pull -->
</por-image-registry>
></por-image-registry>
<!-- !image-and-registry -->
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip
position="bottom"
message="When enabled, Portainer will automatically try to pull the specified image before creating the container."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.alwaysPull" /><i></i> </label>
</div>
</div>
<!-- !always-pull -->
</div>
<div class="col-sm-12 form-section-title">
Network ports configuration
@@ -706,33 +699,6 @@
<!-- !devices-input-list -->
</div>
<!-- !devices-->
<!-- sysctls -->
<div ng-if="showSysctls" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Sysctls</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSysctl()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add sysctl
</span>
</div>
<!-- sysctls-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="sysctl in formValues.Sysctls" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="sysctl.name" placeholder="e.g. FOO" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="sysctl.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSysctl($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !sysctls-input-list -->
</div>
<!-- !sysctls -->
<div class="col-sm-12 form-section-title">
Resources
</div>

View File

@@ -274,17 +274,6 @@
</container-restart-policy>
</td>
</tr>
<tr ng-if="!(container.HostConfig.Sysctls | emptyobject)">
<td>Sysctls</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.HostConfig.Sysctls">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View File

@@ -104,7 +104,6 @@ angular.module('portainer.docker').controller('ContainerController', [
allowContainerCapabilitiesForRegularUsers,
allowHostNamespaceForRegularUsers,
allowDeviceMappingForRegularUsers,
allowSysctlSettingForRegularUsers,
allowBindMountsForRegularUsers,
allowPrivilegedModeForRegularUsers,
} = endpoint.SecuritySettings;
@@ -113,7 +112,6 @@ angular.module('portainer.docker').controller('ContainerController', [
!allowContainerCapabilitiesForRegularUsers ||
!allowBindMountsForRegularUsers ||
!allowDeviceMappingForRegularUsers ||
!allowSysctlSettingForRegularUsers ||
!allowHostNamespaceForRegularUsers ||
!allowPrivilegedModeForRegularUsers;

View File

@@ -15,7 +15,6 @@ export default class DockerFeaturesConfigurationController {
disableStackManagementForRegularUsers: false,
disableDeviceMappingForRegularUsers: false,
disableContainerCapabilitiesForRegularUsers: false,
disableSysctlSettingForRegularUsers: false,
};
this.isAgent = false;
@@ -34,15 +33,13 @@ export default class DockerFeaturesConfigurationController {
disablePrivilegedModeForRegularUsers,
disableDeviceMappingForRegularUsers,
disableContainerCapabilitiesForRegularUsers,
disableSysctlSettingForRegularUsers,
} = this.formValues;
return (
disableBindMountsForRegularUsers ||
disableHostNamespaceForRegularUsers ||
disablePrivilegedModeForRegularUsers ||
disableDeviceMappingForRegularUsers ||
disableContainerCapabilitiesForRegularUsers ||
disableSysctlSettingForRegularUsers
disableContainerCapabilitiesForRegularUsers
);
}
@@ -59,7 +56,6 @@ export default class DockerFeaturesConfigurationController {
allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers,
allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers,
allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers,
allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers,
};
await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings);
@@ -93,7 +89,6 @@ export default class DockerFeaturesConfigurationController {
disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers,
disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers,
disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers,
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
};
}
}

View File

@@ -108,16 +108,6 @@
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableSysctlSettingForRegularUsers"
name="disableSysctlSettingForRegularUsers"
label="Disable sysctl settings 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">

View File

@@ -1,16 +1,14 @@
angular.module('portainer.docker').controller('BuildImageController', [
'$scope',
'$window',
'ModalService',
'$state',
'BuildService',
'Notifications',
'HttpRequestHelper',
function ($scope, $window, ModalService, BuildService, Notifications, HttpRequestHelper) {
function ($scope, $state, BuildService, Notifications, HttpRequestHelper) {
$scope.state = {
BuildType: 'editor',
actionInProgress: false,
activeTab: 0,
isEditorDirty: false,
};
$scope.formValues = {
@@ -22,12 +20,6 @@ angular.module('portainer.docker').controller('BuildImageController', [
NodeName: null,
};
$window.onbeforeunload = () => {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return '';
}
};
$scope.addImageName = function () {
$scope.formValues.ImageNames.push({ Name: '' });
};
@@ -101,13 +93,6 @@ angular.module('portainer.docker').controller('BuildImageController', [
$scope.editorUpdate = function (cm) {
$scope.formValues.DockerFileContent = cm.getValue();
$scope.state.isEditorDirty = true;
};
this.uiCanExit = async function () {
if ($scope.state.BuildType === 'editor' && $scope.formValues.DockerFileContent && $scope.state.isEditorDirty) {
return ModalService.confirmWebEditorDiscard();
}
};
},
]);

View File

@@ -14,41 +14,30 @@
<rd-widget-body>
<form class="form-horizontal">
<!-- image-and-registry -->
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
pull-warning="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
set-validity="setPullImageValidity"
check-rate-limits="true"
>
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12 form-section-title">
Deployment
</div>
<!-- node-selection -->
<node-selector model="formValues.NodeName"> </node-selector>
<!-- !node-selection -->
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !state.pullRateValid"
ng-click="pullImage()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Pull the image</span>
<span ng-show="state.actionInProgress">Download in progress...</span>
</button>
</div>
</div>
</por-image-registry>
<por-image-registry model="formValues.RegistryModel" auto-complete="true" pull-warning="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<!-- !image-and-registry -->
<div ng-if="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12 form-section-title">
Deployment
</div>
<!-- node-selection -->
<node-selector model="formValues.NodeName"> </node-selector>
<!-- !node-selection -->
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
ng-click="pullImage()"
button-spinner="state.actionInProgress"
>
<span ng-hide="state.actionInProgress">Pull the image</span>
<span ng-show="state.actionInProgress">Download in progress...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>

View File

@@ -4,7 +4,6 @@ import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
angular.module('portainer.docker').controller('ImagesController', [
'$scope',
'$state',
'Authentication',
'ImageService',
'Notifications',
'ModalService',
@@ -12,15 +11,10 @@ angular.module('portainer.docker').controller('ImagesController', [
'FileSaver',
'Blob',
'EndpointProvider',
'endpoint',
function ($scope, $state, Authentication, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider, endpoint) {
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) {
$scope.state = {
actionInProgress: false,
exportInProgress: false,
pullRateValid: false,
};
$scope.formValues = {
@@ -146,11 +140,6 @@ angular.module('portainer.docker').controller('ImagesController', [
});
}
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullRateValid = validity;
}
function initView() {
getImages();
}

View File

@@ -31,6 +31,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'HttpRequestHelper',
'NodeService',
'WebhookService',
'EndpointProvider',
'endpoint',
function (
$q,
@@ -56,10 +57,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
HttpRequestHelper,
NodeService,
WebhookService,
EndpointProvider,
endpoint
) {
$scope.endpoint = endpoint;
$scope.formValues = {
Name: '',
RegistryModel: new PorImageRegistryModel(),
@@ -104,7 +104,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope.state = {
formValidationError: '',
actionInProgress: false,
pullImageValidity: false,
};
$scope.allowBindMounts = false;
@@ -115,11 +114,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
});
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullImageValidity = validity;
}
$scope.addPortBinding = function () {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
};
@@ -499,7 +493,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
const resourceControl = data.Portainer.ResourceControl;
const userId = Authentication.getUserDetails().ID;
const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
const webhookPromise = $q.when(endpoint.Type !== 4 && $scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, endpoint.ID));
const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID()));
return $q.all([rcPromise, webhookPromise]);
})
.then(function success() {

View File

@@ -20,17 +20,7 @@
Image configuration
</div>
<!-- image-and-registry -->
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="true"
set-validity="setPullImageValidity"
>
</por-image-registry>
<por-image-registry model="formValues.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
<!-- !image-and-registry -->
<div class="col-sm-12 form-section-title">
Scheduling
@@ -105,21 +95,19 @@
</div>
<!-- !port-mapping -->
<!-- create-webhook -->
<div ng-if="endpoint.Type !== 4">
<div class="col-sm-12 form-section-title">
Webhooks
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a service webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
</div>
<div class="col-sm-12 form-section-title">
Webhooks
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Create a service webhook
<portainer-tooltip
position="top"
message="Create a webhook (or callback URI) to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Webhook" /><i></i> </label>
</div>
</div>
<!-- !create-webhook -->

View File

@@ -3,17 +3,7 @@
<rd-widget-header icon="fa-clone" title-text="Change container image"> </rd-widget-header>
<rd-widget-body ng-if="!isUpdating">
<form class="form-horizontal">
<por-image-registry
model="formValues.RegistryModel"
auto-complete="true"
label-class="col-sm-1"
input-class="col-sm-11"
endpoint="endpoint"
is-admin="isAdmin"
check-rate-limits="true"
set-validity="setPullImageValidity"
>
</por-image-registry>
<por-image-registry model="formValues.RegistryModel" auto-complete="true" label-class="col-sm-1" input-class="col-sm-11"></por-image-registry>
</form>
</rd-widget-body>
<rd-widget-body ng-if="isUpdating">

View File

@@ -28,7 +28,7 @@
name="mountType"
class="form-control"
ng-model="mount.Type"
ng-change="onChangeMountType(service, mount)"
ng-change="updateMount(service, mount)"
ng-disabled="isUpdating"
disable-authorization="DockerServiceUpdate"
>

View File

@@ -85,8 +85,6 @@ angular.module('portainer.docker').controller('ServiceController', [
NetworkService,
endpoint
) {
$scope.endpoint = endpoint;
$scope.state = {
updateInProgress: false,
deletionInProgress: false,
@@ -211,12 +209,6 @@ angular.module('portainer.docker').controller('ServiceController', [
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
}
};
$scope.onChangeMountType = function onChangeMountType(service, mount) {
mount.Source = null;
$scope.updateMount(service, mount);
};
$scope.updateMount = function updateMount(service) {
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};
@@ -534,11 +526,6 @@ angular.module('portainer.docker').controller('ServiceController', [
});
};
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullImageValidity = validity;
}
$scope.updateService = function updateService(service) {
let config = {};
service, (config = buildChanges(service));

View File

@@ -70,12 +70,9 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
function prepareNFSConfiguration(driverOptions) {
var data = $scope.formValues.NFSData;
driverOptions.push({ name: 'type', value: 'nfs' });
driverOptions.push({ name: 'type', value: data.version.toLowerCase() });
var options = 'addr=' + data.serverAddress + ',' + data.options;
if (data.version === 'NFS4') {
options = options + ',nfsvers=4';
}
driverOptions.push({ name: 'o', value: options });
var mountPoint = data.mountPoint[0] === ':' ? data.mountPoint : ':' + data.mountPoint;

View File

@@ -72,7 +72,6 @@ export class EdgeJobFormController {
editorUpdate(cm) {
this.model.FileContent = cm.getValue();
this.isEditorDirty = true;
}
associateEndpoint(endpoint) {

View File

@@ -14,6 +14,5 @@ angular.module('portainer.edge').component('edgeJobForm', {
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
isEditorDirty: '=',
},
});

View File

@@ -6,6 +6,5 @@ export class EditEdgeStackFormController {
editorUpdate(cm) {
this.model.StackFileContent = cm.getValue();
this.isEditorDirty = true;
}
}

View File

@@ -10,6 +10,5 @@ angular.module('portainer.edge').component('editEdgeStackForm', {
actionInProgress: '<',
submitAction: '<',
edgeGroups: '<',
isEditorDirty: '=',
},
});

View File

@@ -14,7 +14,6 @@
form-action="$ctrl.create"
form-action-label="Create edge job"
action-in-progress="$ctrl.state.actionInProgress"
is-editor-dirty="$ctrl.state.isEditorDirty"
></edge-job-form>
</rd-widget-body>
</rd-widget>

View File

@@ -1,9 +1,8 @@
export class CreateEdgeJobViewController {
/* @ngInject */
constructor($async, $q, $state, $window, ModalService, EdgeJobService, GroupService, Notifications, TagService) {
constructor($async, $q, $state, EdgeJobService, GroupService, Notifications, TagService) {
this.state = {
actionInProgress: false,
isEditorDirty: false,
};
this.model = {
@@ -18,8 +17,6 @@ export class CreateEdgeJobViewController {
this.$async = $async;
this.$q = $q;
this.$state = $state;
this.$window = $window;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.GroupService = GroupService;
this.EdgeJobService = EdgeJobService;
@@ -40,7 +37,6 @@ export class CreateEdgeJobViewController {
try {
await this.createEdgeJob(method, this.model);
this.Notifications.success('Edge job successfully created');
this.state.isEditorDirty = false;
this.$state.go('edge.jobs', {}, { reload: true });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create Edge job');
@@ -56,12 +52,6 @@ export class CreateEdgeJobViewController {
return this.EdgeJobService.createEdgeJobFromFileUpload(model);
}
async uiCanExit() {
if (this.model.FileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
async $onInit() {
try {
const [groups, tags] = await Promise.all([this.GroupService.groups(), this.TagService.tags()]);
@@ -70,11 +60,5 @@ export class CreateEdgeJobViewController {
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve page data');
}
this.$window.onbeforeunload = () => {
if (this.model.FileContent && this.state.isEditorDirty) {
return '';
}
};
}
}

View File

@@ -24,7 +24,6 @@
form-action="$ctrl.update"
form-action-label="Update Edge job"
action-in-progress="$ctrl.state.actionInProgress"
is-editor-dirty="$ctrl.state.isEditorDirty"
></edge-job-form>
</uib-tab>

View File

@@ -2,18 +2,15 @@ import _ from 'lodash-es';
export class EdgeJobController {
/* @ngInject */
constructor($async, $q, $state, $window, ModalService, EdgeJobService, EndpointService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) {
constructor($async, $q, $state, EdgeJobService, EndpointService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) {
this.state = {
actionInProgress: false,
showEditorTab: false,
isEditorDirty: false,
};
this.$async = $async;
this.$q = $q;
this.$state = $state;
this.$window = $window;
this.ModalService = ModalService;
this.EdgeJobService = EdgeJobService;
this.EndpointService = EndpointService;
this.FileSaver = FileSaver;
@@ -46,7 +43,6 @@ export class EdgeJobController {
try {
await this.EdgeJobService.updateEdgeJob(model);
this.Notifications.success('Edge job successfully updated');
this.state.isEditorDirty = false;
this.$state.go('edge.jobs', {}, { reload: true });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update Edge job');
@@ -125,12 +121,6 @@ export class EdgeJobController {
this.state.showEditorTab = true;
}
async uiCanExit() {
if (this.edgeJob.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
async $onInit() {
const { id, tab } = this.$state.params;
this.state.activeTab = tab;
@@ -148,7 +138,6 @@ export class EdgeJobController {
]);
edgeJob.FileContent = file.FileContent;
this.oldFileContent = edgeJob.FileContent;
this.edgeJob = edgeJob;
this.groups = groups;
this.tags = tags;
@@ -163,11 +152,5 @@ export class EdgeJobController {
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
}
this.$window.onbeforeunload = () => {
if (this.edgeJob.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
return '';
}
};
}
}

View File

@@ -2,8 +2,8 @@ import _ from 'lodash-es';
export class CreateEdgeStackViewController {
/* @ngInject */
constructor($state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async) {
Object.assign(this, { $state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async });
constructor($state, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async) {
Object.assign(this, { $state, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async });
this.formValues = {
Name: '',
@@ -24,7 +24,6 @@ export class CreateEdgeStackViewController {
formValidationError: '',
actionInProgress: false,
StackType: null,
isEditorDirty: false,
};
this.edgeGroups = null;
@@ -42,12 +41,6 @@ export class CreateEdgeStackViewController {
this.onChangeMethod = this.onChangeMethod.bind(this);
}
async uiCanExit() {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
async $onInit() {
try {
this.edgeGroups = await this.EdgeGroupService.groups();
@@ -62,12 +55,6 @@ export class CreateEdgeStackViewController {
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve Templates');
}
this.$window.onbeforeunload = () => {
if (this.state.Method === 'editor' && this.formValues.StackFileContent && this.state.isEditorDirty) {
return '';
}
};
}
createStack() {
@@ -110,7 +97,6 @@ export class CreateEdgeStackViewController {
await this.createStackByMethod(name, method);
this.Notifications.success('Stack successfully deployed');
this.state.isEditorDirty = false;
this.$state.go('edge.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Unable to deploy stack');
@@ -163,6 +149,5 @@ export class CreateEdgeStackViewController {
editorUpdate(cm) {
this.formValues.StackFileContent = cm.getValue();
this.state.isEditorDirty = true;
}
}

View File

@@ -21,7 +21,6 @@
model="$ctrl.formValues"
action-in-progress="$ctrl.state.actionInProgress"
submit-action="$ctrl.deployStack"
is-editor-dirty="$ctrl.state.isEditorDirty"
></edit-edge-stack-form>
</div>
</uib-tab>

View File

@@ -2,11 +2,9 @@ import _ from 'lodash-es';
export class EditEdgeStackViewController {
/* @ngInject */
constructor($async, $state, $window, ModalService, EdgeGroupService, EdgeStackService, EndpointService, Notifications) {
constructor($async, $state, EdgeGroupService, EdgeStackService, EndpointService, Notifications) {
this.$async = $async;
this.$state = $state;
this.$window = $window;
this.ModalService = ModalService;
this.EdgeGroupService = EdgeGroupService;
this.EdgeStackService = EdgeStackService;
this.EndpointService = EndpointService;
@@ -18,7 +16,6 @@ export class EditEdgeStackViewController {
this.state = {
actionInProgress: false,
activeTab: 0,
isEditorDirty: false,
};
this.deployStack = this.deployStack.bind(this);
@@ -41,22 +38,9 @@ export class EditEdgeStackViewController {
EdgeGroups: this.stack.EdgeGroups,
Prune: this.stack.Prune,
};
this.oldFileContent = this.formValues.StackFileContent;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve stack data');
}
this.$window.onbeforeunload = () => {
if (this.formValues.StackFileContent !== this.oldFileContent && this.state.isEditorDirty) {
return '';
}
};
}
async uiCanExit() {
if (this.formValues.StackFileContent !== this.oldFileContent && this.state.isEditorDirty) {
return this.ModalService.confirmWebEditorDiscard();
}
}
filterStackEndpoints(groupIds, groups) {
@@ -80,7 +64,6 @@ export class EditEdgeStackViewController {
}
await this.EdgeStackService.updateStack(this.stack.Id, this.formValues);
this.Notifications.success('Stack successfully deployed');
this.state.isEditorDirty = false;
this.$state.go('edge.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Unable to deploy stack');

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
}
try {
if (endpoint.Type === 7) {
//edge
try {
await KubernetesHealthService.ping(endpoint.Id);
endpoint.Status = 1;
@@ -28,10 +27,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
EndpointProvider.setEndpointID(endpoint.Id);
await StateManager.updateEndpointState(endpoint, []);
if (endpoint.Type === 7 && endpoint.Status === 2) {
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
}
} catch (e) {
Notifications.error('Failed loading endpoint', e);
$state.go('portainer.home', {}, { reload: true });

View File

@@ -53,19 +53,12 @@
<div
class="col-sm-11 small text-warning"
style="margin-top: 5px;"
ng-show="
kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid ||
(!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) ||
$ctrl.state.invalidKeys[index]
"
ng-show="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || $ctrl.state.duplicateKeys[index] !== undefined"
>
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</ng-messages>
<p ng-if="$ctrl.state.duplicateKeys[index] !== undefined"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key is already defined.</p>
<p ng-if="$ctrl.state.invalidKeys[index]"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key is invalid. A valid key must consist of alphanumeric characters, '-', '_' or '.'</p
>
</div>
</div>

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