Compare commits
108 Commits
fix-k8s-sy
...
fix/CE/484
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f924a16e9b | ||
|
|
e4184b4025 | ||
|
|
5c1888bfc6 | ||
|
|
bc459b55ae | ||
|
|
f2ec7605c2 | ||
|
|
81b4672076 | ||
|
|
0cfa912d77 | ||
|
|
fc0de913c3 | ||
|
|
f7e6ba544e | ||
|
|
24b1894a84 | ||
|
|
46dec01fe3 | ||
|
|
e401724d43 | ||
|
|
d2d7f6fdb9 | ||
|
|
b747f5f81e | ||
|
|
afbd353808 | ||
|
|
51d584bb50 | ||
|
|
36fbaa9026 | ||
|
|
a71e71f481 | ||
|
|
83f4c5ec0b | ||
|
|
41308d570d | ||
|
|
46ff8a01bc | ||
|
|
2b257d2785 | ||
|
|
da41dbb79a | ||
|
|
68d42617f2 | ||
|
|
8323e22309 | ||
|
|
20d4341170 | ||
|
|
832cafc933 | ||
|
|
f3c537ac2c | ||
|
|
958baf6283 | ||
|
|
08e392378e | ||
|
|
a2d9734b8b | ||
|
|
15aed9fc6f | ||
|
|
121d33538d | ||
|
|
7a03351df8 | ||
|
|
0c2987893d | ||
|
|
d1eddaa188 | ||
|
|
d336ada3c2 | ||
|
|
839198fbff | ||
|
|
486ffa5bbd | ||
|
|
4cd468ce21 | ||
|
|
cbd7fdc62e | ||
|
|
b9fe8009dd | ||
|
|
6a504e7134 | ||
|
|
51ba0876a5 | ||
|
|
769e6a4c6c | ||
|
|
105d1ae519 | ||
|
|
cf508065ec | ||
|
|
eab828279e | ||
|
|
d5763a970b | ||
|
|
c9f68a4d8f | ||
|
|
7848bcf2f4 | ||
|
|
b924347c5b | ||
|
|
9fbda9fb99 | ||
|
|
82f8062784 | ||
|
|
49982eb98a | ||
|
|
4be3ac470f | ||
|
|
a50ab51bef | ||
|
|
7975ef796d | ||
|
|
f8b226a1ef | ||
|
|
342a0d6d22 | ||
|
|
58bf76a58f | ||
|
|
bd98b8956a | ||
|
|
4bc958f865 | ||
|
|
b67c0e870c | ||
|
|
067257df2b | ||
|
|
5f2f7a87ab | ||
|
|
f656ad7124 | ||
|
|
f681e2d532 | ||
|
|
fdb9bf09de | ||
|
|
92ad3e788d | ||
|
|
bc2f5a3260 | ||
|
|
487123491e | ||
|
|
380f106571 | ||
|
|
341378e783 | ||
|
|
b360936454 | ||
|
|
8204d32538 | ||
|
|
60c5ab3eec | ||
|
|
20cf948e53 | ||
|
|
45fcb1ad26 | ||
|
|
7398d54ed0 | ||
|
|
faded67deb | ||
|
|
eadd8b36d6 | ||
|
|
1ad4623b08 | ||
|
|
890bbf4058 | ||
|
|
865c8d899b | ||
|
|
aa5277de2e | ||
|
|
9136ba30eb | ||
|
|
3d9c10adf1 | ||
|
|
0d20988bef | ||
|
|
1545a42f08 | ||
|
|
a12f2ee893 | ||
|
|
174e28b850 | ||
|
|
3da9751c82 | ||
|
|
ccea7cca3d | ||
|
|
43891703c2 | ||
|
|
74429d6d46 | ||
|
|
bb5c2c2875 | ||
|
|
3e82d01894 | ||
|
|
9e80037e72 | ||
|
|
da29c2b6a5 | ||
|
|
0ed4d443ee | ||
|
|
a4fa44f831 | ||
|
|
e479e41aee | ||
|
|
d4c4c4e895 | ||
|
|
466bd24648 | ||
|
|
2fc60f14e1 | ||
|
|
9300603777 | ||
|
|
8dac2df7bf |
11
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -9,14 +13,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.io/slack/.
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
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
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Bug description**
|
||||
@@ -27,7 +31,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://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
@@ -40,6 +44,7 @@ You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-d
|
||||
|
||||
- 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:
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/Custom.md
vendored
37
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -1,17 +1,20 @@
|
||||
---
|
||||
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... ?
|
||||
---
|
||||
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... ?
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,31 +1,34 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
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.
|
||||
|
||||
3
.github/stale.yml
vendored
3
.github/stale.yml
vendored
@@ -15,6 +15,7 @@ issues:
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- kind/refactor
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
@@ -47,7 +48,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
|
||||
and mention @itsconquest. One of our staff will then review the issue.
|
||||
mentioning `ametdoohan`, `balasu` or `keverv` and 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.
|
||||
|
||||
@@ -74,3 +74,23 @@ Our contribution process is described below. Some of the steps can be visualized
|
||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
||||
|
||||

|
||||
|
||||
## 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/>.
|
||||
|
||||
23
README.md
23
README.md
@@ -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,32 +24,39 @@ 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 same, including default credentials.
|
||||
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||
- [Documentation](https://www.portainer.io/documentation/)
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
## Getting help
|
||||
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
|
||||
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- FAQ: https://www.portainer.io/documentation/faqs/
|
||||
- FAQ: https://documentation.portainer.io
|
||||
- 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://www.portainer.io/documentation/how-to-contribute/) 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://documentation.portainer.io/contributing/instructions/) 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.
|
||||
|
||||
@@ -40,18 +40,11 @@ func (store *Store) Init() error {
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
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,
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
51
api/bolt/migrator/migrate_dbversion25.go
Normal file
51
api/bolt/migrator/migrate_dbversion25.go
Normal file
@@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
@@ -342,5 +342,13 @@ 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)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
)
|
||||
@@ -47,6 +48,8 @@ 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()
|
||||
@@ -58,7 +61,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Username == username {
|
||||
if strings.ToLower(u.Username) == username {
|
||||
user = &u
|
||||
break
|
||||
}
|
||||
@@ -123,6 +126,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -133,6 +137,7 @@ 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 {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
@@ -17,6 +17,8 @@ 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"
|
||||
@@ -71,7 +73,12 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
|
||||
if composeWrapper != nil {
|
||||
return composeWrapper
|
||||
}
|
||||
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
}
|
||||
|
||||
@@ -89,6 +96,10 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if settings.UserSessionTimeout == "" {
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -237,6 +248,18 @@ 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://") {
|
||||
@@ -286,6 +309,18 @@ 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)
|
||||
@@ -380,8 +415,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
||||
|
||||
@@ -448,27 +485,29 @@ 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,
|
||||
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,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
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)
|
||||
|
||||
132
api/exec/compose_wrapper.go
Normal file
132
api/exec/compose_wrapper.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
)
|
||||
|
||||
// ComposeWrapper is a wrapper for docker-compose binary
|
||||
type ComposeWrapper struct {
|
||||
binaryPath string
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
|
||||
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ComposeWrapper{
|
||||
binaryPath: binaryPath,
|
||||
proxyManager: proxyManager,
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
|
||||
return portainer.ComposeSyntaxMaxVersion
|
||||
}
|
||||
|
||||
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
|
||||
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
|
||||
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
|
||||
if endpoint == nil {
|
||||
return nil, errors.New("cannot call a compose command on an empty endpoint")
|
||||
}
|
||||
|
||||
program := programPath(w.binaryPath, "docker-compose")
|
||||
|
||||
options := setComposeFile(stack)
|
||||
|
||||
options = addProjectNameOption(options, stack)
|
||||
options, err := addEnvFileOption(options, stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
|
||||
|
||||
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer proxy.Close()
|
||||
|
||||
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
|
||||
}
|
||||
|
||||
args := append(options, command...)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(program, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return out, errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func setComposeFile(stack *portainer.Stack) []string {
|
||||
options := make([]string, 0)
|
||||
|
||||
if stack == nil || stack.EntryPoint == "" {
|
||||
return options
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
options = append(options, "-f", composeFilePath)
|
||||
return options
|
||||
}
|
||||
|
||||
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
|
||||
if stack == nil || stack.Name == "" {
|
||||
return options
|
||||
}
|
||||
|
||||
options = append(options, "-p", stack.Name)
|
||||
return options
|
||||
}
|
||||
|
||||
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
|
||||
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
|
||||
return options, nil
|
||||
}
|
||||
|
||||
envFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
|
||||
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return options, err
|
||||
}
|
||||
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
envfile.Close()
|
||||
|
||||
options = append(options, "--env-file", envFilePath)
|
||||
return options, nil
|
||||
}
|
||||
75
api/exec/compose_wrapper_integration_test.go
Normal file
75
api/exec/compose_wrapper_integration_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// +build integration
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const composeFile = `version: "3.9"
|
||||
services:
|
||||
busybox:
|
||||
image: "alpine:latest"
|
||||
container_name: "compose_wrapper_test"`
|
||||
const composedContainerName = "compose_wrapper_test"
|
||||
|
||||
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||
dir := t.TempDir()
|
||||
composeFileName := "compose_wrapper_test.yml"
|
||||
f, _ := os.Create(filepath.Join(dir, composeFileName))
|
||||
f.WriteString(composeFile)
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: composeFileName,
|
||||
Name: "project-name",
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{}
|
||||
|
||||
return stack, endpoint
|
||||
}
|
||||
|
||||
func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w := NewComposeWrapper("", nil)
|
||||
|
||||
err := w.Up(stack, endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling docker-compose up: %s", err)
|
||||
}
|
||||
|
||||
if containerExists(composedContainerName) == false {
|
||||
t.Fatal("container should exist")
|
||||
}
|
||||
|
||||
err = w.Down(stack, endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling docker-compose down: %s", err)
|
||||
}
|
||||
|
||||
if containerExists(composedContainerName) {
|
||||
t.Fatal("container should be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func containerExists(contaierName string) bool {
|
||||
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to list containers: %s", err)
|
||||
}
|
||||
|
||||
return strings.Contains(string(out), contaierName)
|
||||
}
|
||||
143
api/exec/compose_wrapper_test.go
Normal file
143
api/exec/compose_wrapper_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_setComposeFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "should return empty result if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should return empty result if stack don't have entrypoint",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should allow file name and dir",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: "dir",
|
||||
EntryPoint: "file",
|
||||
},
|
||||
expected: []string{"-f", path.Join("dir", "file")},
|
||||
},
|
||||
{
|
||||
name: "should allow file name only",
|
||||
stack: &portainer.Stack{
|
||||
EntryPoint: "file",
|
||||
},
|
||||
expected: []string{"-f", "file"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := setComposeFile(tt.stack)
|
||||
assert.ElementsMatch(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addProjectNameOption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "should not add project option if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add project option if stack doesn't have name",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should add project name option if stack has a name",
|
||||
stack: &portainer.Stack{
|
||||
Name: "project-name",
|
||||
},
|
||||
expected: []string{"-p", "project-name"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := []string{"-a", "b"}
|
||||
result := addProjectNameOption(options, tt.stack)
|
||||
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addEnvFileOption(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
expectedContent string
|
||||
}{
|
||||
{
|
||||
name: "should not add env file option if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add env file option if stack doesn't have env variables",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add env file option if stack's env variables are empty",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
Env: []portainer.Pair{},
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should add env file option if stack has env variables",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
Env: []portainer.Pair{
|
||||
{Name: "var1", Value: "value1"},
|
||||
{Name: "var2", Value: "value2"},
|
||||
},
|
||||
},
|
||||
expected: []string{"--env-file", path.Join(dir, "stack.env")},
|
||||
expectedContent: "var1=value1\nvar2=value2\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := []string{"-a", "b"}
|
||||
result, _ := addEnvFileOption(options, tt.stack)
|
||||
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||
|
||||
if tt.expectedContent != "" {
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
content, _ := ioutil.ReadAll(f)
|
||||
|
||||
assert.Equal(t, tt.expectedContent, string(content))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,8 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
} else {
|
||||
args = append(args, "--tlscacert", "''")
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
|
||||
24
api/exec/utils.go
Normal file
24
api/exec/utils.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func osProgram(program string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
program += ".exe"
|
||||
}
|
||||
return program
|
||||
}
|
||||
|
||||
func programPath(rootPath, program string) string {
|
||||
return filepath.Join(rootPath, osProgram(program))
|
||||
}
|
||||
|
||||
// IsBinaryPresent returns true if corresponding program exists on PATH
|
||||
func IsBinaryPresent(program string) bool {
|
||||
_, err := exec.LookPath(program)
|
||||
return err == nil
|
||||
}
|
||||
16
api/exec/utils_test.go
Normal file
16
api/exec/utils_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_isBinaryPresent(t *testing.T) {
|
||||
|
||||
if !IsBinaryPresent("docker") {
|
||||
t.Error("expect docker binary to exist on the path")
|
||||
}
|
||||
|
||||
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
|
||||
t.Error("expect binary with a random name to be missing on the path")
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
|
||||
@@ -262,12 +262,15 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/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=
|
||||
@@ -392,6 +395,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.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=
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
@@ -440,6 +440,18 @@ 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
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
hideFields(endpoint)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -89,6 +88,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
for idx := range paginatedEndpoints {
|
||||
hideFields(&paginatedEndpoints[idx])
|
||||
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
}
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
|
||||
90
api/http/handler/endpoints/endpoint_settings_update.go
Normal file
90
api/http/handler/endpoints/endpoint_settings_update.go
Normal file
@@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type Handler struct {
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SnapshotService portainer.SnapshotService
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
@@ -38,6 +39,8 @@ 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",
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
registry.Username = *payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != nil {
|
||||
if payload.Password != nil && *payload.Password != "" {
|
||||
registry.Password = *payload.Password
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,11 @@ import (
|
||||
)
|
||||
|
||||
type publicSettingsResponse struct {
|
||||
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"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -33,18 +25,10 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
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,
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
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,
|
||||
|
||||
@@ -14,25 +14,17 @@ import (
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -107,38 +99,10 @@ 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 {
|
||||
@@ -158,10 +122,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -60,13 +61,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -89,6 +91,8 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -146,13 +150,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -185,6 +190,8 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -242,13 +249,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -271,6 +279,8 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -329,31 +339,27 @@ 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
|
||||
}
|
||||
|
||||
if (!settings.AllowBindMountsForRegularUsers ||
|
||||
!settings.AllowPrivilegedModeForRegularUsers ||
|
||||
!settings.AllowHostNamespaceForRegularUsers ||
|
||||
!settings.AllowDeviceMappingForRegularUsers ||
|
||||
!settings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
securitySettings := &config.endpoint.SecuritySettings
|
||||
|
||||
if (!securitySettings.AllowBindMountsForRegularUsers ||
|
||||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
|
||||
!securitySettings.AllowHostNamespaceForRegularUsers ||
|
||||
!securitySettings.AllowDeviceMappingForRegularUsers ||
|
||||
!securitySettings.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, settings)
|
||||
err = handler.isValidStackFile(stackContent, securitySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -55,14 +56,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -85,6 +87,8 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -145,14 +149,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -185,6 +190,8 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -249,14 +256,15 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -279,6 +287,8 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -334,16 +344,13 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
}
|
||||
|
||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) 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
|
||||
}
|
||||
|
||||
settings := &config.endpoint.SecuritySettings
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -78,6 +78,17 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdmin(userID portainer.UserID) (bool, error) {
|
||||
user, err := handler.DataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
|
||||
@@ -46,12 +46,14 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if !settings.AllowStackManagementForRegularUsers {
|
||||
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
@@ -69,13 +71,6 @@ 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}
|
||||
@@ -129,7 +124,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, settings *portainer.Settings) error {
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
|
||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,7 +149,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
|
||||
for key := range composeConfig.Services {
|
||||
service := composeConfig.Services[key]
|
||||
if !settings.AllowBindMountsForRegularUsers {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers {
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
@@ -162,19 +157,19 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
}
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
return errors.New("privileged mode disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
return errors.New("pid host disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
return errors.New("device mapping disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
return errors.New("container capabilities disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
@@ -183,9 +178,20 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
}
|
||||
|
||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||
resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
var resourceControl *portainer.ResourceControl
|
||||
|
||||
err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
isAdmin, err := handler.userIsAdmin(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
|
||||
} else {
|
||||
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
}
|
||||
|
||||
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||
}
|
||||
|
||||
@@ -155,5 +155,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,15 +4,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// POST request on /api/stacks/:id/stop
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -135,6 +136,9 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
@@ -163,6 +167,9 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,23 +197,23 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
return forbiddenResponse, errors.New("forbidden to use privileged mode")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
return forbiddenResponse, errors.New("forbidden to use device mapping")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
return nil, errors.New("forbidden to use container capabilities")
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
|
||||
if !securitySettings.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")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -407,12 +407,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
if volumeBrowseRestrictionCheck {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowVolumeBrowserForRegularUsers {
|
||||
if !securitySettings.AllowVolumeBrowserForRegularUsers {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
@@ -559,16 +559,18 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
|
||||
return response, err
|
||||
}
|
||||
|
||||
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 response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
@@ -680,3 +682,12 @@ 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
|
||||
}
|
||||
|
||||
88
api/http/proxy/factory/docker_compose.go
Normal file
88
api/http/proxy/factory/docker_compose.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
|
||||
)
|
||||
|
||||
// ProxyServer provide an extedned proxy with a local server to forward requests
|
||||
type ProxyServer struct {
|
||||
server *http.Server
|
||||
Port int
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
return &ProxyServer{
|
||||
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL.Scheme = "http"
|
||||
httpTransport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpTransport.TLSClientConfig = config
|
||||
endpointURL.Scheme = "https"
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
|
||||
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||
|
||||
proxyServer := &ProxyServer{
|
||||
&http.Server{
|
||||
Handler: proxy,
|
||||
},
|
||||
0,
|
||||
}
|
||||
|
||||
return proxyServer, proxyServer.start()
|
||||
}
|
||||
|
||||
func (proxy *ProxyServer) start() error {
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Port = listener.Addr().(*net.TCPAddr).Port
|
||||
go func() {
|
||||
proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port)
|
||||
log.Printf("Starting Proxy server on %s...\n", proxyHost)
|
||||
|
||||
err := proxy.server.Serve(listener)
|
||||
log.Printf("Exiting Proxy server %s\n", proxyHost)
|
||||
|
||||
if err != http.ErrServerClosed {
|
||||
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts down the server
|
||||
func (proxy *ProxyServer) Close() {
|
||||
if proxy.server != nil {
|
||||
proxy.server.Close()
|
||||
}
|
||||
}
|
||||
40
api/http/proxy/factory/dockercompose/transport.go
Normal file
40
api/http/proxy/factory/dockercompose/transport.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package dockercompose
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
|
||||
AgentTransport struct {
|
||||
httpTransport *http.Transport
|
||||
signatureService portainer.DigitalSignatureService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
)
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
|
||||
transport := &AgentTransport{
|
||||
httpTransport: httpTransport,
|
||||
signatureService: signatureService,
|
||||
}
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
|
||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
|
||||
return transport.httpTransport.RoundTrip(request)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package kubernetes
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -13,14 +14,16 @@ import (
|
||||
|
||||
type (
|
||||
localTransport struct {
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
|
||||
agentTransport struct {
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
signatureService portainer.DigitalSignatureService
|
||||
httpTransport *http.Transport
|
||||
tokenManager *tokenManager
|
||||
signatureService portainer.DigitalSignatureService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
|
||||
edgeTransport struct {
|
||||
@@ -50,21 +53,11 @@ 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) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||
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)
|
||||
@@ -85,21 +78,11 @@ 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) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||
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)
|
||||
@@ -127,21 +110,11 @@ 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) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier)
|
||||
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)
|
||||
@@ -154,3 +127,27 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
@@ -21,6 +22,7 @@ type (
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
k8sClientFactory *cli.ClientFactory
|
||||
}
|
||||
)
|
||||
|
||||
@@ -29,6 +31,7 @@ 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),
|
||||
}
|
||||
}
|
||||
@@ -41,13 +44,19 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
||||
manager.endpointProxies.Set(fmt.Sprint(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// 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(string(endpoint.ID))
|
||||
proxy, ok := manager.endpointProxies.Get(fmt.Sprint(endpoint.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -56,8 +65,11 @@ 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(string(endpoint.ID))
|
||||
manager.endpointProxies.Remove(fmt.Sprint(endpoint.ID))
|
||||
manager.k8sClientFactory.RemoveKubeClient(endpoint)
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||
|
||||
@@ -39,39 +39,41 @@ 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
|
||||
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
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
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 := kubernetes.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
kubernetesTokenCacheManager := server.KubernetesTokenCacheManager
|
||||
|
||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||
|
||||
@@ -82,7 +84,7 @@ func (server *Server) Start() error {
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.ProxyManager = proxyManager
|
||||
authHandler.ProxyManager = server.ProxyManager
|
||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||
authHandler.OAuthService = server.OAuthService
|
||||
|
||||
@@ -116,10 +118,10 @@ func (server *Server) Start() error {
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.ProxyManager = server.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
|
||||
@@ -131,7 +133,7 @@ func (server *Server) Start() error {
|
||||
|
||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.DataStore = server.DataStore
|
||||
endpointProxyHandler.ProxyManager = proxyManager
|
||||
endpointProxyHandler.ProxyManager = server.ProxyManager
|
||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
@@ -141,7 +143,7 @@ func (server *Server) Start() error {
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
registryHandler.DataStore = server.DataStore
|
||||
registryHandler.FileService = server.FileService
|
||||
registryHandler.ProxyManager = proxyManager
|
||||
registryHandler.ProxyManager = server.ProxyManager
|
||||
|
||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
@@ -6,6 +6,21 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
|
||||
// identifier and type parameters.
|
||||
func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl {
|
||||
return &portainer.ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: []portainer.UserResourceAccess{},
|
||||
TeamAccesses: []portainer.TeamResourceAccess{},
|
||||
AdministratorsOnly: true,
|
||||
Public: false,
|
||||
System: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
|
||||
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
|
||||
func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl {
|
||||
|
||||
@@ -40,6 +40,11 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the cached kube client so a new one can be created
|
||||
func (factory *ClientFactory) RemoveKubeClient(endpoint *portainer.Endpoint) {
|
||||
factory.endpointClients.Remove(strconv.Itoa(int(endpoint.ID)))
|
||||
}
|
||||
|
||||
// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found.
|
||||
// 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) {
|
||||
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
"github.com/portainer/libcompose/lookup"
|
||||
"github.com/portainer/libcompose/project"
|
||||
"github.com/portainer/libcompose/project/options"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerClientVersion = "1.24"
|
||||
dockerClientVersion = "1.24"
|
||||
composeSyntaxMaxVersion = "2"
|
||||
)
|
||||
|
||||
// ComposeStackManager represents a service for managing compose stacks.
|
||||
@@ -58,6 +59,11 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
|
||||
return client.NewDefaultFactory(clientOpts)
|
||||
}
|
||||
|
||||
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||
func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||
return composeSyntaxMaxVersion
|
||||
}
|
||||
|
||||
// Up will deploy a compose stack (equivalent of docker-compose up)
|
||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
|
||||
|
||||
101
api/portainer.go
101
api/portainer.go
@@ -190,24 +190,26 @@ type (
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type EndpointType `json:"Type"`
|
||||
URL string `json:"URL"`
|
||||
GroupID EndpointGroupID `json:"GroupId"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
Extensions []EndpointExtension `json:"Extensions"`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||
TagIDs []TagID `json:"TagIds"`
|
||||
Status EndpointStatus `json:"Status"`
|
||||
Snapshots []DockerSnapshot `json:"Snapshots"`
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
EdgeID string `json:"EdgeID,omitempty"`
|
||||
EdgeKey string `json:"EdgeKey"`
|
||||
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
|
||||
Kubernetes KubernetesData `json:"Kubernetes"`
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type EndpointType `json:"Type"`
|
||||
URL string `json:"URL"`
|
||||
GroupID EndpointGroupID `json:"GroupId"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
Extensions []EndpointExtension `json:"Extensions"`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||
TagIDs []TagID `json:"TagIds"`
|
||||
Status EndpointStatus `json:"Status"`
|
||||
Snapshots []DockerSnapshot `json:"Snapshots"`
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
EdgeID string `json:"EdgeID,omitempty"`
|
||||
EdgeKey string `json:"EdgeKey"`
|
||||
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
|
||||
Kubernetes KubernetesData `json:"Kubernetes"`
|
||||
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
|
||||
SecuritySettings EndpointSecuritySettings
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
@@ -271,6 +273,18 @@ 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
|
||||
|
||||
@@ -515,29 +529,31 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
|
||||
// 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
|
||||
@@ -554,6 +570,10 @@ type (
|
||||
Env []Pair `json:"Env"`
|
||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||
Status StackStatus `json:"Status"`
|
||||
CreationDate int64
|
||||
CreatedBy string
|
||||
UpdateDate int64
|
||||
UpdatedBy string
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
@@ -774,6 +794,7 @@ type (
|
||||
|
||||
// ComposeStackManager represents a service to manage Compose stacks
|
||||
ComposeStackManager interface {
|
||||
ComposeSyntaxMaxVersion() string
|
||||
Up(stack *Stack, endpoint *Endpoint) error
|
||||
Down(stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
@@ -1119,9 +1140,11 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.0.0"
|
||||
APIVersion = "2.1.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 25
|
||||
DBVersion = 26
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
ComposeSyntaxMaxVersion = "3.9"
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
@@ -178,6 +178,11 @@ 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;
|
||||
}
|
||||
@@ -620,57 +625,6 @@ 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;
|
||||
@@ -922,6 +876,27 @@ 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;
|
||||
@@ -1037,3 +1012,7 @@ json-tree .branch-preview {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
/* !spinkit override */
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -581,6 +581,16 @@ 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);
|
||||
@@ -630,5 +640,6 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
||||
$stateRegistryProvider.register(volume);
|
||||
$stateRegistryProvider.register(volumeBrowse);
|
||||
$stateRegistryProvider.register(volumeCreation);
|
||||
$stateRegistryProvider.register(dockerFeaturesConfiguration);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -4,100 +4,8 @@
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
<div class="settings">
|
||||
<span
|
||||
class="setting"
|
||||
ng-class="{ 'setting-active': $ctrl.columnVisibility.state.open }"
|
||||
uib-dropdown
|
||||
dropdown-append-to-body
|
||||
auto-close="disabled"
|
||||
is-open="$ctrl.columnVisibility.state.open"
|
||||
>
|
||||
<span uib-dropdown-toggle><i class="fa fa-columns space-right" aria-hidden="true"></i>Columns</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader">
|
||||
Show / Hide Columns
|
||||
</div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_state"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.state.display"
|
||||
/>
|
||||
<label for="col_vis_state" ng-bind="$ctrl.columnVisibility.columns.state.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_actions"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.actions.display"
|
||||
/>
|
||||
<label for="col_vis_actions" ng-bind="$ctrl.columnVisibility.columns.actions.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_stack"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.stack.display"
|
||||
/>
|
||||
<label for="col_vis_stack" ng-bind="$ctrl.columnVisibility.columns.stack.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_image"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.image.display"
|
||||
/>
|
||||
<label for="col_vis_image" ng-bind="$ctrl.columnVisibility.columns.image.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_created"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.created.display"
|
||||
/>
|
||||
<label for="col_vis_created" ng-bind="$ctrl.columnVisibility.columns.created.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.showHostColumn">
|
||||
<input
|
||||
id="col_vis_host"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.host.display"
|
||||
/>
|
||||
<label for="col_vis_host" ng-bind="$ctrl.columnVisibility.columns.host.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_ports"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.ports.display"
|
||||
/>
|
||||
<label for="col_vis_ports" ng-bind="$ctrl.columnVisibility.columns.ports.label"></label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="col_vis_ownership"
|
||||
ng-change="$ctrl.onColumnVisibilityChange($ctrl.columnVisibility)"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.columnVisibility.columns.ownership.display"
|
||||
/>
|
||||
<label for="col_vis_ownership" ng-bind="$ctrl.columnVisibility.columns.ownership.label"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.columnVisibility.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
||||
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
|
||||
@@ -36,9 +36,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
};
|
||||
|
||||
this.columnVisibility = {
|
||||
state: {
|
||||
open: false,
|
||||
},
|
||||
columns: {
|
||||
state: {
|
||||
label: 'State',
|
||||
@@ -75,9 +72,11 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
},
|
||||
};
|
||||
|
||||
this.onColumnVisibilityChange = function (columnVisibility) {
|
||||
DatatableService.setColumnVisibilitySettings(this.tableKey, columnVisibility);
|
||||
};
|
||||
this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
|
||||
function onColumnVisibilityChange(columns) {
|
||||
this.columnVisibility.columns = columns;
|
||||
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||
}
|
||||
|
||||
this.onSelectionChanged = function () {
|
||||
this.updateSelectionState();
|
||||
@@ -199,7 +198,6 @@ angular.module('portainer.docker').controller('ContainersDatatableController', [
|
||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||
if (storedColumnVisibility !== null) {
|
||||
this.columnVisibility = storedColumnVisibility;
|
||||
this.columnVisibility.state.open = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -66,6 +66,13 @@ 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();
|
||||
|
||||
@@ -37,7 +37,15 @@
|
||||
</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>
|
||||
|
||||
@@ -36,10 +36,13 @@
|
||||
<!-- don't use registry -->
|
||||
<div ng-if="!$ctrl.model.UseRegistry">
|
||||
<div class="form-group">
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left"
|
||||
>Image
|
||||
<portainer-tooltip position="bottom" message="Image and repository should be publicly available."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-left: 15px;">
|
||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When using advanced mode, image and repository <b>must be</b> publicly available.
|
||||
</p>
|
||||
</span>
|
||||
<label for="image_name" ng-class="$ctrl.labelClass" class="control-label text-left">Image </label>
|
||||
<div ng-class="$ctrl.inputClass">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Image" name="image_name" placeholder="e.g. registry:port/myImage:myTag" required />
|
||||
</div>
|
||||
|
||||
@@ -7,5 +7,6 @@ angular.module('portainer.docker').component('logViewer', {
|
||||
logCollectionChange: '<',
|
||||
sinceTimestamp: '=',
|
||||
lineCount: '=',
|
||||
resourceName: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
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()"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker').controller('LogViewerController', [
|
||||
'clipboard',
|
||||
function (clipboard) {
|
||||
'Blob',
|
||||
'FileSaver',
|
||||
function (clipboard, Blob, FileSaver) {
|
||||
this.state = {
|
||||
availableSinceDatetime: [
|
||||
{ desc: 'Last day', value: moment().subtract(1, 'days').format() },
|
||||
@@ -43,5 +46,10 @@ 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');
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(
|
||||
atob(str)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
try {
|
||||
return decodeURIComponent(
|
||||
atob(str)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
} catch (err) {
|
||||
return atob(str);
|
||||
}
|
||||
}
|
||||
|
||||
export function ConfigViewModel(data) {
|
||||
|
||||
@@ -4,7 +4,6 @@ export function ImageViewModel(data) {
|
||||
this.Repository = data.Repository;
|
||||
this.Created = data.Created;
|
||||
this.Checked = false;
|
||||
|
||||
this.RepoTags = data.RepoTags;
|
||||
if (!this.RepoTags && data.RepoDigests) {
|
||||
this.RepoTags = [];
|
||||
@@ -21,6 +20,7 @@ export function ImageViewModel(data) {
|
||||
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
|
||||
this.NodeName = data.Portainer.Agent.NodeName;
|
||||
}
|
||||
this.Labels = data.Labels;
|
||||
}
|
||||
|
||||
export function ImageBuildModel(data) {
|
||||
|
||||
@@ -16,4 +16,5 @@ export function ImageDetailsViewModel(data) {
|
||||
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
|
||||
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
|
||||
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
|
||||
this.Labels = data.ContainerConfig.Labels;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ angular.module('portainer.docker').factory('Container', [
|
||||
params: { all: 0, action: 'json', filters: '@filters' },
|
||||
isArray: true,
|
||||
interceptor: ContainersInterceptor,
|
||||
timeout: 15000,
|
||||
},
|
||||
get: {
|
||||
method: 'GET',
|
||||
@@ -48,20 +47,17 @@ 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: {
|
||||
|
||||
@@ -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, timeout: 15000 },
|
||||
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor },
|
||||
get: { method: 'GET', params: { action: 'json' } },
|
||||
search: { method: 'GET', params: { action: 'search' } },
|
||||
history: { method: 'GET', params: { action: 'history' }, isArray: true },
|
||||
|
||||
@@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
|
||||
method: 'GET',
|
||||
isArray: true,
|
||||
interceptor: NetworksInterceptor,
|
||||
timeout: 15000,
|
||||
},
|
||||
get: {
|
||||
method: 'GET',
|
||||
|
||||
@@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
|
||||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
||||
@@ -18,10 +18,9 @@ angular.module('portainer.docker').factory('System', [
|
||||
info: {
|
||||
method: 'GET',
|
||||
params: { action: 'info' },
|
||||
timeout: 15000,
|
||||
interceptor: InfoInterceptor,
|
||||
},
|
||||
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
||||
version: { method: 'GET', params: { action: 'version' }, interceptor: VersionInterceptor },
|
||||
events: {
|
||||
method: 'GET',
|
||||
params: { action: 'events', since: '@since', until: '@until' },
|
||||
|
||||
@@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
|
||||
logs: {
|
||||
method: 'GET',
|
||||
params: { id: '@id', action: 'logs' },
|
||||
timeout: 4500,
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: logsHandler,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ angular.module('portainer.docker').factory('Volume', [
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
},
|
||||
{
|
||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
|
||||
query: { method: 'GET', interceptor: VolumesInterceptor },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
create: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -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
|
||||
HttpRequestHelper,
|
||||
endpoint
|
||||
) {
|
||||
$scope.create = create;
|
||||
|
||||
@@ -709,14 +709,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
||||
|
||||
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
||||
$scope.availableLoggingDrivers = loggingDrivers;
|
||||
@@ -933,15 +927,11 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
}
|
||||
|
||||
async function shouldShowDevices() {
|
||||
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
async function checkIfContainerCapabilitiesEnabled() {
|
||||
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin();
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -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">
|
||||
<table class="table container-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
|
||||
@@ -22,6 +22,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
'HttpRequestHelper',
|
||||
'Authentication',
|
||||
'StateManager',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -41,7 +42,8 @@ angular.module('portainer.docker').controller('ContainerController', [
|
||||
ImageService,
|
||||
HttpRequestHelper,
|
||||
Authentication,
|
||||
StateManager
|
||||
StateManager,
|
||||
endpoint
|
||||
) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
@@ -97,14 +99,13 @@ 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,
|
||||
} = appState.application;
|
||||
} = endpoint.SecuritySettings;
|
||||
|
||||
const settingRestrictsRegularUsers =
|
||||
!allowContainerCapabilitiesForRegularUsers ||
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="container.Name"
|
||||
></log-viewer>
|
||||
|
||||
@@ -17,6 +17,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
'EndpointProvider',
|
||||
'StateManager',
|
||||
'TagService',
|
||||
'endpoint',
|
||||
function (
|
||||
$scope,
|
||||
$q,
|
||||
@@ -32,7 +33,8 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
Notifications,
|
||||
EndpointProvider,
|
||||
StateManager,
|
||||
TagService
|
||||
TagService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.dismissInformationPanel = function (id) {
|
||||
StateManager.dismissInformationPanel(id);
|
||||
@@ -89,9 +91,8 @@ angular.module('portainer.docker').controller('DashboardController', [
|
||||
|
||||
async function shouldShowStacks() {
|
||||
const isAdmin = Authentication.isAdmin();
|
||||
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
return isAdmin || allowStackManagementForRegularUsers;
|
||||
return isAdmin || endpoint.SecuritySettings.allowStackManagementForRegularUsers;
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<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>
|
||||
11
app/docker/views/docker-features-configuration/index.js
Normal file
11
app/docker/views/docker-features-configuration/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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: '<',
|
||||
},
|
||||
});
|
||||
@@ -29,7 +29,7 @@ angular.module('portainer.docker').controller('HostViewController', [
|
||||
ctrl.state.isAdmin = Authentication.isAdmin();
|
||||
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
||||
ctrl.state.agentApiVersion = agentApiVersion;
|
||||
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
||||
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
|
||||
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
angular.module('portainer.docker').component('hostView', {
|
||||
templateUrl: './host-view.html',
|
||||
controller: 'HostViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,6 +128,17 @@
|
||||
<td>Build</td>
|
||||
<td>Docker {{ image.DockerVersion }} on {{ image.Os }}, {{ image.Architecture }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!(image.Labels | emptyobject)">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="(k, v) in image.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="image.Author">
|
||||
<td>Author</td>
|
||||
<td>{{ image.Author }}</td>
|
||||
|
||||
@@ -196,7 +196,8 @@
|
||||
<div class="form-group" ng-hide="config.Driver === 'macvlan' && formValues.Macvlan.Scope === 'local'">
|
||||
<div class="col-sm-12">
|
||||
<label for="ownership" class="control-label text-left">
|
||||
Restrict external access to the network
|
||||
Isolated network
|
||||
<portainer-tooltip position="bottom" message="An isolated network has no inbound or outbound communications."></portainer-tooltip>
|
||||
</label>
|
||||
<label name="ownership" class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="config.Internal" />
|
||||
|
||||
@@ -20,7 +20,7 @@ angular.module('portainer.docker').controller('NodeDetailsViewController', [
|
||||
var applicationState = StateManager.getState();
|
||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||
ctrl.state.isAdmin = Authentication.isAdmin();
|
||||
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
||||
ctrl.state.enableHostManagementFeatures = ctrl.endpoint.SecuritySettings.enableHostManagementFeatures;
|
||||
|
||||
var fetchJobs = ctrl.state.isAdmin && ctrl.state.isAgent;
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
angular.module('portainer.docker').component('nodeDetailsView', {
|
||||
templateUrl: './node-details-view.html',
|
||||
controller: 'NodeDetailsViewController',
|
||||
bindings: {
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,9 +30,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
'RegistryService',
|
||||
'HttpRequestHelper',
|
||||
'NodeService',
|
||||
'SettingsService',
|
||||
'WebhookService',
|
||||
'EndpointProvider',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -56,9 +56,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
RegistryService,
|
||||
HttpRequestHelper,
|
||||
NodeService,
|
||||
SettingsService,
|
||||
WebhookService,
|
||||
EndpointProvider
|
||||
EndpointProvider,
|
||||
endpoint
|
||||
) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
@@ -520,6 +520,12 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.volumesAreValid = volumesAreValid;
|
||||
function volumesAreValid() {
|
||||
const volumes = $scope.formValues.Volumes;
|
||||
return volumes.every((volume) => volume.Target && volume.Source);
|
||||
}
|
||||
|
||||
$scope.create = function createService() {
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
|
||||
@@ -587,10 +593,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
||||
async function checkIfAllowedBindMounts() {
|
||||
const isAdmin = Authentication.isAdmin();
|
||||
|
||||
const settings = await SettingsService.publicSettings();
|
||||
const { AllowBindMountsForRegularUsers } = settings;
|
||||
const { allowBindMountsForRegularUsers } = endpoint.SecuritySettings;
|
||||
|
||||
return isAdmin || AllowBindMountsForRegularUsers;
|
||||
return isAdmin || allowBindMountsForRegularUsers;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image"
|
||||
ng-disabled="state.actionInProgress || !formValues.RegistryModel.Image || !volumesAreValid()"
|
||||
ng-click="create()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
@@ -298,16 +298,19 @@
|
||||
<!-- volume-line1 -->
|
||||
<div class="col-sm-12 form-inline">
|
||||
<!-- container-path -->
|
||||
<div class="input-group input-group-sm col-sm-6">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
<div class="input-group col-sm-6">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</div>
|
||||
<!-- !container-path -->
|
||||
<!-- volume-type -->
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.Source = null">Volume</label>
|
||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Source = null">Bind</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
@@ -318,27 +321,35 @@
|
||||
<!-- !volume-line1 -->
|
||||
<!-- volume-line2 -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
<div style="height: 30px; display: inline-block; vertical-align: top; display: inline-flex; align-items: center;">
|
||||
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
<!-- volume -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
</select>
|
||||
<div class="col-sm-6 input-group" ng-if="volume.Type === 'volume'" style="float: none; padding: 0;">
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="volume.Source"
|
||||
ng-options="vol as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
>
|
||||
<option selected disabled value="">Select a volume</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !volume -->
|
||||
<!-- bind -->
|
||||
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
<div class="input-group input-group-sm w-full">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host" />
|
||||
</div>
|
||||
<div class="small text-warning" ng-show="!volume.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</div>
|
||||
<!-- !bind -->
|
||||
<!-- read-only -->
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
|
||||
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px; vertical-align: top;">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
|
||||
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted"> By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers. </div>
|
||||
<div class="col-sm-12 small text-muted">
|
||||
By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers (Linux) or
|
||||
<code>C:\ProgramData\Docker\secrets\$SECRET_NAME</code> (Windows).</div
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
|
||||
@@ -24,7 +24,14 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="mount in service.ServiceMounts">
|
||||
<td ng-if="isAdmin || allowBindMounts">
|
||||
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating" disable-authorization="DockerServiceUpdate">
|
||||
<select
|
||||
name="mountType"
|
||||
class="form-control"
|
||||
ng-model="mount.Type"
|
||||
ng-change="updateMount(service, mount)"
|
||||
ng-disabled="isUpdating"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
>
|
||||
<option value="volume">Volume</option>
|
||||
<option value="bind">Bind</option>
|
||||
</select>
|
||||
@@ -35,6 +42,7 @@
|
||||
ng-model="mount.Source"
|
||||
ng-options="vol.Id as ((vol.Id|truncate:30) + ' - ' + (vol.Driver|truncate:30)) for vol in availableVolumes"
|
||||
ng-if="mount.Type === 'volume'"
|
||||
ng-change="updateMount(service, mount)"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
@@ -42,12 +50,14 @@
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name=""
|
||||
ng-model="mount.Source"
|
||||
placeholder="e.g. /tmp/portainer/data"
|
||||
ng-change="updateMount(service, mount)"
|
||||
ng-disabled="isUpdating || (!isAdmin && !allowBindMounts && mount.Type === 'bind')"
|
||||
ng-if="mount.Type === 'bind'"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Source"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Source is required. </div>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
@@ -59,6 +69,7 @@
|
||||
ng-disabled="isUpdating"
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
/>
|
||||
<div class="col-sm-12 small text-warning" ng-show="!mount.Target"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target is required. </div>
|
||||
</td>
|
||||
<td authorization="DockerServiceUpdate">
|
||||
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating" />
|
||||
@@ -77,7 +88,9 @@
|
||||
<rd-widget-footer authorization="DockerServiceUpdate">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!mountsAreValid() || !hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">
|
||||
Apply changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
||||
@@ -34,7 +34,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'SecretService',
|
||||
'ImageService',
|
||||
'SecretHelper',
|
||||
'Service',
|
||||
'ServiceHelper',
|
||||
'LabelHelper',
|
||||
'TaskService',
|
||||
@@ -45,7 +44,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'ModalService',
|
||||
'PluginService',
|
||||
'Authentication',
|
||||
'SettingsService',
|
||||
'VolumeService',
|
||||
'ImageHelper',
|
||||
'WebhookService',
|
||||
@@ -53,6 +51,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
'clipboard',
|
||||
'WebhookHelper',
|
||||
'NetworkService',
|
||||
'endpoint',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -67,7 +66,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
SecretService,
|
||||
ImageService,
|
||||
SecretHelper,
|
||||
Service,
|
||||
ServiceHelper,
|
||||
LabelHelper,
|
||||
TaskService,
|
||||
@@ -78,14 +76,14 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
ModalService,
|
||||
PluginService,
|
||||
Authentication,
|
||||
SettingsService,
|
||||
VolumeService,
|
||||
ImageHelper,
|
||||
WebhookService,
|
||||
EndpointProvider,
|
||||
clipboard,
|
||||
WebhookHelper,
|
||||
NetworkService
|
||||
NetworkService,
|
||||
endpoint
|
||||
) {
|
||||
$scope.state = {
|
||||
updateInProgress: false,
|
||||
@@ -378,6 +376,12 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.mountsAreValid = mountsAreValid;
|
||||
function mountsAreValid() {
|
||||
const mounts = $scope.service.ServiceMounts;
|
||||
return mounts.every((mount) => mount.Source && mount.Target);
|
||||
}
|
||||
|
||||
function buildChanges(service) {
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.Name;
|
||||
@@ -660,7 +664,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
availableImages: ImageService.images(),
|
||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
||||
availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25),
|
||||
settings: SettingsService.publicSettings(),
|
||||
webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()),
|
||||
});
|
||||
})
|
||||
@@ -671,7 +674,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
|
||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
||||
$scope.allowBindMounts = endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.availableNetworks = data.availableNetworks;
|
||||
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="service.Name"
|
||||
></log-viewer>
|
||||
|
||||
@@ -13,12 +13,13 @@ angular.module('portainer.docker').controller('ServicesController', [
|
||||
function getServices() {
|
||||
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
||||
|
||||
$q.all({
|
||||
services: ServiceService.services(),
|
||||
tasks: TaskService.tasks(),
|
||||
containers: agentProxy ? ContainerService.containers(1) : [],
|
||||
nodes: NodeService.nodes(),
|
||||
})
|
||||
return $q
|
||||
.all({
|
||||
services: ServiceService.services(),
|
||||
tasks: TaskService.tasks(),
|
||||
containers: agentProxy ? ContainerService.containers(1) : [],
|
||||
nodes: NodeService.nodes(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var services = data.services;
|
||||
var tasks = data.tasks;
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
display-timestamps="state.displayTimestamps"
|
||||
line-count="state.lineCount"
|
||||
since-timestamp="state.sinceTimestamp"
|
||||
resource-name="task.Id"
|
||||
></log-viewer>
|
||||
|
||||
@@ -51,14 +51,18 @@ angular.module('portainer.docker').controller('VolumeController', [
|
||||
};
|
||||
|
||||
$scope.removeVolume = function removeVolume() {
|
||||
VolumeService.remove($scope.volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', $transition$.params().id);
|
||||
$state.go('docker.volumes', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
});
|
||||
ModalService.confirmDeletion('Do you want to remove this volume?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
VolumeService.remove($scope.volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', $transition$.params().id);
|
||||
$state.go('docker.volumes', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getVolumeDataFromContainer(container, volumeId) {
|
||||
|
||||
@@ -9,26 +9,32 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||
'HttpRequestHelper',
|
||||
'EndpointProvider',
|
||||
'Authentication',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication) {
|
||||
'ModalService',
|
||||
'endpoint',
|
||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider, Authentication, ModalService, endpoint) {
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (volume) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
|
||||
VolumeService.remove(volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', volume.Id);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (volume) {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
|
||||
VolumeService.remove(volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully removed', volume.Id);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -70,8 +76,7 @@ angular.module('portainer.docker').controller('VolumesController', [
|
||||
function initView() {
|
||||
getVolumes();
|
||||
|
||||
$scope.showBrowseAction =
|
||||
$scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || $scope.applicationState.application.enableVolumeBrowserForNonAdminUsers);
|
||||
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy && (Authentication.isAdmin() || endpoint.SecuritySettings.allowVolumeBrowserForRegularUsers);
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html
|
||||
><html lang="en" ng-app="<%= name %>">
|
||||
><html lang="en" ng-app="<%= name %>" ng-strict-di>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Portainer</title>
|
||||
|
||||
@@ -12,9 +12,9 @@ angular.module('portainer.integrations.storidge').factory('Storidge', [
|
||||
{
|
||||
rebootCluster: { method: 'POST', params: { resource: 'clusters', action: 'reboot' } },
|
||||
shutdownCluster: { method: 'POST', params: { resource: 'clusters', action: 'shutdown' } },
|
||||
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
|
||||
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, ignoreLoadingBar: true, isArray: true },
|
||||
getVersion: { method: 'GET', params: { resource: 'clusters', action: 'version' } },
|
||||
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, timeout: 4500, ignoreLoadingBar: true },
|
||||
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, ignoreLoadingBar: true },
|
||||
|
||||
queryNodes: { method: 'GET', params: { resource: 'nodes' } },
|
||||
getNode: { method: 'GET', params: { resource: 'nodes', id: '@id' } },
|
||||
|
||||
@@ -14,7 +14,6 @@ angular.module('portainer.kubernetes').factory('KubernetesComponentStatus', [
|
||||
{
|
||||
get: {
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
ignoreLoadingBar: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th ng-if="!$ctrl.isPod">
|
||||
<a ng-click="$ctrl.changeOrderBy('PodName')">
|
||||
Pod
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PodName' && !$ctrl.state.reverseOrder"></i>
|
||||
@@ -78,6 +78,13 @@
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('ImagePullPolicy')">
|
||||
Image Pull Policy
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ImagePullPolicy' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
Status
|
||||
@@ -107,9 +114,10 @@
|
||||
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||
pagination-id="$ctrl.tableKey"
|
||||
>
|
||||
<td>{{ item.PodName }}</td>
|
||||
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.Image }}</td>
|
||||
<td>{{ item.ImagePullPolicy }}</td>
|
||||
<td
|
||||
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
|
||||
>
|
||||
@@ -130,10 +138,10 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="6" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No pod available.</td>
|
||||
<td colspan="7" class="text-center text-muted">No pod available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -8,5 +8,6 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
isPod: '<',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -151,11 +151,14 @@
|
||||
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
|
||||
>
|
||||
<td>{{ item.ApplicationType | kubernetesApplicationTypeText }}</td>
|
||||
<td>
|
||||
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">
|
||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
|
||||
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
|
||||
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></td
|
||||
>
|
||||
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code>
|
||||
</td>
|
||||
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.POD">
|
||||
{{ item.Pods[0].Status }}
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.PublishedPorts.length">
|
||||
<span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
||||
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
|
||||
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
|
||||
@@ -42,6 +42,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||
this.$onInit = function () {
|
||||
this.isAdmin = Authentication.isAdmin();
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th style="width: 55px;">
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
@@ -142,8 +142,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||
<td colspan="5">
|
||||
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
|
||||
<td></td>
|
||||
<td colspan="4">
|
||||
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })">{{ app.Name }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
|
||||
>external</span
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user