Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony Lapenna
56284690a3 feat(k8s/dashboard): remove the RC banner 2020-08-13 11:40:17 +12:00
343 changed files with 2650 additions and 7141 deletions

View File

@@ -1,10 +1,6 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug/need-confirmation, kind/bug
assignees: ''
---
<!--
@@ -13,14 +9,14 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Bug description**
@@ -31,7 +27,7 @@ A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**
@@ -44,7 +40,6 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
- Browser:

View File

@@ -1,20 +1,17 @@
---
name: Question
about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
<!--
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Question**:
How can I deploy Portainer on... ?
---
name: Question
about: Ask us a question about Portainer usage or deployment
---
<!--
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Question**:
How can I deploy Portainer on... ?

View File

@@ -1,34 +1,31 @@
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

3
.github/stale.yml vendored
View File

@@ -15,7 +15,6 @@ issues:
- kind/question
- kind/style
- kind/workaround
- kind/refactor
- bug/need-confirmation
- bug/confirmed
- status/discuss
@@ -48,7 +47,7 @@ issues:
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
and mention @itsconquest. One of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

View File

@@ -74,23 +74,3 @@ Our contribution process is described below. Some of the steps can be visualized
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies with yarn:
```sh
$ yarn
```
Then build and run the project:
```sh
$ yarn start
```
Portainer can now be accessed at <http://localhost:9000>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.

View File

@@ -10,7 +10,7 @@
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo
@@ -24,39 +24,32 @@ Alternatively, you can deploy a copy of the demo stack inside a [play-with-docke
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
## Getting started
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Documentation](https://documentation.portainer.io)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://www.portainer.io/documentation/)
## Getting help
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://documentation.portainer.io
- FAQ: https://www.portainer.io/documentation/faqs/
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
## Security
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
## Limitations
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.

View File

@@ -1,30 +1,13 @@
package bolt
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
if err == errors.ErrObjectNotFound {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.SettingsService.Settings()
_, err := store.SettingsService.Settings()
if err == errors.ErrObjectNotFound {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
@@ -40,11 +23,18 @@ func (store *Store) Init() error {
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowHostNamespaceForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
@@ -89,8 +89,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
}
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.NoAnalytics {
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
if flags.NoAnalytics != nil {
log.Println("Warning: The --no-analytics has been deprecated and will be removed in a future version of Portainer. It currently has no effect, telemetry settings are available in the Portainer settings.")
}
}

View File

@@ -8,6 +8,7 @@ const (
defaultTunnelServerPort = "8000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"

View File

@@ -6,6 +6,7 @@ const (
defaultTunnelServerPort = "8000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,8 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
username, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
return "", err
log.Printf("[DEBUG] - Unable to authenticate user via OAuth: %v", err)
return "", nil
}
return username, nil

View File

@@ -57,17 +57,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
}
for _, existingTemplate := range customTemplates {
if existingTemplate.ID != portainer.CustomTemplateID(customTemplateID) && existingTemplate.Title == payload.Title {
return &httperror.HandlerError{http.StatusInternalServerError, "Template name must be unique", errors.New("Template name must be unique")}
}
}
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import (
"github.com/portainer/portainer/api/http/handler/settings"
"github.com/portainer/portainer/api/http/handler/stacks"
"github.com/portainer/portainer/api/http/handler/status"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/tags"
"github.com/portainer/portainer/api/http/handler/teammemberships"
"github.com/portainer/portainer/api/http/handler/teams"
@@ -54,6 +55,7 @@ type Handler struct {
SettingsHandler *settings.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
SupportHandler *support.Handler
TagHandler *tags.Handler
TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler
@@ -114,6 +116,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/status"):
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/support"):
http.StripPrefix("/api", h.SupportHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/tags"):
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/templates"):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,13 +4,10 @@ import (
"errors"
"net/http"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
@@ -21,11 +18,6 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
@@ -33,6 +25,10 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.Status == portainer.StackStatusActive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
@@ -40,28 +36,6 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
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}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusActive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
}
err = handler.startStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}

View File

@@ -7,11 +7,8 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
// POST request on /api/stacks/:id/stop
@@ -21,11 +18,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
@@ -33,6 +25,10 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
@@ -40,28 +36,6 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
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}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}
err = handler.stopStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}

View File

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

View File

@@ -0,0 +1,26 @@
package support
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle support operations.
type Handler struct {
*mux.Router
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/support",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.supportList))).Methods(http.MethodGet)
return h
}

View File

@@ -0,0 +1,39 @@
package support
import (
"encoding/json"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"net/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type supportProduct struct {
ID int `json:"Id"`
Name string `json:"Name"`
ShortDescription string `json:"ShortDescription"`
Price string `json:"Price"`
PriceDescription string `json:"PriceDescription"`
Description string `json:"Description"`
ProductID string `json:"ProductId"`
}
func (handler *Handler) supportList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
supportData, err := client.Get(portainer.SupportProductsURL, 30)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
}
var supportProducts []supportProduct
err = json.Unmarshal(supportData, &supportProducts)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
}
return response.JSON(w, supportProducts)
}

View File

@@ -155,11 +155,11 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
return err
}
if resourceControl == nil && (executor.operationContext.isAdmin) {
if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) {
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
}
if executor.operationContext.isAdmin || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
responseObject = decorateObject(responseObject, resourceControl)
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
}
@@ -168,7 +168,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
}
func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls)
}
@@ -241,13 +241,13 @@ func (transport *Transport) filterResourceList(parameters *resourceOperationPara
}
if resourceControl == nil {
if context.isAdmin {
if context.isAdmin || context.endpointResourceAccess {
filteredResourceData = append(filteredResourceData, resourceObject)
}
continue
}
if context.isAdmin || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
if context.isAdmin || context.endpointResourceAccess || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
resourceObject = decorateObject(resourceObject, resourceControl)
filteredResourceData = append(filteredResourceData, resourceObject)
}

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import (
"strings"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
@@ -43,10 +43,11 @@ type (
}
restrictedDockerOperationContext struct {
isAdmin bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
isAdmin bool
endpointResourceAccess bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
}
operationExecutor struct {
@@ -155,14 +156,8 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
return transport.administratorOperation(r)
}
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeIDParameter[0])
if err != nil {
return nil, err
}
// volume browser request
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
}
return transport.executeDockerRequest(r)
@@ -407,12 +402,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
if tokenData.Role != portainer.AdministratorRole {
if volumeBrowseRestrictionCheck {
securitySettings, err := transport.fetchEndpointSecuritySettings()
settings, err := transport.dataStore.Settings().Settings()
if err != nil {
return nil, err
}
if !securitySettings.AllowVolumeBrowserForRegularUsers {
if !settings.AllowVolumeBrowserForRegularUsers {
return responseutils.WriteAccessDeniedResponse()
}
}
@@ -559,18 +554,16 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
return response, err
}
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil {
return response, err
}
if resourceControl != nil {
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return response, err
}
if resourceControl != nil {
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return response, err
}
}
}
return response, err
@@ -651,14 +644,25 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
}
operationContext := &restrictedDockerOperationContext{
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
endpointResourceAccess: false,
}
if tokenData.Role != portainer.AdministratorRole {
operationContext.isAdmin = false
user, err := transport.dataStore.User().User(operationContext.userID)
if err != nil {
return nil, err
}
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
if ok {
operationContext.endpointResourceAccess = true
}
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
@@ -682,12 +686,3 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
return tokenData.Role == portainer.AdministratorRole, nil
}
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
if err != nil {
return nil, err
}
return &endpoint.SecuritySettings, nil
}

View File

@@ -168,30 +168,16 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
return transport.rewriteOperation(request, transport.volumeInspectOperation)
}
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, path.Base(requestPath))
cli := transport.dockerClient
volume, err := cli.VolumeInspect(context.Background(), path.Base(requestPath))
if err != nil {
return nil, err
}
volumeID := volume.Name + volume.CreatedAt
if request.Method == http.MethodDelete {
return transport.executeGenericResourceDeletionOperation(request, resourceID, portainer.VolumeResourceControl)
return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl)
}
return transport.restrictedResourceOperation(request, resourceID, portainer.VolumeResourceControl, false)
}
func (transport *Transport) getVolumeResourceID(nodename, volumeID string) (string, error) {
cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodename)
if err != nil {
return "", err
}
defer cli.Close()
volume, err := cli.VolumeInspect(context.Background(), volumeID)
if err != nil {
return "", err
}
return volume.Name + volume.CreatedAt, nil
return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false)
}

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ package kubernetes
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/portainer/portainer/api/http/security"
@@ -14,16 +13,14 @@ import (
type (
localTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
endpointIdentifier portainer.EndpointID
httpTransport *http.Transport
tokenManager *tokenManager
}
agentTransport struct {
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
httpTransport *http.Transport
tokenManager *tokenManager
signatureService portainer.DigitalSignatureService
}
edgeTransport struct {
@@ -53,11 +50,21 @@ func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) {
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return transport.httpTransport.RoundTrip(request)
@@ -78,11 +85,21 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@@ -110,11 +127,21 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) {
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = transport.tokenManager.getAdminServiceAccountToken()
} else {
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
return nil, err
}
}
request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
response, err := transport.httpTransport.RoundTrip(request)
@@ -127,27 +154,3 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
return response, err
}
func getRoundTripToken(
request *http.Request,
tokenManager *tokenManager,
endpointIdentifier portainer.EndpointID,
) (string, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", err
}
var token string
if tokenData.Role == portainer.AdministratorRole {
token = tokenManager.getAdminServiceAccountToken()
} else {
token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID))
if err != nil {
log.Printf("Failed retrieving service account token: %v", err)
return "", err
}
}
return token, nil
}

View File

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

View File

@@ -28,6 +28,7 @@ import (
"github.com/portainer/portainer/api/http/handler/settings"
"github.com/portainer/portainer/api/http/handler/stacks"
"github.com/portainer/portainer/api/http/handler/status"
"github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/tags"
"github.com/portainer/portainer/api/http/handler/teammemberships"
"github.com/portainer/portainer/api/http/handler/teams"
@@ -39,41 +40,39 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
BindAddress string
AssetsPath string
Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService
ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService
SnapshotService portainer.SnapshotService
FileService portainer.FileService
DataStore portainer.DataStore
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
Handler *handler.Handler
SSL bool
SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
}
// Start starts the HTTP server
func (server *Server) Start() error {
kubernetesTokenCacheManager := server.KubernetesTokenCacheManager
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
@@ -84,7 +83,7 @@ func (server *Server) Start() error {
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.LDAPService = server.LDAPService
authHandler.ProxyManager = server.ProxyManager
authHandler.ProxyManager = proxyManager
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
authHandler.OAuthService = server.OAuthService
@@ -118,10 +117,10 @@ func (server *Server) Start() error {
var endpointHandler = endpoints.NewHandler(requestBouncer)
endpointHandler.DataStore = server.DataStore
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = server.ProxyManager
endpointHandler.ProxyManager = proxyManager
endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.DataStore = server.DataStore
@@ -133,7 +132,7 @@ func (server *Server) Start() error {
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.DataStore = server.DataStore
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ProxyManager = proxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
@@ -143,7 +142,7 @@ func (server *Server) Start() error {
var registryHandler = registries.NewHandler(requestBouncer)
registryHandler.DataStore = server.DataStore
registryHandler.FileService = server.FileService
registryHandler.ProxyManager = server.ProxyManager
registryHandler.ProxyManager = proxyManager
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
resourceControlHandler.DataStore = server.DataStore
@@ -174,6 +173,8 @@ func (server *Server) Start() error {
var statusHandler = status.NewHandler(requestBouncer, server.Status)
var supportHandler = support.NewHandler(requestBouncer)
var templatesHandler = templates.NewHandler(requestBouncer)
templatesHandler.DataStore = server.DataStore
templatesHandler.FileService = server.FileService
@@ -216,6 +217,7 @@ func (server *Server) Start() error {
SettingsHandler: settingsHandler,
StatusHandler: statusHandler,
StackHandler: stackHandler,
SupportHandler: supportHandler,
TagHandler: tagHandler,
TeamHandler: teamHandler,
TeamMembershipHandler: teamMembershipHandler,

View File

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

View File

@@ -5,8 +5,9 @@ func KubernetesDefault() KubernetesData {
Configuration: KubernetesConfiguration{
UseLoadBalancer: false,
UseServerMetrics: false,
UseIngress: false,
StorageClasses: []KubernetesStorageClassConfig{},
IngressClasses: []KubernetesIngressClassConfig{},
IngressClasses: []string{},
},
Snapshots: []KubernetesSnapshot{},
}

View File

@@ -40,11 +40,6 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
}
}
// Remove the cached kube client so a new one can be created
func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) {
factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID)))
}
// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found.
// If no client is registered, it will create a new client, register it, and returns it.
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {

View File

@@ -18,11 +18,6 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
Resources: []string{"storageclasses"},
APIGroups: []string{"storage.k8s.io"},
},
{
Verbs: []string{"list"},
Resources: []string{"ingresses"},
APIGroups: []string{"networking.k8s.io"},
},
}
}

View File

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

View File

@@ -88,13 +88,6 @@ func getUsername(token string, configuration *portainer.OAuthSettings) (string,
}
username := values.Get(configuration.UserIdentifier)
if username == "" {
return username, &oauth2.RetrieveError{
Response: resp,
Body: body,
}
}
return username, nil
}

View File

@@ -190,26 +190,24 @@ type (
// Endpoint represents a Docker endpoint with all the info required
// to connect to it
Endpoint struct {
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
SecuritySettings EndpointSecuritySettings
ID EndpointID `json:"Id"`
Name string `json:"Name"`
Type EndpointType `json:"Type"`
URL string `json:"URL"`
GroupID EndpointGroupID `json:"GroupId"`
PublicURL string `json:"PublicURL"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
Extensions []EndpointExtension `json:"Extensions"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
TagIDs []TagID `json:"TagIds"`
Status EndpointStatus `json:"Status"`
Snapshots []DockerSnapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeID string `json:"EdgeID,omitempty"`
EdgeKey string `json:"EdgeKey"`
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
// Deprecated fields
// Deprecated in DBVersion == 4
@@ -273,18 +271,6 @@ type (
// Deprecated
EndpointSyncJob struct{}
// EndpointSecuritySettings represents settings for an endpoint
EndpointSecuritySettings struct {
AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers"`
EnableHostManagementFeatures bool `json:"enableHostManagementFeatures"`
}
// EndpointType represents the type of an endpoint
EndpointType int
@@ -346,8 +332,9 @@ type (
KubernetesConfiguration struct {
UseLoadBalancer bool `json:"UseLoadBalancer"`
UseServerMetrics bool `json:"UseServerMetrics"`
UseIngress bool `json:"UseIngress"`
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
IngressClasses []string `json:"IngressClasses"`
}
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
@@ -358,12 +345,6 @@ type (
AllowVolumeExpansion bool `json:"AllowVolumeExpansion"`
}
// KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration
KubernetesIngressClassConfig struct {
Name string `json:"Name"`
Type string `json:"Type"`
}
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
LDAPGroupSearchSettings struct {
GroupBaseDN string `json:"GroupBaseDN"`
@@ -529,31 +510,29 @@ type (
// Settings represents the application settings
Settings struct {
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
// Deprecated fields v26
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
}
// SnapshotJob represents a scheduled job that can create endpoint snapshots
@@ -570,10 +549,6 @@ type (
Env []Pair `json:"Env"`
ResourceControl *ResourceControl `json:"ResourceControl"`
Status StackStatus `json:"Status"`
CreationDate int64
CreatedBy string
UpdateDate int64
UpdatedBy string
ProjectPath string
}
@@ -794,7 +769,6 @@ type (
// ComposeStackManager represents a service to manage Compose stacks
ComposeStackManager interface {
ComposeSyntaxMaxVersion() string
Up(stack *Stack, endpoint *Endpoint) error
Down(stack *Stack, endpoint *Endpoint) error
}
@@ -1140,17 +1114,17 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.1.0"
APIVersion = "2.0.0-dev"
// DBVersion is the version number of the Portainer database
DBVersion = 26
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
DBVersion = 25
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
// SupportProductsURL represents the URL where Portainer support products can be retrieved
SupportProductsURL = AssetsServerURL + "/support.json"
// PortainerAgentHeader represents the name of the header available in any agent response
PortainerAgentHeader = "Portainer-Agent"
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster

View File

@@ -54,7 +54,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: '2.0.0'
version: '2.0.0-dev'
title: 'Portainer API'
contact:
email: 'info@portainer.io'
@@ -2976,7 +2976,7 @@ definitions:
description: 'Is authentication enabled'
Version:
type: 'string'
example: '2.0.0'
example: '2.0.0-dev'
description: 'Portainer API version'
PublicSettingsInspectResponse:
type: 'object'

View File

@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "2.0.0",
"packageVersion": "2.0.0-dev",
"projectName": "portainer"
}

View File

@@ -1,5 +1,4 @@
export class FileUploaderController {
/* @ngInject */
constructor($async) {
Object.assign(this, { $async });

View File

@@ -3,7 +3,6 @@ import _ from 'lodash-es';
const ROOT_PATH = '/host';
export class HostBrowserController {
/* @ngInject */
constructor($async, HostBrowserService, Notifications, FileSaver, ModalService) {
Object.assign(this, { $async, HostBrowserService, Notifications, FileSaver, ModalService });

View File

@@ -1,7 +1,6 @@
import _ from 'lodash-es';
export class VolumeBrowserController {
/* @ngInject */
constructor($async, HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
Object.assign(this, { $async, HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications });
this.state = {

View File

@@ -16,7 +16,6 @@ angular.module('portainer').run([
EndpointProvider.initialize();
$rootScope.$state = $state;
$rootScope.defaultTitle = document.title;
// Workaround to prevent the loading bar from going backward
// https://github.com/chieffancypants/angular-loading-bar/issues/273

View File

@@ -178,11 +178,6 @@ a[ng-click] {
word-break: break-word;
}
.widget .widget-body table.container-details-table > tbody > tr > td:first-child {
word-break: normal;
text-transform: uppercase;
}
.widget .widget-body table.description-table {
table-layout: fixed;
}
@@ -625,6 +620,57 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
margin-left: 21px;
}
/* switch box */
:root {
--switch-size: 24px;
}
.switch input {
display: none;
}
.switch i,
.bootbox-form .checkbox i {
display: inline-block;
vertical-align: middle;
cursor: pointer;
padding-right: var(--switch-size);
transition: all ease 0.2s;
-webkit-transition: all ease 0.2s;
-moz-transition: all ease 0.2s;
-o-transition: all ease 0.2s;
border-radius: var(--switch-size);
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch i:before,
.bootbox-form .checkbox i:before {
display: block;
content: '';
width: var(--switch-size);
height: var(--switch-size);
border-radius: var(--switch-size);
background: white;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5);
}
.switch :checked + i,
.bootbox-form .checkbox :checked ~ i {
padding-right: 0;
padding-left: var(--switch-size);
-webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
-moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7;
}
/* !switch box */
/* small switch box */
.switch.small {
--switch-size: 12px;
}
/* !small switch box */
.boxselector_wrapper {
display: flex;
flex-flow: row wrap;
@@ -876,27 +922,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
z-index: 2;
}
.striketext:before,
.striketext:after {
background-color: #777777;
content: '';
display: inline-block;
height: 1px;
position: relative;
vertical-align: middle;
width: 50%;
}
.striketext:before {
right: 0.5em;
margin-left: -50%;
}
.striketext:after {
left: 0.5em;
margin-right: -50%;
}
/*bootbox override*/
.modal-open {
padding-right: 0 !important;
@@ -1012,7 +1037,3 @@ json-tree .branch-preview {
background-color: #337ab7;
}
/* !spinkit override */
.w-full {
width: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -39,9 +39,6 @@ export function CreateContainerGroupRequest(model) {
var addressPorts = [];
for (var i = 0; i < model.Ports.length; i++) {
var binding = model.Ports[i];
if (!binding.container || !binding.host) {
continue;
}
containerPorts.push({
port: binding.container,

View File

@@ -14,7 +14,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
actionInProgress: false,
selectedSubscription: null,
selectedResourceGroup: null,
formValidationError: '',
};
$scope.changeSubscription = function () {
@@ -35,11 +34,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
var subscriptionId = $scope.state.selectedSubscription.Id;
var resourceGroupName = $scope.state.selectedResourceGroup.Name;
$scope.state.formValidationError = validateForm(model);
if ($scope.state.formValidationError) {
return false;
}
$scope.state.actionInProgress = true;
AzureService.createContainerGroup(model, subscriptionId, resourceGroupName)
.then(function success() {
@@ -47,7 +41,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
$state.go('azure.containerinstances');
})
.catch(function error(err) {
err = err.data ? err.data.error : err;
Notifications.error('Failure', err, 'Unable to create container');
})
.finally(function final() {
@@ -55,14 +48,6 @@ angular.module('portainer.azure').controller('AzureCreateContainerInstanceContro
});
};
function validateForm(model) {
if (!model.Ports || !model.Ports.length || model.Ports.every((port) => !port.host || !port.container)) {
return 'At least one port binding is required';
}
return null;
}
function updateResourceGroupsAndLocations(subscription, resourceGroups, providers) {
$scope.state.selectedResourceGroup = resourceGroups[subscription.Id][0];
$scope.resourceGroups = resourceGroups[subscription.Id];

View File

@@ -7,7 +7,7 @@
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" autocomplete="off" name="aciForm">
<form class="form-horizontal" autocomplete="off">
<div class="col-sm-12 form-section-title">
Azure settings
</div>
@@ -53,14 +53,7 @@
<div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Name" name="container_name" placeholder="e.g. myContainer" required />
</div>
</div>
<div class="form-group" ng-show="aciForm.container_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="aciForm.container_name.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Name is required. </p>
</div>
<input type="text" class="form-control" ng-model="model.Name" name="container_name" placeholder="e.g. myContainer" />
</div>
</div>
<!-- !name-input -->
@@ -68,14 +61,7 @@
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="model.Image" name="image_name" placeholder="e.g. nginx:alpine" required />
</div>
</div>
<div class="form-group" ng-show="aciForm.image_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="aciForm.image_name.$error">
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Image is required. </p>
</div>
<input type="text" class="form-control" ng-model="model.Image" name="image_name" placeholder="e.g. nginx:alpine" />
</div>
</div>
<!-- !image-input -->
@@ -167,7 +153,6 @@
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->

View File

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

View File

@@ -4,8 +4,67 @@
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div class="settings">
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
<span
class="setting"
ng-class="{ 'setting-active': $ctrl.columnVisibility.state.open }"
uib-dropdown
dropdown-append-to-body
auto-close="disabled"
is-open="$ctrl.columnVisibility.state.open"
>
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Show / Hide Columns
</div>
<div class="menuContent">
<div class="md-checkbox">
<input id="col_vis_state" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.state.display" />
<label for="col_vis_state" ng-bind="$ctrl.columnVisibility.columns.state.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_actions" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.actions.display" />
<label for="col_vis_actions" ng-bind="$ctrl.columnVisibility.columns.actions.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_stack" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.stack.display" />
<label for="col_vis_stack" ng-bind="$ctrl.columnVisibility.columns.stack.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_image" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.image.display" />
<label for="col_vis_image" ng-bind="$ctrl.columnVisibility.columns.image.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_created" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.created.display" />
<label for="col_vis_created" ng-bind="$ctrl.columnVisibility.columns.created.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_ip" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.ip.display" />
<label for="col_vis_ip" ng-bind="$ctrl.columnVisibility.columns.ip.label"></label>
</div>
<div class="md-checkbox" ng-if="$ctrl.showHostColumn">
<input id="col_vis_host" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.host.display" />
<label for="col_vis_host" ng-bind="$ctrl.columnVisibility.columns.host.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_ports" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.ports.display" />
<label for="col_vis_ports" ng-bind="$ctrl.columnVisibility.columns.ports.label"></label>
</div>
<div class="md-checkbox">
<input id="col_vis_ownership" ng-click="$ctrl.onColumnVisibilityChange()" type="checkbox" ng-model="$ctrl.columnVisibility.columns.ownership.display" />
<label for="col_vis_ownership" ng-bind="$ctrl.columnVisibility.columns.ownership.label"></label>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
</div>
</div>
</div>
</span>
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>

View File

@@ -36,6 +36,9 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
};
this.columnVisibility = {
state: {
open: false,
},
columns: {
state: {
label: 'State',
@@ -57,6 +60,10 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
label: 'Created',
display: true,
},
ip: {
label: 'IP Address',
display: true,
},
host: {
label: 'Host',
display: true,
@@ -72,11 +79,9 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
},
};
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
function onColumnVisibilityChange(columns) {
this.columnVisibility.columns = columns;
this.onColumnVisibilityChange = function () {
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
}
};
this.onSelectionChanged = function () {
this.updateSelectionState();
@@ -198,6 +203,7 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
if (storedColumnVisibility !== null) {
this.columnVisibility = storedColumnVisibility;
this.columnVisibility.state.open = false;
}
};
},

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,5 @@ angular.module('portainer.docker').component('logViewer', {
logCollectionChange: '<',
sinceTimestamp: '=',
lineCount: '=',
resourceName: '<',
},
});

View File

@@ -67,7 +67,6 @@
Actions
</label>
<div class="col-sm-11">
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0;"><i class="fa fa-download"></i> Download logs</button>
<button
class="btn btn-primary btn-sm"
ng-click="$ctrl.copy()"

View File

@@ -1,11 +1,8 @@
import moment from 'moment';
import _ from 'lodash-es';
angular.module('portainer.docker').controller('LogViewerController', [
'clipboard',
'Blob',
'FileSaver',
function (clipboard, Blob, FileSaver) {
function (clipboard) {
this.state = {
availableSinceDatetime: [
{ desc: 'Last day', value: moment().subtract(1, 'days').format() },
@@ -46,10 +43,5 @@ angular.module('portainer.docker').controller('LogViewerController', [
this.state.selectedLines.splice(idx, 1);
}
};
this.downloadLogs = function () {
const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]);
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
};
},
]);

View File

@@ -1,6 +1,6 @@
export function MacvlanFormData() {
this.Scope = 'local';
this.SelectedNetworkConfig = null;
this.SelectedNetworkConfig = '';
this.DatatableState = {
selectedItems: [],
};

View File

@@ -1,18 +1,14 @@
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
function b64DecodeUnicode(str) {
try {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
} catch (err) {
return atob(str);
}
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
}
export function ConfigViewModel(data) {

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ angular.module('portainer.docker').factory('Container', [
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
interceptor: ContainersInterceptor,
timeout: 15000,
},
get: {
method: 'GET',
@@ -47,17 +48,20 @@ angular.module('portainer.docker').factory('Container', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500,
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
timeout: 4500,
ignoreLoadingBar: true,
},
start: {

View File

@@ -16,7 +16,7 @@ angular.module('portainer.docker').factory('Image', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor },
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor, timeout: 15000 },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },

View File

@@ -18,6 +18,7 @@ angular.module('portainer.docker').factory('Network', [
method: 'GET',
isArray: true,
interceptor: NetworksInterceptor,
timeout: 15000,
},
get: {
method: 'GET',

View File

@@ -35,6 +35,7 @@ angular.module('portainer.docker').factory('Service', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,9 +18,10 @@ angular.module('portainer.docker').factory('System', [
info: {
method: 'GET',
params: { action: 'info' },
timeout: 15000,
interceptor: InfoInterceptor,
},
version: { method: 'GET', params: { action: 'version' }, interceptor: VersionInterceptor },
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },

View File

@@ -17,6 +17,7 @@ angular.module('portainer.docker').factory('Task', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,7 +18,7 @@ angular.module('portainer.docker').factory('Volume', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', interceptor: VolumesInterceptor },
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',

View File

@@ -27,9 +27,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'ModalService',
'RegistryService',
'SystemService',
'SettingsService',
'PluginService',
'HttpRequestHelper',
'endpoint',
function (
$q,
$scope,
@@ -53,9 +53,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
ModalService,
RegistryService,
SystemService,
SettingsService,
PluginService,
HttpRequestHelper,
endpoint
HttpRequestHelper
) {
$scope.create = create;
@@ -63,7 +63,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
alwaysPull: true,
Console: 'none',
Volumes: [],
NetworkContainer: null,
NetworkContainer: '',
Labels: [],
ExtraHosts: [],
MacAddress: '',
@@ -121,7 +121,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
NetworkMode: 'bridge',
Privileged: false,
Init: false,
Runtime: null,
Runtime: '',
ExtraHosts: [],
Devices: [],
CapAdd: [],
@@ -696,6 +696,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
SystemService.info()
.then(function success(data) {
$scope.availableRuntimes = data.Runtimes ? Object.keys(data.Runtimes) : [];
$scope.config.HostConfig.Runtime = '';
$scope.state.sliderMaxCpu = 32;
if (data.NCPU) {
$scope.state.sliderMaxCpu = data.NCPU;
@@ -709,8 +710,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
Notifications.error('Failure', err, 'Unable to retrieve engine details');
});
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
SettingsService.publicSettings()
.then(function success(data) {
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers;
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
});
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
$scope.availableLoggingDrivers = loggingDrivers;
@@ -927,11 +934,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
async function shouldShowDevices() {
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
}
async function checkIfContainerCapabilitiesEnabled() {
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
}
initView();

View File

@@ -214,7 +214,7 @@
<rd-widget>
<rd-widget-header icon="fa-server" title-text="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table container-details-table">
<table class="table">
<tbody>
<tr>
<td>Image</td>

View File

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

View File

@@ -12,5 +12,4 @@
display-timestamps="state.displayTimestamps"
line-count="state.lineCount"
since-timestamp="state.sinceTimestamp"
resource-name="container.Name"
></log-viewer>

View File

@@ -57,7 +57,14 @@
</tr>
<tr>
<td>Tags</td>
<td>{{ endpointTags }}</td>
<td>
<span ng-if="endpoint.Tags.length === 0">
-
</span>
<span ng-if="endpoint.Tags.length > 0">
<span ng-repeat="tag in endpoint.Tags"> {{ tag }}{{ $last ? '' : ', ' }} </span>
</span>
</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<td colspan="2">

View File

@@ -1,6 +1,3 @@
import angular from 'angular';
import _ from 'lodash';
angular.module('portainer.docker').controller('DashboardController', [
'$scope',
'$q',
@@ -16,8 +13,6 @@ angular.module('portainer.docker').controller('DashboardController', [
'Notifications',
'EndpointProvider',
'StateManager',
'TagService',
'endpoint',
function (
$scope,
$q,
@@ -32,9 +27,7 @@ angular.module('portainer.docker').controller('DashboardController', [
EndpointService,
Notifications,
EndpointProvider,
StateManager,
TagService,
endpoint
StateManager
) {
$scope.dismissInformationPanel = function (id) {
StateManager.dismissInformationPanel(id);
@@ -59,7 +52,6 @@ angular.module('portainer.docker').controller('DashboardController', [
stacks: StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId),
info: SystemService.info(),
endpoint: EndpointService.endpoint(endpointId),
tags: TagService.tags(),
})
.then(function success(data) {
$scope.containers = data.containers;
@@ -70,18 +62,6 @@ angular.module('portainer.docker').controller('DashboardController', [
$scope.stackCount = data.stacks.length;
$scope.info = data.info;
$scope.endpoint = data.endpoint;
$scope.endpointTags = $scope.endpoint.TagIds.length
? _.join(
_.filter(
_.map($scope.endpoint.TagIds, (id) => {
const tag = data.tags.find((tag) => tag.Id === id);
return tag ? tag.Name : '';
}),
Boolean
),
', '
)
: '-';
$scope.offlineMode = EndpointProvider.offlineMode();
})
.catch(function error(err) {
@@ -91,8 +71,9 @@ angular.module('portainer.docker').controller('DashboardController', [
async function shouldShowStacks() {
const isAdmin = Authentication.isAdmin();
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers;
return isAdmin || allowStackManagementForRegularUsers;
}
initView();

View File

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

View File

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

View File

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

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