Compare commits
78 Commits
fix/EE-632
...
refactor/E
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ab739adfd | ||
|
|
0ee6c5c6e9 | ||
|
|
1e2dbd7778 | ||
|
|
92dd6ed7bc | ||
|
|
f780207b82 | ||
|
|
86a848d927 | ||
|
|
ca4130b221 | ||
|
|
bb7c6077d5 | ||
|
|
bccab06abb | ||
|
|
531f88b947 | ||
|
|
2953848b9a | ||
|
|
c0ba221021 | ||
|
|
be85d34c4b | ||
|
|
7125ef81f3 | ||
|
|
1aae2e27f4 | ||
|
|
3237e1990c | ||
|
|
1e61f7e305 | ||
|
|
5586910e9d | ||
|
|
bb646162d1 | ||
|
|
cfe0d3092d | ||
|
|
6fde4195f8 | ||
|
|
36b8c849b3 | ||
|
|
0f6607e703 | ||
|
|
23295d2736 | ||
|
|
6290e9facc | ||
|
|
95424c322d | ||
|
|
a1e610a39a | ||
|
|
a27cc6c0e5 | ||
|
|
2b4cb1b7b4 | ||
|
|
26074437ca | ||
|
|
665a25e448 | ||
|
|
4a91e947ed | ||
|
|
d514eeec86 | ||
|
|
0ef4aad79a | ||
|
|
8355d449c5 | ||
|
|
fd7e8a629e | ||
|
|
7757bf7a84 | ||
|
|
5862aa5dd8 | ||
|
|
925a0d0a9a | ||
|
|
2a7a96f498 | ||
|
|
c472fe9c18 | ||
|
|
0eaf296e1b | ||
|
|
598b8d0f28 | ||
|
|
e1a3010bc7 | ||
|
|
2de4863532 | ||
|
|
8cf54cd0df | ||
|
|
1ef1953d7d | ||
|
|
5b033abaa4 | ||
|
|
5865f1ca77 | ||
|
|
f59573f306 | ||
|
|
1cecbd7177 | ||
|
|
acf9203580 | ||
|
|
9845518aa9 | ||
|
|
d7e83aad26 | ||
|
|
df47f3d8a8 | ||
|
|
d0ecf6c16b | ||
|
|
e400c4dfc6 | ||
|
|
721457b71d | ||
|
|
b19800681f | ||
|
|
6a4e44ee0a | ||
|
|
37ece734f0 | ||
|
|
bf79ef7d89 | ||
|
|
883ef2578f | ||
|
|
a585f34106 | ||
|
|
b128139b69 | ||
|
|
4c425a7af8 | ||
|
|
400d95c1a5 | ||
|
|
ca617e2ac9 | ||
|
|
4a90b8a3f7 | ||
|
|
43ad3face2 | ||
|
|
69e61be474 | ||
|
|
a4ea7a3709 | ||
|
|
c5ecf8a66d | ||
|
|
c2c0631495 | ||
|
|
4ff3cee72e | ||
|
|
c4e8251e52 | ||
|
|
21b00c267d | ||
|
|
86ec058347 |
11
.github/DISCUSSION_TEMPLATE/help.yaml
vendored
Normal file
11
.github/DISCUSSION_TEMPLATE/help.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before asking a question, make sure it hasn't been already asked and answered. You can search our [discussions](https://github.com/orgs/portainer/discussions) and [bug reports](https://github.com/portainer/portainer/issues) in GitHub. Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io/) first.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Ask a Question!
|
||||
validations:
|
||||
required: true
|
||||
38
.github/DISCUSSION_TEMPLATE/ideas.yaml
vendored
Normal file
38
.github/DISCUSSION_TEMPLATE/ideas.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Welcome!
|
||||
|
||||
Thanks for suggesting an idea for Portainer!
|
||||
|
||||
Before opening a new idea or feature request, make sure that we do not have any duplicates already open. You can ensure this by [searching this discussion cagetory](https://github.com/orgs/portainer/discussions/categories/ideas). If there is a duplicate, please add a comment to the existing idea instead.
|
||||
|
||||
Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io) as they may point you toward a solution.
|
||||
|
||||
**DO NOT FILE DUPLICATE REQUESTS.**
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe
|
||||
description: Short list of what the feature request aims to address.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
54
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
54
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
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 https://portainer.io/slack/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Bug description**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Technical details:**
|
||||
|
||||
- Portainer version:
|
||||
- Docker version (managed by Portainer):
|
||||
- Kubernetes version (managed by Portainer):
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
|
||||
- Browser:
|
||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
|
||||
- Have you reviewed our technical documentation and knowledge base? Yes/No
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
164
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
164
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve.
|
||||
labels: kind/bug,bug/need-confirmation
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Welcome!
|
||||
|
||||
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
|
||||
|
||||
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
|
||||
|
||||
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Before you start please confirm the following.
|
||||
options:
|
||||
- label: Yes, I've searched similar issues on [GitHub](https://github.com/portainer/portainer/issues).
|
||||
required: true
|
||||
- label: Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io) or [knowledge base](https://portal.portainer.io/knowledge).
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# About your issue
|
||||
|
||||
Tell us a bit about the issue you're having.
|
||||
|
||||
How to write a good bug report:
|
||||
|
||||
- Respect the issue template as much as possible.
|
||||
- Summarize the issue so that we understand what is going wrong.
|
||||
- Describe what you would have expected to have happened, and what actually happened instead.
|
||||
- Provide easy to follow steps to reproduce the issue.
|
||||
- Remain clear and concise.
|
||||
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear and concise description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please be as detailed as possible when providing steps to reproduce.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Portainer logs or screenshots
|
||||
description: Provide Portainer container logs or any screenshots related to the issue.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# About your environment
|
||||
|
||||
Tell us a bit about your Portainer environment.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Portainer version
|
||||
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
|
||||
multiple: false
|
||||
options:
|
||||
- '2.18.4'
|
||||
- '2.18.3'
|
||||
- '2.18.2'
|
||||
- '2.18.1'
|
||||
- '2.17.1'
|
||||
- '2.17.0'
|
||||
- '2.16.2'
|
||||
- '2.16.1'
|
||||
- '2.16.0'
|
||||
- '2.15.1'
|
||||
- '2.15.0'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Portainer Edition
|
||||
multiple: false
|
||||
options:
|
||||
- 'Business Edition (BE/EE) with 5NF / 3NF license'
|
||||
- 'Business Edition (BE/EE) with Home & Student license'
|
||||
- 'Business Edition (BE/EE) with Starter license'
|
||||
- 'Business Edition (BE/EE) with Professional or Enterprise license'
|
||||
- 'Community Edition (CE)'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Platform and Version
|
||||
description: |
|
||||
Enter your container management platform (Docker | Swarm | Kubernetes) along with the version.
|
||||
Example: Docker 24.0.3 | Docker Swarm 24.0.3 | Kubernetes 1.26
|
||||
You can find our supported platforms [in our documentation](https://docs.portainer.io/start/requirements-and-prerequisites).
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: OS and Architecture
|
||||
description: |
|
||||
Enter your Operating System, Version and Architecture. Example: Ubuntu 22.04, AMD64 | Raspbian OS, ARM64
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Browser
|
||||
description: |
|
||||
Enter your browser and version. Example: Google Chrome 114.0
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What command did you use to deploy Portainer?
|
||||
description: |
|
||||
Example: `docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest`
|
||||
If you deployed Portainer using a compose file or manifest you can provide this here as well.
|
||||
render: bash
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any additional information about your environment, the bug, or anything else you think might be helpful.
|
||||
validations:
|
||||
required: false
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Portainer Business Edition - Get 3 nodes free
|
||||
url: https://www.portainer.io/take-3
|
||||
- name: Question
|
||||
url: https://github.com/orgs/portainer/discussions/new?category=help
|
||||
about: Ask us a question about Portainer usage or deployment.
|
||||
- name: Idea or Feature Request
|
||||
url: https://github.com/orgs/portainer/discussions/new?category=ideas
|
||||
about: Suggest an idea or feature/enhancement that should be added in Portainer.
|
||||
- name: Portainer Business Edition - Get 3 Nodes Free
|
||||
url: https://www.portainer.io/take-3
|
||||
about: Portainer Business Edition has more features, more support and you can now get 3 nodes free for as long as you want.
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -41,6 +41,6 @@ jobs:
|
||||
- name: GolangCI-Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.54.1
|
||||
version: v1.52.2
|
||||
working-directory: api
|
||||
args: --timeout=10m -c .golangci.yaml
|
||||
|
||||
5
.github/workflows/stale.yml
vendored
5
.github/workflows/stale.yml
vendored
@@ -1,7 +1,8 @@
|
||||
name: Close Stale Issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * *'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -9,7 +10,7 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v4.0.0
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn jest --maxWorkers=2
|
||||
run: make test-client ARGS="--maxWorkers=2"
|
||||
test-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -65,7 +65,7 @@ clean: ## Remove all build and download artifacts
|
||||
test: test-server test-client ## Run all tests
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test
|
||||
yarn test $(ARGS)
|
||||
|
||||
test-server: ## Run server tests
|
||||
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
||||
|
||||
@@ -10,17 +10,17 @@ linters:
|
||||
- exportloopref
|
||||
linters-settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: 'github.com/sirupsen/logrus'
|
||||
desc: 'logging is allowed only by github.com/rs/zerolog'
|
||||
- pkg: 'golang.org/x/exp'
|
||||
desc: 'exp is not allowed'
|
||||
files:
|
||||
- '!**/*_test.go'
|
||||
- '!**/base.go'
|
||||
- '!**/base_tx.go'
|
||||
list-type: denylist
|
||||
include-go-root: true
|
||||
packages:
|
||||
- github.com/sirupsen/logrus
|
||||
- golang.org/x/exp
|
||||
packages-with-error-message:
|
||||
- github.com/sirupsen/logrus: 'logging is allowed only by github.com/rs/zerolog'
|
||||
ignore-file-rules:
|
||||
- '**/*_test.go'
|
||||
- '**/base.go'
|
||||
- '**/base_tx.go'
|
||||
|
||||
# errorlint is causing a typecheck error for some reason. The go compiler will report these
|
||||
# anyway, so ignore them from the linter
|
||||
|
||||
@@ -30,7 +30,6 @@ var filesToBackup = []string{
|
||||
"portainer.key",
|
||||
"portainer.pub",
|
||||
"tls",
|
||||
"chisel",
|
||||
}
|
||||
|
||||
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
|
||||
|
||||
@@ -75,11 +75,10 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("max_alive_minutes", maxAlive.Minutes()).
|
||||
Msg("KeepTunnelAlive: start")
|
||||
Msg("start")
|
||||
|
||||
maxAliveTicker := time.NewTicker(maxAlive)
|
||||
defer maxAliveTicker.Stop()
|
||||
|
||||
pingTicker := time.NewTicker(tunnelCleanupInterval)
|
||||
defer pingTicker.Stop()
|
||||
|
||||
@@ -92,13 +91,13 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("KeepTunnelAlive: ping agent")
|
||||
Msg("ping agent")
|
||||
}
|
||||
case <-maxAliveTicker.C:
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("timeout_minutes", maxAlive.Minutes()).
|
||||
Msg("KeepTunnelAlive: tunnel keep alive timeout")
|
||||
Msg("tunnel keep alive timeout")
|
||||
|
||||
return
|
||||
case <-ctx.Done():
|
||||
@@ -106,7 +105,7 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("KeepTunnelAlive: tunnel stop")
|
||||
Msg("tunnel stop")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -127,8 +126,8 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
|
||||
}
|
||||
|
||||
config := &chserver.Config{
|
||||
Reverse: true,
|
||||
PrivateKeyFile: privateKeyFile,
|
||||
Reverse: true,
|
||||
KeyFile: privateKeyFile,
|
||||
}
|
||||
|
||||
chiselServer, err := chserver.NewServer(config)
|
||||
|
||||
@@ -39,9 +39,9 @@ func setLoggingMode(mode string) {
|
||||
case "PRETTY":
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
NoColor: true,
|
||||
TimeFormat: "2006/01/02 03:04PM",
|
||||
FormatMessage: formatMessage})
|
||||
FormatMessage: formatMessage,
|
||||
})
|
||||
case "JSON":
|
||||
log.Logger = log.Output(os.Stderr)
|
||||
}
|
||||
@@ -51,5 +51,6 @@ func formatMessage(i interface{}) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s |", i)
|
||||
}
|
||||
|
||||
@@ -157,16 +157,6 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
return store
|
||||
}
|
||||
|
||||
// checkDBSchemaServerVersionMatch checks if the server version matches the db scehma version
|
||||
func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersion string, serverEdition int) bool {
|
||||
v, err := dbStore.Version().Version()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||
}
|
||||
|
||||
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||
if err != nil {
|
||||
@@ -398,11 +388,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// check if the db schema version matches with server version
|
||||
if !checkDBSchemaServerVersionMatch(dataStore, portainer.APIVersion, int(portainer.Edition)) {
|
||||
log.Fatal().Msg("The database schema version does not align with the server version. Please consider reverting to the previous server version or addressing the database migration issue.")
|
||||
}
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed getting instance id")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
@@ -145,23 +144,6 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
return endpoints, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
|
||||
for t := range e.TeamAccessPolicies {
|
||||
if t == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
var identifier int
|
||||
|
||||
@@ -122,23 +122,6 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
return endpoints, service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
|
||||
for t := range e.TeamAccessPolicies {
|
||||
if t == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
|
||||
@@ -89,7 +89,6 @@ type (
|
||||
EndpointService interface {
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
||||
EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error)
|
||||
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
||||
UpdateHeartbeat(endpointID portainer.EndpointID)
|
||||
Endpoints() ([]portainer.Endpoint, error)
|
||||
|
||||
@@ -51,9 +51,9 @@ func (store *Store) MigrateData() error {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
||||
restorErr := store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||
if restorErr != nil {
|
||||
return errors.Wrap(restorErr, "failed to restore database")
|
||||
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore database")
|
||||
}
|
||||
|
||||
log.Info().Msg("database restored to previous version")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
@@ -72,7 +72,7 @@ func dbVersionToSemanticVersion(dbVersion int) string {
|
||||
func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
|
||||
// Very old versions of portainer did not have a version bucket, lets set some defaults
|
||||
dbVersion := 24
|
||||
edition := int(portainer.PortainerCE)
|
||||
edition := int(portaineree.PortainerCE)
|
||||
instanceId := ""
|
||||
|
||||
// If we already have a version key, we don't need to migrate
|
||||
|
||||
@@ -944,6 +944,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -302,38 +302,6 @@ func (service *Service) UpdateStoreStackFileFromBytes(stackIdentifier, fileName
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// UpdateStoreStackFileFromBytesByVersion makes stack file backup and updates a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) UpdateStoreStackFileFromBytesByVersion(stackIdentifier, fileName string, version int, commitHash string, data []byte) (string, error) {
|
||||
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
|
||||
|
||||
versionStr := ""
|
||||
if version != 0 {
|
||||
versionStr = fmt.Sprintf("v%d", version)
|
||||
}
|
||||
if commitHash != "" {
|
||||
versionStr = commitHash
|
||||
}
|
||||
|
||||
if versionStr != "" {
|
||||
stackStorePath = JoinPaths(stackStorePath, versionStr)
|
||||
}
|
||||
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
err := service.createBackupFileInStore(composeFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// RemoveStackFileBackup removes the stack file backup in the ComposeStorePath.
|
||||
func (service *Service) RemoveStackFileBackup(stackIdentifier, fileName string) error {
|
||||
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
|
||||
|
||||
21
api/go.mod
21
api/go.mod
@@ -30,7 +30,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/jpillora/chisel v1.9.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
@@ -40,7 +40,7 @@ require (
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/libhttp v0.0.0-20230615144939-a999f666d9a9
|
||||
github.com/portainer/portainer/pkg/featureflags v0.0.0-20230711022654-64b227b2e146
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230928223730-157393c965ce
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230711022654-64b227b2e146
|
||||
github.com/portainer/portainer/pkg/libstack v0.0.0-20230711022654-64b227b2e146
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
@@ -48,11 +48,11 @@ require (
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||
golang.org/x/mod v0.9.0
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sync v0.3.0
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.27.4
|
||||
@@ -109,7 +109,7 @@ require (
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/ansi v1.0.2 // indirect
|
||||
github.com/jpillora/ansi v1.0.3 // indirect
|
||||
github.com/jpillora/requestlog v1.0.0 // indirect
|
||||
github.com/jpillora/sizestr v1.0.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
@@ -143,10 +143,10 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/term v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
@@ -161,6 +161,3 @@ require (
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
// Remove below line when the "determinstic key" patch for Chisel merged
|
||||
replace github.com/jpillora/chisel => github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25
|
||||
|
||||
44
api/go.sum
44
api/go.sum
@@ -227,9 +227,10 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s=
|
||||
github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jpillora/ansi v1.0.3 h1:nn4Jzti0EmRfDxm7JtEs5LzCbNwd5sv+0aE+LdS9/ZQ=
|
||||
github.com/jpillora/ansi v1.0.3/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
|
||||
github.com/jpillora/chisel v1.9.0 h1:pGZuxCZZ3W56Y2wX5bcXUvtB3r6wdaXRruJLAev8xzk=
|
||||
github.com/jpillora/chisel v1.9.0/go.mod h1:qvgGfFR9ZhiDoYJM4IM1omX1HLbQSkZag8miP9u4SsQ=
|
||||
github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=
|
||||
github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
|
||||
github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
|
||||
@@ -310,8 +311,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25 h1:OfU9WGqE8bYdKB1dH3jgQpM2tP1+l5wGdNLO8Kk7nww=
|
||||
github.com/portainer/chisel v0.0.0-20230704222304-426f515c6c25/go.mod h1:jhzGKO7NT6pNc/qto8YrNBGnuWZdqswvY6+n4zwE/Zc=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhttp v0.0.0-20230615144939-a999f666d9a9 h1:Jq8g/pDcFL1Z/DnZgn6DyaWu29y9+RiB5aOJ/Xw4960=
|
||||
@@ -320,10 +319,6 @@ github.com/portainer/portainer/pkg/featureflags v0.0.0-20230711022654-64b227b2e1
|
||||
github.com/portainer/portainer/pkg/featureflags v0.0.0-20230711022654-64b227b2e146/go.mod h1:x4Lpq/BjFhZmuNB8e8FO0ObRPQ/Z/V9rTe54bMedf1A=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230711022654-64b227b2e146 h1:1qW7quKyFG4tOnMcnnqyYsDVfL09etO1h/Cu/3ak7KU=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230711022654-64b227b2e146/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230919060741-8f42ba025479 h1:DbmhSQZpDo5f0cr+CKLJqoqhQiuxp8QFXdZsjPS1lI4=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230919060741-8f42ba025479/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230928223730-157393c965ce h1:DQTMXYH1zn2DzuAe+4rT40JqdHLhpHHJ2pzRFhvZ/+c=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230928223730-157393c965ce/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM=
|
||||
github.com/portainer/portainer/pkg/libstack v0.0.0-20230711022654-64b227b2e146 h1:ZGj+j5HoajaO+mXgCm6NzOU+zUdIlJK2amagB+QIDvc=
|
||||
github.com/portainer/portainer/pkg/libstack v0.0.0-20230711022654-64b227b2e146/go.mod h1:+zCK2UbsH6A3yEGi0yZ45ec5VFRP7svob5Q2lW6LFgk=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73 h1:7bPOnwucE0nor0so1BQJxQKCL5t+vCWO4nAz/S0lci0=
|
||||
@@ -402,9 +397,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
@@ -427,9 +421,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
@@ -439,9 +432,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -457,8 +449,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -471,19 +461,17 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -24,7 +24,6 @@ type Handler struct {
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
passwordStrengthChecker security.PasswordStrengthChecker
|
||||
bouncer security.BouncerService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
@@ -32,7 +31,6 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
bouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
@@ -40,6 +38,7 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
|
||||
h.Handle("/auth",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||
h.Handle("/auth/logout",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/internal/logoutcontext"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: public
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /auth/logout [post]
|
||||
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
tokenData := handler.bouncer.JWTAuthLookup(r)
|
||||
|
||||
if tokenData != nil {
|
||||
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
|
||||
logoutcontext.Cancel(tokenData.Token)
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve user details from authentication token", err)
|
||||
}
|
||||
|
||||
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package customtemplates
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -473,29 +472,3 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
// @id CustomTemplateCreate
|
||||
// @summary Create a custom template
|
||||
// @description Create a custom template.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json,multipart/form-data
|
||||
// @produce json
|
||||
// @param method query string true "method for creating template" Enums(string, file, repository)
|
||||
// @param body body object true "for body documentation see the relevant /custom_templates/{method} endpoint"
|
||||
// @success 200 {object} portainer.CustomTemplate
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @deprecated
|
||||
// @router /custom_templates [post]
|
||||
func deprecatedCustomTemplateCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/custom_templates/create/%s", method)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -33,7 +32,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
|
||||
h.Handle("/custom_templates/create/{method}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/custom_templates", middlewares.Deprecated(h, deprecatedCustomTemplateCreateUrlParser)).Methods(http.MethodPost) // Deprecated
|
||||
h.Handle("/custom_templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet)
|
||||
h.Handle("/custom_templates/{id}",
|
||||
|
||||
@@ -2,7 +2,6 @@ package edgejobs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -288,26 +287,3 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
|
||||
return tx.EdgeJob().CreateWithID(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
// @id EdgeJobCreate
|
||||
// @summary Create an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file, string)
|
||||
// @param body body object true "for body documentation see the relevant /edge_jobs/create/{method} endpoint"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @deprecated
|
||||
// @router /edge_jobs [post]
|
||||
func deprecatedEdgeJobCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/edge_jobs/create/%s", method), nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -30,8 +29,6 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeJobCreateUrlParser)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/create/{method}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/{id}",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -19,7 +18,6 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
dryrun, _ := request.RetrieveBooleanQueryParameter(r, "dryrun", true)
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
@@ -62,26 +60,3 @@ func (handler *Handler) createSwarmStack(tx dataservices.DataStoreTx, method str
|
||||
|
||||
return nil, httperrors.NewInvalidPayloadError("Invalid value for query parameter: method. Value must be one of: string, repository or file")
|
||||
}
|
||||
|
||||
// @id EdgeStackCreate
|
||||
// @summary Create an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file,string,repository)
|
||||
// @param body body object true "for body documentation see the relevant /edge_stacks/create/{method} endpoint"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @deprecated
|
||||
// @router /edge_stacks [post]
|
||||
func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/edge_stacks/create/%s", method), nil
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
|
||||
h.Handle("/edge_stacks/create/{method}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeStackCreateUrlParser)))).Methods(http.MethodPost) // Deprecated
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
|
||||
@@ -66,6 +66,11 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().ReadAll()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge groups from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
@@ -88,7 +93,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, edgeGroups, settings)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to filter endpoints", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
"github.com/portainer/portainer/api/internal/unique"
|
||||
@@ -118,7 +119,13 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
|
||||
func (handler *Handler) filterEndpointsByQuery(
|
||||
filteredEndpoints []portainer.Endpoint,
|
||||
query EnvironmentsQuery,
|
||||
groups []portainer.EndpointGroup,
|
||||
edgeGroups []portainer.EdgeGroup,
|
||||
settings *portainer.Settings,
|
||||
) ([]portainer.Endpoint, int, error) {
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if len(query.endpointIds) > 0 {
|
||||
@@ -190,7 +197,7 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
|
||||
tagsMap[tag.ID] = tag.Name
|
||||
}
|
||||
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, edgeGroups, tagsMap, query.search)
|
||||
}
|
||||
|
||||
if len(query.types) > 0 {
|
||||
@@ -285,7 +292,13 @@ func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs
|
||||
return endpoints[:n]
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
|
||||
func filterEndpointsBySearchCriteria(
|
||||
endpoints []portainer.Endpoint,
|
||||
endpointGroups []portainer.EndpointGroup,
|
||||
edgeGroups []portainer.EdgeGroup,
|
||||
tagsMap map[portainer.TagID]string,
|
||||
searchCriteria string,
|
||||
) []portainer.Endpoint {
|
||||
n := 0
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
@@ -299,6 +312,15 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
endpoints[n] = endpoint
|
||||
n++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpoints, endpointGroups) {
|
||||
endpoints[n] = endpoint
|
||||
n++
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +400,29 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
|
||||
return false
|
||||
}
|
||||
|
||||
// search endpoint's related edgegroups
|
||||
func edgeGroupMatchSearchCriteria(
|
||||
endpoint *portainer.Endpoint,
|
||||
edgeGroups []portainer.EdgeGroup,
|
||||
searchCriteria string,
|
||||
endpoints []portainer.Endpoint,
|
||||
endpointGroups []portainer.EndpointGroup,
|
||||
) bool {
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, endpoints, endpointGroups)
|
||||
|
||||
for _, endpointID := range relatedEndpointIDs {
|
||||
if endpointID == endpoint.ID {
|
||||
if strings.Contains(strings.ToLower(edgeGroup.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
|
||||
@@ -158,7 +158,13 @@ func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []po
|
||||
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
|
||||
is := assert.New(t)
|
||||
|
||||
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
|
||||
filteredEndpoints, _, err := handler.filterEndpointsByQuery(
|
||||
endpoints,
|
||||
test.query,
|
||||
[]portainer.EndpointGroup{},
|
||||
[]portainer.EdgeGroup{},
|
||||
&portainer.Settings{},
|
||||
)
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.19.2
|
||||
// @version 2.20.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/git/update"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
"github.com/portainer/portainer/api/stacks/stackbuilders"
|
||||
@@ -177,14 +176,6 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
handler.KubernetesDeployer,
|
||||
user)
|
||||
|
||||
// Refresh ECR registry secret if needed
|
||||
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
||||
// otherwise return nil
|
||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err == nil {
|
||||
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, payload.Namespace)
|
||||
}
|
||||
|
||||
stackBuilderDirector := stackbuilders.NewStackBuilderDirector(k8sStackBuilder)
|
||||
_, httpErr := stackBuilderDirector.Build(&stackPayload, endpoint)
|
||||
if httpErr != nil {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
@@ -59,8 +58,6 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
|
||||
h.Handle("/stacks/create/{type}/{method}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks",
|
||||
bouncer.AuthenticatedAccess(middlewares.Deprecated(h, deprecatedStackCreateUrlParser))).Methods(http.MethodPost) // Deprecated
|
||||
h.Handle("/stacks",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackList))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -140,53 +139,3 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func getStackTypeFromQueryParameter(r *http.Request) (string, error) {
|
||||
stackType, err := request.RetrieveNumericQueryParameter(r, "type", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch stackType {
|
||||
case 1:
|
||||
return "swarm", nil
|
||||
case 2:
|
||||
return "standalone", nil
|
||||
case 3:
|
||||
return "kubernetes", nil
|
||||
}
|
||||
|
||||
return "", errors.New(request.ErrInvalidQueryParameter)
|
||||
}
|
||||
|
||||
// @id StackCreate
|
||||
// @summary Deploy a new stack
|
||||
// @description Deploy a new stack into a Docker environment(endpoint) specified via the environment(endpoint) identifier.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags stacks
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json,multipart/form-data
|
||||
// @produce json
|
||||
// @param type query int true "Stack deployment type. Possible values: 1 (Swarm stack), 2 (Compose stack) or 3 (Kubernetes stack)." Enums(1,2,3)
|
||||
// @param method query string true "Stack deployment method. Possible values: file, string, repository or url." Enums(string, file, repository, url)
|
||||
// @param endpointId query int true "Identifier of the environment(endpoint) that will be used to deploy the stack"
|
||||
// @param body body object true "for body documentation see the relevant /stacks/create/{type}/{method} endpoint"
|
||||
// @success 200 {object} portainer.Stack
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @deprecated
|
||||
// @router /stacks [post]
|
||||
func deprecatedStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
stackType, err := getStackTypeFromQueryParameter(r)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: type", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/stacks/create/%s/%s", stackType, method), nil
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.UndeployRemoteSwarmStack(stack, endpoint)
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St
|
||||
if stack.Type == portainer.DockerComposeStack {
|
||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.UndeployRemoteComposeStack(stack, endpoint)
|
||||
}
|
||||
|
||||
|
||||
@@ -141,34 +141,34 @@ func (handler *Handler) startStack(
|
||||
endpoint *portainer.Endpoint,
|
||||
securityContext *security.RestrictedRequestContext,
|
||||
) error {
|
||||
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load user information from the database: %w", err)
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve registries from the database: %w", err)
|
||||
}
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint)
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries)
|
||||
}
|
||||
|
||||
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
|
||||
case portainer.DockerSwarmStack:
|
||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
return handler.StackDeployer.StartRemoteSwarmStack(stack, endpoint)
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.StartRemoteSwarmStack(stack, endpoint, filteredRegistries)
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load user information from the database: %w", err)
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve registries from the database: %w", err)
|
||||
}
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
return handler.StackDeployer.DeploySwarmStack(stack, endpoint, filteredRegistries, true, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.En
|
||||
case portainer.DockerComposeStack:
|
||||
stack.Name = handler.ComposeStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.StopRemoteComposeStack(stack, endpoint)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.En
|
||||
case portainer.DockerSwarmStack:
|
||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
return handler.StackDeployer.StopRemoteSwarmStack(stack, endpoint)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/git/update"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
|
||||
@@ -114,14 +113,6 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
return httperror.InternalServerError("Failed to persist deployment file in a temp directory", err)
|
||||
}
|
||||
|
||||
// Refresh ECR registry secret if needed
|
||||
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
||||
// otherwise return nil
|
||||
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err == nil {
|
||||
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, stack.Namespace)
|
||||
}
|
||||
|
||||
//use temp dir as the stack project path for deployment
|
||||
//so if the deployment failed, the original file won't be over-written
|
||||
stack.ProjectPath = tempFileDir
|
||||
|
||||
@@ -4,12 +4,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -17,8 +13,7 @@ import (
|
||||
// Handler is the HTTP handler used to handle team membership operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore dataservices.DataStore
|
||||
K8sClientFactory *cli.ClientFactory
|
||||
DataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team membership operations.
|
||||
@@ -36,27 +31,3 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) updateUserServiceAccounts(membership *portainer.TeamMembership) {
|
||||
endpoints, err := handler.DataStore.Endpoint().EndpointsByTeamID(membership.TeamID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed fetching environments for team %d", membership.TeamID)
|
||||
return
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||
// update kubernenets service accounts if the team is associated with a kubernetes environment
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
kubecli, err := handler.K8sClientFactory.GetKubeClient(&endpoint)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
|
||||
continue
|
||||
}
|
||||
teamIDs := []int{int(membership.TeamID)}
|
||||
err = kubecli.SetupUserServiceAccount(int(membership.UserID), teamIDs, restrictDefaultNamespace)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed setting-up service account for user %d", membership.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,5 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.InternalServerError("Unable to persist team memberships inside the database", err)
|
||||
}
|
||||
|
||||
defer handler.updateUserServiceAccounts(membership)
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,5 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.InternalServerError("Unable to remove the team membership from the database", err)
|
||||
}
|
||||
|
||||
defer handler.updateUserServiceAccounts(membership)
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,5 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.InternalServerError("Unable to persist membership changes inside the database", err)
|
||||
}
|
||||
|
||||
defer handler.updateUserServiceAccounts(membership)
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ var (
|
||||
errAdminCannotRemoveSelf = errors.New("Cannot remove your own user account. Contact another administrator")
|
||||
errCannotRemoveLastLocalAdmin = errors.New("Cannot remove the last local administrator account")
|
||||
errCryptoHashFailure = errors.New("Unable to hash data")
|
||||
errWrongPassword = errors.New("Wrong password")
|
||||
)
|
||||
|
||||
func hideFields(user *portainer.User) {
|
||||
|
||||
@@ -10,13 +10,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID portainer.UserID `json:"Id" example:"1"`
|
||||
Username string `json:"Username" example:"bob"`
|
||||
// User role (1 for administrator account and 2 for regular account)
|
||||
Role portainer.UserRole `json:"Role" example:"1"`
|
||||
}
|
||||
|
||||
// @id UserList
|
||||
// @summary List users
|
||||
// @description List Portainer users.
|
||||
@@ -33,25 +26,24 @@ type User struct {
|
||||
// @failure 500 "Server error"
|
||||
// @router /users [get]
|
||||
func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return httperror.Forbidden("Permission denied to access users list", err)
|
||||
}
|
||||
|
||||
users, err := handler.DataStore.User().ReadAll()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve users from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
availableUsers := security.FilterUsers(users, securityContext)
|
||||
for i := range availableUsers {
|
||||
hideFields(&availableUsers[i])
|
||||
}
|
||||
|
||||
endpointID, _ := request.RetrieveNumericQueryParameter(r, "environmentId", true)
|
||||
if endpointID == 0 {
|
||||
return response.JSON(w, sanitizeUsers(availableUsers))
|
||||
return response.JSON(w, availableUsers)
|
||||
}
|
||||
|
||||
// filter out users who do not have access to the specific endpoint
|
||||
@@ -65,11 +57,11 @@ func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httper
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
canAccessEndpoint := make([]User, 0)
|
||||
canAccessEndpoint := make([]portainer.User, 0)
|
||||
for _, user := range availableUsers {
|
||||
// the users who have the endpoint authorization
|
||||
if _, ok := user.EndpointAuthorizations[endpoint.ID]; ok {
|
||||
canAccessEndpoint = append(canAccessEndpoint, sanitizeUser(user))
|
||||
canAccessEndpoint = append(canAccessEndpoint, user)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -80,25 +72,9 @@ func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httper
|
||||
}
|
||||
|
||||
if security.AuthorizedEndpointAccess(endpoint, endpointGroup, user.ID, teamMemberships) {
|
||||
canAccessEndpoint = append(canAccessEndpoint, sanitizeUser(user))
|
||||
canAccessEndpoint = append(canAccessEndpoint, user)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, canAccessEndpoint)
|
||||
}
|
||||
|
||||
func sanitizeUser(user portainer.User) User {
|
||||
return User{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeUsers(users []portainer.User) []User {
|
||||
u := make([]User, len(users))
|
||||
for i := range users {
|
||||
u[i] = sanitizeUser(users[i])
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -111,14 +111,28 @@ func Test_userList(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("standard user cannot list users", func(t *testing.T) {
|
||||
t.Run("standard user cannot list amdin users", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/users", nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusForbidden, rr.Code)
|
||||
is.Equal(http.StatusOK, rr.Code)
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
var resp []portainer.User
|
||||
err = json.Unmarshal(body, &resp)
|
||||
is.NoError(err, "response should be list json")
|
||||
|
||||
is.Len(resp, 2)
|
||||
if len(resp) > 0 {
|
||||
for _, user := range resp {
|
||||
is.NotEqual(portainer.AdministratorRole, user.Role)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Case 2: the user is under an environment group and the environment group has endpoint access.
|
||||
|
||||
@@ -21,10 +21,9 @@ type themePayload struct {
|
||||
}
|
||||
|
||||
type userUpdatePayload struct {
|
||||
Username string `validate:"required" example:"bob"`
|
||||
Password string `validate:"required" example:"cg9Wgky3"`
|
||||
NewPassword string `validate:"required" example:"asfj2emv"`
|
||||
Theme *themePayload
|
||||
Username string `validate:"required" example:"bob"`
|
||||
Password string `validate:"required" example:"cg9Wgky3"`
|
||||
Theme *themePayload
|
||||
|
||||
// User role (1 for administrator account and 2 for regular account)
|
||||
Role int `validate:"required" enums:"1,2" example:"2"`
|
||||
@@ -38,14 +37,12 @@ func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
||||
return errors.New("invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id UserUpdate
|
||||
// @summary Update a user
|
||||
// @description Update user details. A regular user account can only update his details.
|
||||
// @description A regular user account cannot change their username or role.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags users
|
||||
// @security ApiKeyAuth
|
||||
@@ -98,10 +95,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
if payload.Username != "" && payload.Username != user.Username {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return httperror.Forbidden("Permission denied. Unable to update username", httperrors.ErrResourceAccessDenied)
|
||||
}
|
||||
|
||||
sameNameUser, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.InternalServerError("Unable to retrieve users from the database", err)
|
||||
@@ -113,28 +106,8 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
user.Username = payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != "" && payload.NewPassword == "" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return httperror.BadRequest("Existing password field specified without new password field.", errors.New("To change the password as an admin, you only need 'newPassword' in your request"))
|
||||
}
|
||||
|
||||
return httperror.BadRequest("Existing password field specified without new password field.", errors.New("To change the password, you must include both 'password' and 'newPassword' in your request"))
|
||||
}
|
||||
|
||||
if payload.NewPassword != "" {
|
||||
// Non-admins need to supply the previous password
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
err := handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Current password doesn't match. Password left unchanged", errors.New("Current password does not match the password provided. Please try again"))
|
||||
}
|
||||
}
|
||||
|
||||
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
|
||||
return httperror.BadRequest("Password does not meet the minimum strength requirements", nil)
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.NewPassword)
|
||||
if payload.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to hash user password", errCryptoHashFailure)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
|
||||
return httperror.BadRequest("Password does not meet the minimum strength requirements", nil)
|
||||
return httperror.BadRequest("Password does not meet the requirements", nil)
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.NewPassword)
|
||||
|
||||
@@ -9,11 +9,9 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// @summary Attach a websocket
|
||||
@@ -76,13 +74,6 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("unable to retrieve user details from authentication token")
|
||||
return err
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
|
||||
@@ -98,15 +89,10 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
defer websocketConn.Close()
|
||||
|
||||
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
|
||||
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID)
|
||||
}
|
||||
|
||||
func hijackAttachStartOperation(
|
||||
websocketConn *websocket.Conn,
|
||||
endpoint *portainer.Endpoint,
|
||||
attachID string,
|
||||
token string,
|
||||
) error {
|
||||
func hijackAttachStartOperation(websocketConn *websocket.Conn, endpoint *portainer.Endpoint, attachID string) error {
|
||||
dial, err := initDial(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,7 +116,7 @@ func hijackAttachStartOperation(
|
||||
return err
|
||||
}
|
||||
|
||||
return hijackRequest(websocketConn, httpConn, attachStartRequest, token)
|
||||
return hijackRequest(websocketConn, httpConn, attachStartRequest)
|
||||
}
|
||||
|
||||
func createAttachStartRequest(attachID string) (*http.Request, error) {
|
||||
|
||||
@@ -11,11 +11,9 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type execStartOperationPayload struct {
|
||||
@@ -82,14 +80,6 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
|
||||
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("unable to retrieve user details from authentication token")
|
||||
return err
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
@@ -104,15 +94,10 @@ func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
defer websocketConn.Close()
|
||||
|
||||
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
|
||||
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID)
|
||||
}
|
||||
|
||||
func hijackExecStartOperation(
|
||||
websocketConn *websocket.Conn,
|
||||
endpoint *portainer.Endpoint,
|
||||
execID string,
|
||||
token string,
|
||||
) error {
|
||||
func hijackExecStartOperation(websocketConn *websocket.Conn, endpoint *portainer.Endpoint, execID string) error {
|
||||
dial, err := initDial(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -136,7 +121,7 @@ func hijackExecStartOperation(
|
||||
return err
|
||||
}
|
||||
|
||||
return hijackRequest(websocketConn, httpConn, execStartRequest, token)
|
||||
return hijackRequest(websocketConn, httpConn, execStartRequest)
|
||||
}
|
||||
|
||||
func createExecStartRequest(execID string) (*http.Request, error) {
|
||||
|
||||
@@ -7,15 +7,9 @@ import (
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/portainer/portainer/api/internal/logoutcontext"
|
||||
)
|
||||
|
||||
func hijackRequest(
|
||||
websocketConn *websocket.Conn,
|
||||
httpConn *httputil.ClientConn,
|
||||
request *http.Request,
|
||||
token string,
|
||||
) error {
|
||||
func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error {
|
||||
// Server hijacks the connection, error 'connection closed' expected
|
||||
resp, err := httpConn.Do(request)
|
||||
if !errors.Is(err, httputil.ErrPersistEOF) {
|
||||
@@ -35,15 +29,9 @@ func hijackRequest(
|
||||
go streamFromReaderToWebsocket(websocketConn, brw, errorChan)
|
||||
go streamFromWebsocketToWriter(websocketConn, tcpConn, errorChan)
|
||||
|
||||
logoutCtx := logoutcontext.GetContext(token)
|
||||
|
||||
select {
|
||||
case <-logoutCtx.Done():
|
||||
return fmt.Errorf("Your session has been logged out.")
|
||||
case err = <-errorChan:
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
return err
|
||||
}
|
||||
err = <-errorChan
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/logoutcontext"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/koding/websocketproxy"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
@@ -23,12 +18,33 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
|
||||
return err
|
||||
}
|
||||
|
||||
agentURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
|
||||
endpointURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.doProxyWebsocketRequest(w, r, params, agentURL, true)
|
||||
endpointURL.Scheme = "ws"
|
||||
proxy := websocketproxy.NewProxy(endpointURL)
|
||||
|
||||
signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||
|
||||
handler.ReverseTunnelService.KeepTunnelAlive(params.endpoint.ID, r.Context(), portainer.WebSocketKeepAlive)
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
@@ -43,41 +59,17 @@ func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
agentURL.Scheme = "ws"
|
||||
return handler.doProxyWebsocketRequest(w, r, params, agentURL, false)
|
||||
}
|
||||
|
||||
func (handler *Handler) doProxyWebsocketRequest(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
params *webSocketRequestParams,
|
||||
agentURL *url.URL,
|
||||
isEdge bool,
|
||||
) error {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Msg("unable to retrieve user details from authentication token")
|
||||
return err
|
||||
}
|
||||
|
||||
enableTLS := !isEdge && (params.endpoint.TLSConfig.TLS || params.endpoint.TLSConfig.TLSSkipVerify)
|
||||
|
||||
agentURL.Scheme = "ws"
|
||||
if enableTLS {
|
||||
agentURL.Scheme = "wss"
|
||||
}
|
||||
|
||||
proxy := websocketproxy.NewProxy(agentURL)
|
||||
proxyDialer := *websocket.DefaultDialer
|
||||
proxy.Dialer = &proxyDialer
|
||||
|
||||
if enableTLS {
|
||||
if params.endpoint.TLSConfig.TLS || params.endpoint.TLSConfig.TLSSkipVerify {
|
||||
agentURL.Scheme = "wss"
|
||||
|
||||
tlsConfig := crypto.CreateTLSConfiguration()
|
||||
tlsConfig.InsecureSkipVerify = params.endpoint.TLSConfig.TLSSkipVerify
|
||||
|
||||
proxyDialer.TLSClientConfig = tlsConfig
|
||||
proxy.Dialer = &websocket.Dialer{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
@@ -92,46 +84,7 @@ func (handler *Handler) doProxyWebsocketRequest(
|
||||
out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token)
|
||||
}
|
||||
|
||||
if isEdge {
|
||||
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||
handler.ReverseTunnelService.KeepTunnelAlive(params.endpoint.ID, r.Context(), portainer.WebSocketKeepAlive)
|
||||
}
|
||||
|
||||
abortProxyOnLogout(r.Context(), proxy, tokenData.Token)
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func abortProxyOnLogout(ctx context.Context, proxy *websocketproxy.WebsocketProxy, token string) {
|
||||
var wsConn net.Conn
|
||||
|
||||
proxy.Dialer.NetDial = func(network, addr string) (net.Conn, error) {
|
||||
netDialer := &net.Dialer{}
|
||||
|
||||
conn, err := netDialer.DialContext(context.Background(), network, addr)
|
||||
wsConn = conn
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
logoutCtx := logoutcontext.GetContext(token)
|
||||
|
||||
go func() {
|
||||
log.Debug().
|
||||
Msg("logout watcher for websocket proxy started")
|
||||
|
||||
select {
|
||||
case <-logoutCtx.Done():
|
||||
log.Debug().
|
||||
Msg("logout watcher for websocket proxy stopped as user logged out")
|
||||
if wsConn != nil {
|
||||
wsConn.Close()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
log.Debug().
|
||||
Msg("logout watcher for websocket proxy stopped as the ws connection closed")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// deprecate api route
|
||||
func Deprecated(router http.Handler, urlBuilder func(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError)) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
newUrl, err := urlBuilder(w, r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, err.StatusCode, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn().Msgf("This api is deprecated. Use %s instead", newUrl)
|
||||
|
||||
redirectedRequest := r.Clone(r.Context())
|
||||
redirectedRequest.URL.Path = newUrl
|
||||
router.ServeHTTP(w, redirectedRequest)
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
@@ -45,62 +43,28 @@ func (manager *tokenManager) GetAdminServiceAccountToken() string {
|
||||
return manager.adminToken
|
||||
}
|
||||
|
||||
func (manager *tokenManager) setupUserServiceAccounts(userID portainer.UserID, endpoint *portainer.Endpoint) error {
|
||||
memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
teamIds := make([]int, 0, len(memberships))
|
||||
for _, membership := range memberships {
|
||||
teamIds = append(teamIds, int(membership.TeamID))
|
||||
}
|
||||
|
||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||
err = manager.kubecli.SetupUserServiceAccount(int(userID), teamIds, restrictDefaultNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *tokenManager) UpdateUserServiceAccountsForEndpoint(endpointID portainer.EndpointID) {
|
||||
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed fetching environments %d", endpointID)
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]portainer.UserID, 0)
|
||||
for u := range endpoint.UserAccessPolicies {
|
||||
userIDs = append(userIDs, u)
|
||||
}
|
||||
for t := range endpoint.TeamAccessPolicies {
|
||||
memberships, _ := manager.dataStore.TeamMembership().TeamMembershipsByTeamID(portainer.TeamID(t))
|
||||
for _, membership := range memberships {
|
||||
userIDs = append(userIDs, membership.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, userID := range userIDs {
|
||||
if err := manager.setupUserServiceAccounts(userID, endpoint); err != nil {
|
||||
log.Error().Err(err).Msgf("failed setting-up service account for user %d", userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserServiceAccountToken setup a user's service account if it does not exist, then retrieve its token
|
||||
func (manager *tokenManager) GetUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
|
||||
tokenFunc := func() (string, error) {
|
||||
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||
memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed fetching environment %d", endpointID)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := manager.setupUserServiceAccounts(portainer.UserID(userID), endpoint); err != nil {
|
||||
return "", fmt.Errorf("failed setting-up service account for user %d: %w", userID, err)
|
||||
teamIds := make([]int, 0, len(memberships))
|
||||
for _, membership := range memberships {
|
||||
teamIds = append(teamIds, int(membership.TeamID))
|
||||
}
|
||||
|
||||
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manager.kubecli.GetServiceAccountBearerToken(userID)
|
||||
|
||||
@@ -49,17 +49,7 @@ func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*
|
||||
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/(api|apis/apps)/v[0-9](\.[0-9])?`)
|
||||
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||
|
||||
endpointRe := regexp.MustCompile(`([0-9]+)`)
|
||||
endpointIDMatch := endpointRe.FindAllString(request.RequestURI, 1)
|
||||
endpointID := 0
|
||||
if len(endpointIDMatch) > 0 {
|
||||
endpointID, _ = strconv.Atoi(endpointIDMatch[0])
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.EqualFold(requestPath, "/namespaces/portainer/configmaps/portainer-config") && (request.Method == "PUT" || request.Method == "POST"):
|
||||
defer transport.tokenManager.UpdateUserServiceAccountsForEndpoint(portainer.EndpointID(endpointID))
|
||||
return transport.executeKubernetesRequest(request)
|
||||
case strings.EqualFold(requestPath, "/namespaces"):
|
||||
return transport.executeKubernetesRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/namespaces"):
|
||||
|
||||
@@ -60,15 +60,15 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService dataservices
|
||||
}
|
||||
}
|
||||
|
||||
// PublicAccess defines a security check for public API endpoints.
|
||||
// No authentication is required to access these endpoints.
|
||||
// PublicAccess defines a security check for public API environments(endpoints).
|
||||
// No authentication is required to access these environments(endpoints).
|
||||
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||
return mwSecureHeaders(h)
|
||||
}
|
||||
|
||||
// AdminAccess defines a security check for API endpoints that require an authorization check.
|
||||
// Authentication is required to access these endpoints.
|
||||
// The administrator role is required to use these endpoints.
|
||||
// AdminAccess defines a security check for API environments(endpoints) that require an authorization check.
|
||||
// Authentication is required to access these environments(endpoints).
|
||||
// The administrator role is required to use these environments(endpoints).
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// and resource filtering.
|
||||
@@ -79,8 +79,8 @@ func (bouncer *RequestBouncer) AdminAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// RestrictedAccess defines a security check for restricted API endpoints.
|
||||
// Authentication is required to access these endpoints.
|
||||
// RestrictedAccess defines a security check for restricted API environments(endpoints).
|
||||
// Authentication is required to access these environments(endpoints).
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// and resource filtering.
|
||||
@@ -104,8 +104,8 @@ func (bouncer *RequestBouncer) TeamLeaderAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// AuthenticatedAccess defines a security check for restricted API endpoints.
|
||||
// Authentication is required to access these endpoints.
|
||||
// AuthenticatedAccess defines a security check for restricted API environments(endpoints).
|
||||
// Authentication is required to access these environments(endpoints).
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// and resource filtering.
|
||||
|
||||
@@ -100,7 +100,6 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
||||
endpointGroup := getAssociatedGroup(&endpoint, groups)
|
||||
|
||||
if AuthorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
|
||||
endpoint.UserAccessPolicies = nil
|
||||
endpoints[n] = endpoint
|
||||
n++
|
||||
}
|
||||
|
||||
@@ -259,7 +259,6 @@ func (server *Server) Start() error {
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
teamMembershipHandler.K8sClientFactory = server.KubernetesClientFactory
|
||||
|
||||
var systemHandler = system.NewHandler(requestBouncer,
|
||||
server.Status,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package logoutcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const LogoutPrefix = "logout-"
|
||||
|
||||
func GetContext(token string) context.Context {
|
||||
return GetService(logoutToken(token)).GetLogoutCtx()
|
||||
}
|
||||
|
||||
func Cancel(token string) {
|
||||
GetService(logoutToken(token)).Cancel()
|
||||
RemoveService(logoutToken(token))
|
||||
}
|
||||
|
||||
func logoutToken(token string) string {
|
||||
return LogoutPrefix + token
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package logoutcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type (
|
||||
Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
)
|
||||
|
||||
func NewService() *Service {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Service{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Cancel() {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
func (s *Service) GetLogoutCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package logoutcontext
|
||||
|
||||
import "sync"
|
||||
|
||||
type (
|
||||
ServiceFactory struct {
|
||||
mu sync.Mutex
|
||||
services map[string]*Service
|
||||
}
|
||||
)
|
||||
|
||||
var serviceFactory = ServiceFactory{
|
||||
services: make(map[string]*Service),
|
||||
}
|
||||
|
||||
func GetService(token string) *Service {
|
||||
serviceFactory.mu.Lock()
|
||||
defer serviceFactory.mu.Unlock()
|
||||
|
||||
service, ok := serviceFactory.services[token]
|
||||
if !ok {
|
||||
service = NewService()
|
||||
serviceFactory.services[token] = service
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func RemoveService(token string) {
|
||||
serviceFactory.mu.Lock()
|
||||
defer serviceFactory.mu.Unlock()
|
||||
|
||||
delete(serviceFactory.services, token)
|
||||
}
|
||||
@@ -301,19 +301,6 @@ func (s *stubEndpointService) GetNextIdentifier() int {
|
||||
return len(s.endpoints)
|
||||
}
|
||||
|
||||
func (s *stubEndpointService) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, e := range s.endpoints {
|
||||
for t := range e.TeamAccessPolicies {
|
||||
if t == teamID {
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// WithEndpoints option will instruct testDatastore to return provided environments(endpoints)
|
||||
func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
|
||||
return func(d *testDatastore) {
|
||||
|
||||
@@ -137,7 +137,6 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
|
||||
ID: portainer.UserID(cl.UserID),
|
||||
Username: cl.Username,
|
||||
Role: portainer.UserRole(cl.Role),
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -155,29 +154,17 @@ func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.En
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateClient returns a pointer to a new Clientset instance.
|
||||
// CreateClient returns a pointer to a new Clientset instance
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||
switch endpoint.Type {
|
||||
case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
c, err := factory.CreateConfig(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kubernetes.NewForConfig(c)
|
||||
}
|
||||
return nil, errors.New("unsupported environment type")
|
||||
}
|
||||
|
||||
// CreateConfig returns a pointer to a new kubeconfig ready to create a client.
|
||||
func (factory *ClientFactory) CreateConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
|
||||
switch endpoint.Type {
|
||||
case portainer.KubernetesLocalEnvironment:
|
||||
return buildLocalConfig()
|
||||
return buildLocalClient()
|
||||
case portainer.AgentOnKubernetesEnvironment:
|
||||
return factory.buildAgentConfig(endpoint)
|
||||
return factory.buildAgentClient(endpoint)
|
||||
case portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
return factory.buildEdgeConfig(endpoint)
|
||||
return factory.buildEdgeClient(endpoint)
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported environment type")
|
||||
}
|
||||
|
||||
@@ -197,64 +184,20 @@ func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response,
|
||||
return rt.roundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) buildAgentConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
|
||||
var clientURL strings.Builder
|
||||
if !strings.HasPrefix(endpoint.URL, "http") {
|
||||
clientURL.WriteString("https://")
|
||||
}
|
||||
clientURL.WriteString(endpoint.URL)
|
||||
clientURL.WriteString("/kubernetes")
|
||||
func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
|
||||
|
||||
signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags(clientURL.String(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
signatureHeader: signature,
|
||||
publicKeyHeader: factory.signatureService.EncodedPublicKey(),
|
||||
roundTripper: rt,
|
||||
}
|
||||
})
|
||||
return config, nil
|
||||
return factory.createRemoteClient(endpointURL)
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*rest.Config, error) {
|
||||
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
|
||||
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed activating tunnel")
|
||||
}
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags(endpointURL, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
signatureHeader: signature,
|
||||
publicKeyHeader: factory.signatureService.EncodedPublicKey(),
|
||||
roundTripper: rt,
|
||||
}
|
||||
})
|
||||
|
||||
return config, nil
|
||||
return factory.createRemoteClient(endpointURL)
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernetes.Clientset, error) {
|
||||
@@ -284,14 +227,34 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) CreateRemoteMetricsClient(endpoint *portainer.Endpoint) (*metricsv.Clientset, error) {
|
||||
config, err := factory.CreateConfig(endpoint)
|
||||
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
|
||||
|
||||
signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create metrics KubeConfig")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags(endpointURL, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
signatureHeader: signature,
|
||||
publicKeyHeader: factory.signatureService.EncodedPublicKey(),
|
||||
roundTripper: rt,
|
||||
}
|
||||
})
|
||||
|
||||
return metricsv.NewForConfig(config)
|
||||
}
|
||||
|
||||
func buildLocalConfig() (*rest.Config, error) {
|
||||
func buildLocalClient() (*kubernetes.Clientset, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -300,7 +263,7 @@ func buildLocalConfig() (*rest.Config, error) {
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
return config, nil
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) MigrateEndpointIngresses(e *portainer.Endpoint) error {
|
||||
|
||||
@@ -1269,7 +1269,6 @@ type (
|
||||
Username string
|
||||
Role UserRole
|
||||
ForceChangePassword bool
|
||||
Token string
|
||||
}
|
||||
|
||||
// TunnelDetails represents information associated to a tunnel
|
||||
@@ -1404,7 +1403,6 @@ type (
|
||||
StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error)
|
||||
StoreStackFileFromBytesByVersion(stackIdentifier, fileName string, version int, data []byte) (string, error)
|
||||
UpdateStoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error)
|
||||
UpdateStoreStackFileFromBytesByVersion(stackIdentifier, fileName string, version int, commitHash string, data []byte) (string, error)
|
||||
RemoveStackFileBackup(stackIdentifier, fileName string) error
|
||||
RemoveStackFileBackupByVersion(stackIdentifier string, version int, fileName string) error
|
||||
RollbackStackFile(stackIdentifier, fileName string) error
|
||||
@@ -1561,7 +1559,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.19.2"
|
||||
APIVersion = "2.20.0"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1682,12 +1680,6 @@ const (
|
||||
EdgeStackStatusDeploying
|
||||
// EdgeStackStatusRemoving represents an Edge stack which is being removed
|
||||
EdgeStackStatusRemoving
|
||||
// EdgeStackStatusPausedDeploying represents a paused Edge stack
|
||||
EdgeStackStatusPausedDeploying
|
||||
// EdgeStackStatusRollingBack represents an Edge stack which is being rolled back
|
||||
EdgeStackStatusRollingBack
|
||||
// EdgeStackStatusRolledBack represents an Edge stack which has rolled back
|
||||
EdgeStackStatusRolledBack
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -17,18 +17,6 @@ type Scheduler struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type PermanentError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewPermanentError(err error) *PermanentError {
|
||||
return &PermanentError{err: err}
|
||||
}
|
||||
|
||||
func (e *PermanentError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func NewScheduler(ctx context.Context) *Scheduler {
|
||||
crontab := cron.New(cron.WithChain(cron.Recover(cron.DefaultLogger)))
|
||||
crontab.Start()
|
||||
@@ -96,24 +84,14 @@ func (s *Scheduler) StopJob(jobID string) error {
|
||||
func (s *Scheduler) StartJobEvery(duration time.Duration, job func() error) string {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
jobFn := cron.FuncJob(func() {
|
||||
err := job()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var permErr *PermanentError
|
||||
if errors.As(err, &permErr) {
|
||||
log.Error().Err(permErr).Msg("job returned a permanent error, it will be stopped")
|
||||
j := cron.FuncJob(func() {
|
||||
if err := job(); err != nil {
|
||||
log.Debug().Msg("job returned an error")
|
||||
cancel()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Err(err).Msg("job returned an error, it will be rescheduled")
|
||||
})
|
||||
|
||||
entryID := s.crontab.Schedule(cron.Every(duration), jobFn)
|
||||
entryID := s.crontab.Schedule(cron.Every(duration), j)
|
||||
|
||||
s.mu.Lock()
|
||||
s.activeJobs[entryID] = cancel
|
||||
|
||||
@@ -49,7 +49,7 @@ func Test_JobCanBeStopped(t *testing.T) {
|
||||
assert.False(t, workDone, "job shouldn't had a chance to run")
|
||||
}
|
||||
|
||||
func Test_JobShouldStop_UponPermError(t *testing.T) {
|
||||
func Test_JobShouldStop_UponError(t *testing.T) {
|
||||
s := NewScheduler(context.Background())
|
||||
defer s.Shutdown()
|
||||
|
||||
@@ -58,7 +58,7 @@ func Test_JobShouldStop_UponPermError(t *testing.T) {
|
||||
s.StartJobEvery(jobInterval, func() error {
|
||||
acc++
|
||||
close(ch)
|
||||
return NewPermanentError(fmt.Errorf("failed"))
|
||||
return fmt.Errorf("failed")
|
||||
})
|
||||
|
||||
<-time.After(3 * jobInterval)
|
||||
@@ -66,28 +66,6 @@ func Test_JobShouldStop_UponPermError(t *testing.T) {
|
||||
assert.Equal(t, 1, acc, "job stop after the first run because it returns an error")
|
||||
}
|
||||
|
||||
func Test_JobShouldNotStop_UponError(t *testing.T) {
|
||||
s := NewScheduler(context.Background())
|
||||
defer s.Shutdown()
|
||||
|
||||
var acc int
|
||||
ch := make(chan struct{})
|
||||
s.StartJobEvery(jobInterval, func() error {
|
||||
acc++
|
||||
|
||||
if acc == 2 {
|
||||
close(ch)
|
||||
return NewPermanentError(fmt.Errorf("failed"))
|
||||
}
|
||||
|
||||
return errors.New("non-permanent error")
|
||||
})
|
||||
|
||||
<-time.After(3 * jobInterval)
|
||||
<-ch
|
||||
assert.Equal(t, 2, acc)
|
||||
}
|
||||
|
||||
func Test_CanTerminateAllJobs_ByShuttingDownScheduler(t *testing.T) {
|
||||
s := NewScheduler(context.Background())
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func buildComposeStartCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions
|
||||
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
|
||||
cmd = append(cmd, "-k")
|
||||
cmd = append(cmd, env...)
|
||||
cmd = append(cmd, registries...)
|
||||
cmd = append(cmd, stack.GitConfig.URL)
|
||||
cmd = append(cmd, stack.GitConfig.ReferenceName)
|
||||
cmd = append(cmd, stack.Name)
|
||||
@@ -162,6 +163,7 @@ func buildSwarmStartCmd(stack *portainer.Stack, opts unpackerCmdBuilderOptions,
|
||||
cmd = append(cmd, UnpackerCmdSwarmDeploy, "-f", "-r", "-k")
|
||||
cmd = appendSkipTLSVerifyIfNeeded(cmd, stack)
|
||||
cmd = append(cmd, getEnv(stack.Env)...)
|
||||
cmd = append(cmd, registries...)
|
||||
cmd = append(cmd, stack.GitConfig.URL)
|
||||
cmd = append(cmd, stack.GitConfig.ReferenceName)
|
||||
cmd = append(cmd, stack.Name)
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package deployments
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/agent"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/git/update"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -33,9 +29,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
log.Debug().Int("stack_id", int(stackID)).Msg("redeploying stack")
|
||||
|
||||
stack, err := datastore.Stack().Read(stackID)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return scheduler.NewPermanentError(errors.WithMessagef(err, "failed to get the stack %v", stackID))
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to get the stack %v", stackID)
|
||||
}
|
||||
|
||||
@@ -44,15 +38,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
}
|
||||
|
||||
endpoint, err := datastore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return scheduler.NewPermanentError(
|
||||
errors.WithMessagef(err,
|
||||
"failed to find the environment %v associated to the stack %v",
|
||||
stack.EndpointID,
|
||||
stack.ID,
|
||||
),
|
||||
)
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
|
||||
}
|
||||
|
||||
@@ -73,10 +59,6 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
return &StackAuthorMissingErr{int(stack.ID), author}
|
||||
}
|
||||
|
||||
if !isEnvironmentOnline(endpoint) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var gitCommitChangedOrForceUpdate bool
|
||||
if !stack.FromAppTemplate {
|
||||
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, false, false, stack.ProjectPath)
|
||||
@@ -96,16 +78,14 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
}
|
||||
|
||||
registries, err := getUserRegistries(datastore, user, endpoint.ID)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return scheduler.NewPermanentError(err)
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
err = deployer.DeployRemoteComposeStack(stack, endpoint, registries, true, false)
|
||||
} else {
|
||||
err = deployer.DeployComposeStack(stack, endpoint, registries, true, false)
|
||||
@@ -115,7 +95,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
}
|
||||
case portainer.DockerSwarmStack:
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
if stackutils.IsGitStack(stack) {
|
||||
err = deployer.DeployRemoteSwarmStack(stack, endpoint, registries, true, true)
|
||||
} else {
|
||||
err = deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
|
||||
@@ -136,8 +116,6 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
||||
}
|
||||
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
if err := datastore.Stack().Update(stack.ID, stack); err != nil {
|
||||
return errors.WithMessagef(err, "failed to update the stack %v", stack.ID)
|
||||
}
|
||||
@@ -169,22 +147,3 @@ func getUserRegistries(datastore dataservices.DataStore, user *portainer.User, e
|
||||
|
||||
return filteredRegistries, nil
|
||||
}
|
||||
|
||||
func isEnvironmentOnline(endpoint *portainer.Endpoint) bool {
|
||||
if endpoint.Type != portainer.AgentOnDockerEnvironment &&
|
||||
endpoint.Type != portainer.AgentOnKubernetesEnvironment {
|
||||
return true
|
||||
}
|
||||
|
||||
var err error
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = agent.GetAgentVersionAndPlatform(endpoint.URL, tlsConfig)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -1,78 +1,18 @@
|
||||
package deployments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const localhostCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIEOjCCAiKgAwIBAgIRALg8rJET2/9LjKSxHj0dQhYwDQYJKoZIhvcNAQELBQAw
|
||||
FzEVMBMGA1UEAxMMUG9ydGFpbmVyIENBMB4XDTIzMTAxMTE5NDcxMVoXDTI1MDQx
|
||||
MTE5NTM0MVowFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAx4nNGiwcCqUCxZyVLIHqvjTy20ZtZDVCedssTv1W5tmz
|
||||
YqOIYGaW3CqzlRn6vBHu9bMHXef4+XfS0igKBn76MAKn5IcTccIWIal+5jq48pI3
|
||||
c2FzQ3qNujX2zqZPjAjhJnVeVCP3kJu4wUtuubswLPBVLdktGa6EkL+8nu6o0Phw
|
||||
6scV6s3gUmQk5/lpH4FIff8M7NAdTOxiFImQ1M0vplKtaEeiCnskpgyI8CbZl7X0
|
||||
38Pu178W3+LqB7N4iMy2gKnBwjsXzw/+1dfUGkKjYdDBD+kNEKrQ4dwkjkrkQVdt
|
||||
Z+GN26NvXHoeeyX/MLnVgdLbiIjvsf0DDIhabKqTcwIDAQABo4GDMIGAMA4GA1Ud
|
||||
DwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O
|
||||
BBYEFPCefmK5Szzlfs8FRCa5+kRCIEWuMB8GA1UdIwQYMBaAFKZZ074SR/ajD3zE
|
||||
gxpLGRvFT3XAMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBABcQ
|
||||
/WPSUpuQvrcVBsmIlOMz74cDZYuIDls/mAcB/yP3mm+oWlO0qvH/F/BMs1P/bkgj
|
||||
fByQZq8Zmi6/TEZNlGvW7KGx077VxDKi8jd1jL3gLDPmkFjYuGeIWQusgxBu1y3m
|
||||
0WoTTqnkoism1mzV/dgNwrm3YQIV4H/fi9EEdQSm0UFRTKSAGBkwS7N2pmNb5yQO
|
||||
U8glFpyznCv4evDJbs/JUUXKYExgFFhWUd25P7iBRLXg/BFfqdSTiUGUj/Msz0pO
|
||||
Evqmq78eIiXjyyKSxzve6/mEIeq6AE3AC9zH+fwTd6Mhp+T2P/S/iO4EU19IMR4m
|
||||
sbNBd6h/3GvRekO1KbqQ42awuMnxvWT0NVclSxiU1lMpAmRmk/w9z7wB3r4n7oh4
|
||||
iiOTl5VSw1UBkcLDOJw+HB/FU2PdVFfIJKRfjLCZOGrcJX9vEcz7dYGpB5HrdqOc
|
||||
/8q5j1g6f/pGE+20HITrtz6ChguETzqw5dLNeKeolC6bVH8yEtmpnP2n8VPnT9Di
|
||||
V+hnONcJ+wd/dkBqabGr7LPG24Kj1F2Zp3CDDvJA94FaEsgaLfSg3JD+43uRCOWM
|
||||
RuqU8bGuhQRqilR2dSIOrFaW2+MeUHsb24cUn/pkHqKpSg+RBEnf6QfGDlIgqYEl
|
||||
19f/HFVBc/a8lM/D81lMyDbjQ9zH4LDYj4ipBbkL
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const localhostKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAx4nNGiwcCqUCxZyVLIHqvjTy20ZtZDVCedssTv1W5tmzYqOI
|
||||
YGaW3CqzlRn6vBHu9bMHXef4+XfS0igKBn76MAKn5IcTccIWIal+5jq48pI3c2Fz
|
||||
Q3qNujX2zqZPjAjhJnVeVCP3kJu4wUtuubswLPBVLdktGa6EkL+8nu6o0Phw6scV
|
||||
6s3gUmQk5/lpH4FIff8M7NAdTOxiFImQ1M0vplKtaEeiCnskpgyI8CbZl7X038Pu
|
||||
178W3+LqB7N4iMy2gKnBwjsXzw/+1dfUGkKjYdDBD+kNEKrQ4dwkjkrkQVdtZ+GN
|
||||
26NvXHoeeyX/MLnVgdLbiIjvsf0DDIhabKqTcwIDAQABAoIBAQCqSP6BPG195A52
|
||||
iEeCISksw9ERsou+fflKNvIcQvV7swP0xOyooERUhhiVwQMKpx9QDUXXLRV8CHch
|
||||
JExR+OEYQdv4GhJM/b6XYafLYQfe80thKyQLzTXQWSdUeffe4OEMShODKOKoRUyp
|
||||
oO9Qj9/wKfX3V6S2iwnU4dxdofztv+YP9rYQyjnhKbv/9OfeCp2Pb9eFKKRsA+QQ
|
||||
xneDz1+wr8ToTuiTn8HBPNSeSAKvhzXuzyluI7VAetRloNgCtumrA9kpVbW2cDgE
|
||||
Gk0q3RY125ejFELQO/cOJFuBsqoJlvPxzg8/vHyfyF9hFMqbqvcUw2e1eqHpnJd5
|
||||
dP4+ZGYZAoGBAOOFuPXMLBts0rN9mfNbVfx36H+aOCL77SafZvWm0D+rH69QN3/q
|
||||
/ZSWQEjwH5Tzn1e+NVcl/Um2vL/dIyEGBklXQ7yAyJo25gpEOD/rt1U94HKzMOwy
|
||||
yKtsKghRAOx0piie7ORS6MGbEOQxU3/1Eg1uvd0qoSnALqJ/le75QpFXAoGBAOCD
|
||||
aZQTszzDddr1cFPzLyqjIGJWfPcDYSONXVcCeQmhvC7mkfw9SWdIfku7JbdNgFYq
|
||||
ZAAU0klsLX0lEe8f4A12FnHNylKoxmTWdE3wWPptejdA1KUgzt/2kNljgOMFuY0Q
|
||||
rlCEW/Jabrg5aFMwVVG8bHLZR0xalfniDvXLvnFFAoGACdztJLKiIto31BIYz2Th
|
||||
OF2WVZnA3ztej3MPioydsHThnb7zePcd4QgWZ1MJe3KIMMyNEWcTMNPcINEcSb0y
|
||||
HpHK3OwURiMlG8LTUWoNe4OALFi6QTL+YfgBZnTkflucLFyfVlKFxobLV6kPvpdI
|
||||
Hg7z6heD/wRWwTKYtFBX42cCgYBIeoQJ9rYlRqB0eEm0AEzYweLBfFRJVgD0/j0E
|
||||
ytqSPnFG3s6AFLTur9t9zUPmwhFNP9Aaqp4cb9zbiq0YejzVe6rRQHMxbiTmBslz
|
||||
I8VFyzPqRHahfE7sxGeMlm/UWlPFc34ipigcvA8EUBwaxv60LVUBWp2Gy7OhANZ9
|
||||
iTHI1QKBgQCdHFj9dnbpaEHA426CoaPsyj5cv2nBLRf8p1cs71sq+qQOGlGJfajm
|
||||
L9x22ol5c5rToZa1qKSnSdSDCud298MyRujMUy2UcUKHeNs3MK9AT41sDv266I7b
|
||||
vJUUCFYm8+9p6gTVOcoMit+eGSwa81PCPEs1TnU1PV/PaDFeUhn/mg==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
type noopDeployer struct{}
|
||||
|
||||
// without unpacker
|
||||
@@ -95,7 +35,7 @@ func (s *noopDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint
|
||||
func (s *noopDeployer) UndeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (s *noopDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) StopRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
@@ -107,49 +47,13 @@ func (s *noopDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *
|
||||
func (s *noopDeployer) UndeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (s *noopDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) StopRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func agentServer(t *testing.T) string {
|
||||
h := http.NewServeMux()
|
||||
|
||||
h.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(portainer.PortainerAgentHeader, "v2.19.0")
|
||||
w.Header().Set(portainer.HTTPResponseAgentPlatform, strconv.Itoa(int(portainer.AgentPlatformDocker)))
|
||||
|
||||
response.Empty(w)
|
||||
})
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(localhostCert), []byte(localhostKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
l, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := s.Serve(l)
|
||||
require.ErrorIs(t, err, http.ErrServerClosed)
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
s.Shutdown(context.Background())
|
||||
})
|
||||
|
||||
return "http://" + l.Addr().String()
|
||||
}
|
||||
|
||||
func Test_redeployWhenChanged_FailsWhenCannotFindStack(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
@@ -210,12 +114,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
||||
assert.NoError(t, err, "error creating an admin")
|
||||
|
||||
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||
ID: 0,
|
||||
URL: agentServer(t),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: true,
|
||||
TLSSkipVerify: true,
|
||||
},
|
||||
ID: 0,
|
||||
})
|
||||
assert.NoError(t, err, "error creating environment")
|
||||
|
||||
|
||||
@@ -33,22 +33,29 @@ type RemoteStackDeployer interface {
|
||||
// compose
|
||||
DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error
|
||||
UndeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
||||
StopRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
// swarm
|
||||
DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
||||
UndeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
|
||||
StopRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error
|
||||
}
|
||||
|
||||
// Deploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
func (d *stackDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error {
|
||||
func (d *stackDeployer) DeployRemoteComposeStack(
|
||||
stack *portainer.Stack,
|
||||
endpoint *portainer.Endpoint,
|
||||
registries []portainer.Registry,
|
||||
forcePullImage bool,
|
||||
forceRecreate bool,
|
||||
) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
@@ -57,9 +64,14 @@ func (d *stackDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoin
|
||||
}
|
||||
}
|
||||
|
||||
return d.remoteStack(stack, endpoint, OperationDeploy, unpackerCmdBuilderOptions{
|
||||
registries: registries,
|
||||
})
|
||||
return d.remoteStack(
|
||||
stack,
|
||||
endpoint,
|
||||
OperationDeploy,
|
||||
unpackerCmdBuilderOptions{
|
||||
registries: registries,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Undeploy a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
@@ -71,8 +83,19 @@ func (d *stackDeployer) UndeployRemoteComposeStack(stack *portainer.Stack, endpo
|
||||
}
|
||||
|
||||
// Start a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
func (d *stackDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return d.remoteStack(stack, endpoint, OperationComposeStart, unpackerCmdBuilderOptions{})
|
||||
func (d *stackDeployer) StartRemoteComposeStack(
|
||||
stack *portainer.Stack,
|
||||
endpoint *portainer.Endpoint,
|
||||
registries []portainer.Registry,
|
||||
) error {
|
||||
return d.remoteStack(
|
||||
stack,
|
||||
endpoint,
|
||||
OperationComposeStart,
|
||||
unpackerCmdBuilderOptions{
|
||||
registries: registries,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Stop a compose stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
@@ -81,7 +104,13 @@ func (d *stackDeployer) StopRemoteComposeStack(stack *portainer.Stack, endpoint
|
||||
}
|
||||
|
||||
// Deploy a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
func (d *stackDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (d *stackDeployer) DeployRemoteSwarmStack(
|
||||
stack *portainer.Stack,
|
||||
endpoint *portainer.Endpoint,
|
||||
registries []portainer.Registry,
|
||||
prune bool,
|
||||
pullImage bool,
|
||||
) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
@@ -105,8 +134,19 @@ func (d *stackDeployer) UndeployRemoteSwarmStack(stack *portainer.Stack, endpoin
|
||||
}
|
||||
|
||||
// Start a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
func (d *stackDeployer) StartRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return d.remoteStack(stack, endpoint, OperationSwarmStart, unpackerCmdBuilderOptions{})
|
||||
func (d *stackDeployer) StartRemoteSwarmStack(
|
||||
stack *portainer.Stack,
|
||||
endpoint *portainer.Endpoint,
|
||||
registries []portainer.Registry,
|
||||
) error {
|
||||
return d.remoteStack(
|
||||
stack,
|
||||
endpoint,
|
||||
OperationSwarmStart,
|
||||
unpackerCmdBuilderOptions{
|
||||
registries: registries,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Stop a swarm stack on remote environment using a https://github.com/portainer/compose-unpacker container
|
||||
|
||||
@@ -84,7 +84,7 @@ func (config *ComposeStackDeploymentConfig) Deploy() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if stackutils.IsRelativePathStack(config.stack) {
|
||||
if stackutils.IsGitStack(config.stack) {
|
||||
return config.StackDeployer.DeployRemoteComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func (config *SwarmStackDeploymentConfig) Deploy() error {
|
||||
}
|
||||
}
|
||||
|
||||
if stackutils.IsRelativePathStack(config.stack) {
|
||||
if stackutils.IsGitStack(config.stack) {
|
||||
return config.StackDeployer.DeployRemoteSwarmStack(config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,3 @@ func SanitizeLabel(value string) string {
|
||||
func IsGitStack(stack *portainer.Stack) bool {
|
||||
return stack.GitConfig != nil && len(stack.GitConfig.URL) != 0
|
||||
}
|
||||
|
||||
// IsRelativePathStack checks if the stack is a git stack or not
|
||||
func IsRelativePathStack(stack *portainer.Stack) bool {
|
||||
// Always return false in CE
|
||||
// This function is only for code consistency with EE
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export default 'SvgrURL';
|
||||
export const ReactComponent = 'div';
|
||||
8
app/__mocks__/svg.tsx
Normal file
8
app/__mocks__/svg.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const SvgrMock = forwardRef<HTMLSpanElement>((props, ref) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<span ref={ref} {...props} />
|
||||
));
|
||||
|
||||
export default SvgrMock;
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
--orange-1: #e86925;
|
||||
|
||||
--BE-only: var(--ui-gray-6);
|
||||
--BE-only: var(--ui-warning-7);
|
||||
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
@@ -259,7 +259,8 @@
|
||||
|
||||
/* Dark Theme */
|
||||
[theme='dark'] {
|
||||
--BE-only: var(--ui-gray-6);
|
||||
--BE-only: var(--ui-blue-8);
|
||||
--bg-BE-only: rgba(225, 223, 223, 0.08);
|
||||
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
@@ -433,7 +434,6 @@
|
||||
|
||||
/* High Contrast Theme */
|
||||
[theme='highcontrast'] {
|
||||
--BE-only: var(--ui-gray-6);
|
||||
--text-log-viewer-color-json-grey: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-magenta: var(--text-log-viewer-color);
|
||||
--text-log-viewer-color-json-yellow: var(--text-log-viewer-color);
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<div class="inner-datatable">
|
||||
<table class="table-condensed table-hover nowrap-cells table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" class="w-[10%]">
|
||||
<div class="flex">
|
||||
<table-column-header
|
||||
col-title="'Status'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Status.State'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Status.State')"
|
||||
></table-column-header>
|
||||
<span class="space-left">
|
||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled"
|
||||
>Filter
|
||||
<pr-icon icon="'filter'"></pr-icon>
|
||||
</span>
|
||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled"
|
||||
>Filter
|
||||
<pr-icon icon="'check'"></pr-icon>
|
||||
</span>
|
||||
</span>
|
||||
<div class="dropdown-menu" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Filter by state </div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 22%">Task</th>
|
||||
<th>Actions</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Slot'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Slot'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Slot')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Node'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'NodeId'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'NodeId' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('NodeId')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Last Update'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Updated'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Updated')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))"
|
||||
>
|
||||
<td class="text-center">
|
||||
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a>
|
||||
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{
|
||||
item.Id
|
||||
}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<container-quick-actions
|
||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
||||
container-id="item.ContainerId"
|
||||
task-id="item.Id"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
<container-quick-actions
|
||||
ng-if="$ctrl.agentProxy && item.Container"
|
||||
container-id="item.Container.Id"
|
||||
node-name="item.Container.NodeName"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
</td>
|
||||
<td>{{ item.Slot ? item.Slot : '-' }}</td>
|
||||
<td>{{ item.NodeId | tasknodename : $ctrl.nodes }}</td>
|
||||
<td>{{ item.Updated | getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-muted text-center">No task matching filter.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
angular.module('portainer.docker').component('serviceTasksDatatable', {
|
||||
templateUrl: './serviceTasksDatatable.html',
|
||||
controller: 'ServiceTasksDatatableController',
|
||||
bindings: {
|
||||
dataset: '<',
|
||||
serviceId: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
nodes: '<',
|
||||
agentProxy: '<',
|
||||
textFilter: '=',
|
||||
showTaskLogsButton: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker').controller('ServiceTasksDatatableController', [
|
||||
'$scope',
|
||||
'$controller',
|
||||
'DatatableService',
|
||||
function ($scope, $controller, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
var ctrl = this;
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
showQuickActionStats: true,
|
||||
showQuickActionLogs: true,
|
||||
showQuickActionConsole: true,
|
||||
showQuickActionInspect: true,
|
||||
showQuickActionExec: true,
|
||||
showQuickActionAttach: false,
|
||||
});
|
||||
|
||||
this.filters = {
|
||||
state: {
|
||||
open: false,
|
||||
enabled: false,
|
||||
values: [],
|
||||
},
|
||||
};
|
||||
|
||||
this.applyFilters = function (item) {
|
||||
var filters = ctrl.filters;
|
||||
for (var i = 0; i < filters.state.values.length; i++) {
|
||||
var filter = filters.state.values[i];
|
||||
if (item.Status.State === filter.label && filter.display) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.onStateFilterChange = function () {
|
||||
var filters = this.filters.state.values;
|
||||
var filtered = false;
|
||||
for (var i = 0; i < filters.length; i++) {
|
||||
var filter = filters[i];
|
||||
if (!filter.display) {
|
||||
filtered = true;
|
||||
}
|
||||
}
|
||||
this.filters.state.enabled = filtered;
|
||||
};
|
||||
|
||||
this.prepareTableFromDataset = function () {
|
||||
var availableStateFilters = [];
|
||||
for (var i = 0; i < this.dataset.length; i++) {
|
||||
var item = this.dataset[i];
|
||||
availableStateFilters.push({ label: item.Status.State, display: true });
|
||||
}
|
||||
this.filters.state.values = _.uniqBy(availableStateFilters, 'label');
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
|
||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
this.state.textFilter = textFilter;
|
||||
this.onTextFilterChange();
|
||||
}
|
||||
|
||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
if (storedFilters !== null) {
|
||||
this.filters = storedFilters;
|
||||
}
|
||||
if (this.filters && this.filters.state) {
|
||||
this.filters.state.open = false;
|
||||
}
|
||||
|
||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
};
|
||||
},
|
||||
]);
|
||||
@@ -231,16 +231,7 @@
|
||||
<tr dir-paginate-end ng-show="item.Expanded">
|
||||
<td></td>
|
||||
<td colspan="8">
|
||||
<service-tasks-datatable
|
||||
dataset="item.Tasks"
|
||||
service-id="item.Id"
|
||||
table-key="service-tasks"
|
||||
order-by="Status.State"
|
||||
nodes="$ctrl.nodes"
|
||||
agent-proxy="$ctrl.agentProxy"
|
||||
show-task-logs-button="$ctrl.showTaskLogsButton"
|
||||
text-filter="$ctrl.state.textFilter"
|
||||
></service-tasks-datatable>
|
||||
<docker-service-tasks-datatable dataset="item.Tasks" search="$ctrl.state.textFilter"></docker-service-tasks-datatable>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
||||
@@ -89,13 +89,7 @@
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<container-quick-actions
|
||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
||||
container-id="item.ContainerId"
|
||||
task-id="item.Id"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
<task-table-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" task-id="item.Id" state="$ctrl.state"></task-table-quick-actions>
|
||||
<container-quick-actions
|
||||
ng-if="$ctrl.agentProxy && item.Container"
|
||||
container-id="item.Container.Id"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
import { joinCommand, trimSHA } from './utils';
|
||||
import { joinCommand, taskStatusBadge, trimSHA } from './utils';
|
||||
|
||||
function includeString(text, values) {
|
||||
return values.some(function (val) {
|
||||
@@ -49,22 +49,7 @@ angular
|
||||
})
|
||||
.filter('taskstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
var labelStyle = 'default';
|
||||
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) {
|
||||
labelStyle = 'info';
|
||||
} else if (includeString(status, ['pending'])) {
|
||||
labelStyle = 'warning';
|
||||
} else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) {
|
||||
labelStyle = 'danger';
|
||||
} else if (includeString(status, ['complete'])) {
|
||||
labelStyle = 'primary';
|
||||
} else if (includeString(status, ['running'])) {
|
||||
labelStyle = 'success';
|
||||
}
|
||||
return labelStyle;
|
||||
};
|
||||
return taskStatusBadge;
|
||||
})
|
||||
.filter('taskhaslogs', function () {
|
||||
'use strict';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { TaskState } from 'docker-types/generated/1.41';
|
||||
|
||||
export function trimSHA(imageName: string) {
|
||||
if (!imageName) {
|
||||
@@ -17,3 +18,38 @@ export function joinCommand(command: null | Array<string> = []) {
|
||||
|
||||
return command.join(' ');
|
||||
}
|
||||
|
||||
export function taskStatusBadge(text?: TaskState) {
|
||||
const status = _.toLower(text);
|
||||
if (
|
||||
[
|
||||
'new',
|
||||
'allocated',
|
||||
'assigned',
|
||||
'accepted',
|
||||
'preparing',
|
||||
'ready',
|
||||
'starting',
|
||||
'remove',
|
||||
].includes(status)
|
||||
) {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if (['pending'].includes(status)) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
if (['shutdown', 'failed', 'rejected', 'orphaned'].includes(status)) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
if (['complete'].includes(status)) {
|
||||
return 'primary';
|
||||
}
|
||||
|
||||
if (['running'].includes(status)) {
|
||||
return 'success';
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ angular.module('portainer.docker').factory('ConfigHelper', [
|
||||
return {
|
||||
Id: config.ConfigID,
|
||||
Name: config.ConfigName,
|
||||
...(config.File ? { FileName: config.File.Name, Uid: config.File.UID, Gid: config.File.GID, Mode: config.File.Mode } : {}),
|
||||
credSpec: !!config.Runtime,
|
||||
FileName: config.File.Name,
|
||||
Uid: config.File.UID,
|
||||
Gid: config.File.GID,
|
||||
Mode: config.File.Mode,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
@@ -18,15 +20,12 @@ angular.module('portainer.docker').factory('ConfigHelper', [
|
||||
return {
|
||||
ConfigID: config.Id,
|
||||
ConfigName: config.Name,
|
||||
File: config.credSpec
|
||||
? null
|
||||
: {
|
||||
Name: config.FileName || config.Name,
|
||||
UID: config.Uid || '0',
|
||||
GID: config.Gid || '0',
|
||||
Mode: config.Mode || 292,
|
||||
},
|
||||
Runtime: config.credSpec ? {} : null,
|
||||
File: {
|
||||
Name: config.FileName || config.Name,
|
||||
UID: config.Uid || '0',
|
||||
GID: config.Gid || '0',
|
||||
Mode: config.Mode || 292,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export function TaskViewModel(data) {
|
||||
this.Id = data.ID;
|
||||
this.Created = data.CreatedAt;
|
||||
this.Updated = data.UpdatedAt;
|
||||
this.Slot = data.Slot;
|
||||
this.Spec = data.Spec;
|
||||
this.Status = data.Status;
|
||||
this.DesiredState = data.DesiredState;
|
||||
this.ServiceId = data.ServiceID;
|
||||
this.NodeId = data.NodeID;
|
||||
if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) {
|
||||
this.ContainerId = data.Status.ContainerStatus.ContainerID;
|
||||
}
|
||||
}
|
||||
36
app/docker/models/task.ts
Normal file
36
app/docker/models/task.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41';
|
||||
|
||||
export class TaskViewModel {
|
||||
Id: string;
|
||||
|
||||
Created: string;
|
||||
|
||||
Updated: string;
|
||||
|
||||
Slot: number;
|
||||
|
||||
Spec?: TaskSpec;
|
||||
|
||||
Status: Task['Status'];
|
||||
|
||||
DesiredState: TaskState;
|
||||
|
||||
ServiceId: string;
|
||||
|
||||
NodeId: string;
|
||||
|
||||
ContainerId: string = '';
|
||||
|
||||
constructor(data: Task) {
|
||||
this.Id = data.ID || '';
|
||||
this.Created = data.CreatedAt || '';
|
||||
this.Updated = data.UpdatedAt || '';
|
||||
this.Slot = data.Slot || 0;
|
||||
this.Spec = data.Spec;
|
||||
this.Status = data.Status;
|
||||
this.DesiredState = data.DesiredState || 'pending';
|
||||
this.ServiceId = data.ServiceID || '';
|
||||
this.NodeId = data.NodeID || '';
|
||||
this.ContainerId = data.Status?.ContainerStatus?.ContainerID || '';
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
||||
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
|
||||
import { InsightsBox } from '@/react/components/InsightsBox';
|
||||
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
|
||||
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
|
||||
@@ -22,8 +21,10 @@ import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatab
|
||||
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
|
||||
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
||||
|
||||
import { servicesModule } from './services';
|
||||
|
||||
const ngModule = angular
|
||||
.module('portainer.docker.react.components', [])
|
||||
.module('portainer.docker.react.components', [servicesModule])
|
||||
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
||||
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
||||
.component(
|
||||
@@ -33,7 +34,6 @@ const ngModule = angular
|
||||
'nodeName',
|
||||
'state',
|
||||
'status',
|
||||
'taskId',
|
||||
])
|
||||
)
|
||||
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||
@@ -71,7 +71,6 @@ const ngModule = angular
|
||||
])
|
||||
)
|
||||
.component('betaAlert', r2a(BetaAlert, ['className', 'message', 'isHtml']))
|
||||
.component('gpusInsights', r2a(GpusInsights, []))
|
||||
.component(
|
||||
'dockerImagesDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(ImagesDatatable)), [
|
||||
|
||||
21
app/docker/react/components/services.ts
Normal file
21
app/docker/react/components/services.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { TasksDatatable } from '@/react/docker/services/ListView/ServicesDatatable/TasksDatatable';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions';
|
||||
|
||||
export const servicesModule = angular
|
||||
.module('portainer.docker.react.components.services', [])
|
||||
.component(
|
||||
'dockerServiceTasksDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(TasksDatatable)), ['dataset', 'search'])
|
||||
)
|
||||
.component(
|
||||
'dockerTaskTableQuickActions',
|
||||
r2a(withUIRouter(withCurrentUser(TaskTableQuickActions)), [
|
||||
'state',
|
||||
'taskId',
|
||||
])
|
||||
).name;
|
||||
@@ -66,6 +66,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||
}
|
||||
|
||||
const params = {
|
||||
token: LocalStorage.getJWT(),
|
||||
endpointId: $state.params.endpointId,
|
||||
id: attachId,
|
||||
};
|
||||
@@ -106,6 +107,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||
ContainerService.createExec(execConfig)
|
||||
.then(function success(data) {
|
||||
const params = {
|
||||
token: LocalStorage.getJWT(),
|
||||
endpointId: $state.params.endpointId,
|
||||
id: data.Id,
|
||||
};
|
||||
@@ -164,9 +166,6 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||
if ($transition$.params().nodeName) {
|
||||
url += '&nodeName=' + $transition$.params().nodeName;
|
||||
}
|
||||
|
||||
url += '&token=' + LocalStorage.getJWT();
|
||||
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
|
||||
Add a config:
|
||||
<select class="form-control !h-[30px] !text-[13px]" ng-options="config.Name for config in filterConfigs(configs) | orderBy: 'Name'" ng-model="newConfig">
|
||||
<select class="form-control !h-[30px] !text-[13px]" ng-options="config.Name for config in configs | orderBy: 'Name'" ng-model="newConfig">
|
||||
<option selected disabled hidden value="">Select a config</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'"></pr-icon> add config </a>
|
||||
@@ -22,10 +22,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="config in service.ServiceConfigs">
|
||||
<td
|
||||
><a ui-sref="docker.configs.config({id: config.Id})">{{ config.Name }}</a></td
|
||||
>
|
||||
<td>
|
||||
<a ui-sref="docker.configs.config({id: config.Id})">{{ config.Name }}</a>
|
||||
</td>
|
||||
<td ng-if="!config.credSpec">
|
||||
<input
|
||||
class="form-control"
|
||||
ng-model="config.FileName"
|
||||
@@ -33,13 +33,11 @@
|
||||
placeholder="e.g. /path/in/container"
|
||||
required
|
||||
disable-authorization="DockerServiceUpdate"
|
||||
ng-disabled="config.credSpec"
|
||||
/>
|
||||
</td>
|
||||
<td ng-if="!config.credSpec">{{ config.Uid }}</td>
|
||||
<td ng-if="!config.credSpec">{{ config.Gid }}</td>
|
||||
<td ng-if="!config.credSpec">{{ config.Mode }}</td>
|
||||
<td ng-if="config.credSpec" colspan="4">Credential Spec</td>
|
||||
<td>{{ config.Uid }}</td>
|
||||
<td>{{ config.Gid }}</td>
|
||||
<td>{{ config.Mode }}</td>
|
||||
<td authorization="DockerServiceUpdate">
|
||||
<button class="btn btn-dangerlight pull-right" type="button" ng-click="removeConfig(service, $index)" ng-disabled="isUpdating">
|
||||
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
|
||||
|
||||
@@ -91,7 +91,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
endpoint
|
||||
) {
|
||||
$scope.resourceType = ResourceControlType.Service;
|
||||
$scope.WebhookExists = false;
|
||||
|
||||
$scope.onUpdateResourceControlSuccess = function () {
|
||||
$state.reload();
|
||||
@@ -463,27 +462,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
|
||||
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
|
||||
config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : [];
|
||||
|
||||
// support removal and (future) editing of credential specs
|
||||
const credSpec = service.ServiceConfigs.find((config) => config.credSpec);
|
||||
const credSpecId = credSpec ? credSpec.Id : '';
|
||||
const oldCredSpecId =
|
||||
(config.TaskTemplate.ContainerSpec.Privileges &&
|
||||
config.TaskTemplate.ContainerSpec.Privileges.CredentialSpec &&
|
||||
config.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config) ||
|
||||
'';
|
||||
if (oldCredSpecId && !credSpecId) {
|
||||
delete config.TaskTemplate.ContainerSpec.Privileges.CredentialSpec;
|
||||
} else if (oldCredSpecId !== credSpec) {
|
||||
config.TaskTemplate.ContainerSpec.Privileges = {
|
||||
...(config.TaskTemplate.ContainerSpec.Privileges || {}),
|
||||
CredentialSpec: {
|
||||
...((config.TaskTemplate.ContainerSpec.Privileges && config.TaskTemplate.ContainerSpec.Privileges.CredentialSpec) || {}),
|
||||
Config: credSpec,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : [];
|
||||
|
||||
if (service.Mode === 'replicated') {
|
||||
@@ -604,7 +582,8 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
}
|
||||
|
||||
$scope.updateService = function updateService(service) {
|
||||
const config = buildChanges(service);
|
||||
let config = {};
|
||||
service, (config = buildChanges(service));
|
||||
ServiceService.update(service, config).then(
|
||||
function (data) {
|
||||
if (data.message && data.message.match(/^rpc error:/)) {
|
||||
@@ -756,6 +735,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.availableNetworks = data.availableNetworks;
|
||||
$scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm');
|
||||
$scope.WebhookExists = false;
|
||||
|
||||
const serviceNetworks = _.uniqBy(_.concat($scope.service.Model.Spec.Networks || [], $scope.service.Model.Spec.TaskTemplate.Networks || []), 'Target');
|
||||
const networks = _.filter(
|
||||
@@ -852,11 +832,6 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
return networks.filter((network) => !network.Ingress && (network.Id === current.Id || $scope.service.Networks.every((serviceNetwork) => network.Id !== serviceNetwork.Id)));
|
||||
}
|
||||
|
||||
$scope.filterConfigs = filterConfigs;
|
||||
function filterConfigs(configs) {
|
||||
return configs.filter((config) => $scope.service.ServiceConfigs.every((serviceConfig) => config.Id !== serviceConfig.Id));
|
||||
}
|
||||
|
||||
function updateServiceArray(service, name) {
|
||||
previousServiceValues.push(name);
|
||||
service.hasChanges = true;
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle flex">
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="'server'"></pr-icon>
|
||||
</div>
|
||||
<span>
|
||||
{{ $ctrl.titleText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<pr-icon icon="'search'" class="vertical-center" size="'sm'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput ml-1"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search for an application..."
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div ng-if="$ctrl.refreshCallback" class="settings">
|
||||
<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><pr-icon icon="'more-vertical'"></pr-icon></span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Table settings </div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox">
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
<option value="120">2min</option>
|
||||
<option value="300">5min</option>
|
||||
</select>
|
||||
<span>
|
||||
<pr-icon id="refreshRateChange" style="display: none" icon="'check'" mode="'success'"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table-hover nowrap-cells table" data-cy="k8sAppDetail-containerTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-if="!$ctrl.isPod">
|
||||
<table-column-header
|
||||
col-title="'Pod'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'PodName'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'PodName' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('PodName')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Name'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Name'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Name')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Image'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Image'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Image')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Image Pull Policy'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'ImagePullPolicy'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'ImagePullPolicy' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('ImagePullPolicy')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Status'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Status'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Status')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Node'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Node'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Node')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Pod IP'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'IP'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('IP')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Creation date'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'CreationDate'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('CreationDate')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
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 ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td title="{{ item.Image }}">{{ item.Image | truncate : 64 }}</td>
|
||||
<td>{{ item.ImagePullPolicy }}</td>
|
||||
<td
|
||||
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
|
||||
>
|
||||
<td>
|
||||
<span ng-if="item.Node">
|
||||
<a ui-sref="kubernetes.cluster.node({ name: item.Node })">
|
||||
{{ item.Node }}
|
||||
</a>
|
||||
</span>
|
||||
<span ng-if="!item.Node">-</span>
|
||||
</td>
|
||||
<td>{{ item.PodIP }}</td>
|
||||
<td>{{ item.CreationDate | getisodate }}</td>
|
||||
<td>
|
||||
<a
|
||||
ng-if="item.Status === 'Running' && $ctrl.useServerMetrics"
|
||||
ui-sref="kubernetes.applications.application.stats({ pod: item.PodName, container: item.Name })"
|
||||
class="vertical-center mr-1"
|
||||
>
|
||||
<pr-icon icon="'bar-chart'"></pr-icon>Stats
|
||||
</a>
|
||||
<a ui-sref="kubernetes.applications.application.logs({ pod: item.PodName, container: item.Name })" class="vertical-center mr-1">
|
||||
<pr-icon icon="'file-text'"></pr-icon>Logs
|
||||
</a>
|
||||
<a
|
||||
ng-if="item.Status === 'Running'"
|
||||
ui-sref="kubernetes.applications.application.console({ pod: item.PodName, container: item.Name })"
|
||||
class="vertical-center mr-1"
|
||||
>
|
||||
<pr-icon icon="'terminal'"></pr-icon>Console
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="7" class="text-muted text-center">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="7" class="text-muted text-center">No pod available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px"> Items per page </span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesContainersDatatable', {
|
||||
templateUrl: './containersDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
isPod: '<',
|
||||
useServerMetrics: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
<div>
|
||||
<web-editor-form
|
||||
identifier="application-details-yaml"
|
||||
value="$ctrl.data"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your manifest here"
|
||||
read-only="true"
|
||||
hide-title="true"
|
||||
height="{{ $ctrl.expanded ? '800px' : '500px' }}"
|
||||
>
|
||||
</web-editor-form>
|
||||
<div class="py-5">
|
||||
<span class="btn btn-light btn-sm" ng-click="$ctrl.copyYAML()">
|
||||
<pr-icon class="vertical-center" icon="'copy'"></pr-icon>
|
||||
Copy to clipboard
|
||||
</span>
|
||||
<span class="btn btn-light btn-sm space-left !ml-0" ng-click="$ctrl.toggleYAMLInspectorExpansion()">
|
||||
<pr-icon class="vertical-center" icon="'minus'" size="'sm'" ng-if="$ctrl.expanded"></pr-icon>
|
||||
<pr-icon class="vertical-center" icon="'plus'" size="'sm'" ng-if="!$ctrl.expanded"></pr-icon>
|
||||
{{ $ctrl.expanded ? 'Collapse' : 'Expand' }}
|
||||
</span>
|
||||
<span id="copyNotificationYAML" style="display: none" class="small vertical-center ml-1">
|
||||
<pr-icon class="vertical-center" icon="'check'" size="'md'" mode="'success'"></pr-icon> copied
|
||||
</span>
|
||||
|
||||
<be-teaser-button
|
||||
class="float-right"
|
||||
feature-id="$ctrl.limitedFeature"
|
||||
message="'Applies any changes that you make in the YAML editor by calling the Kubernetes API to patch the relevant resources. Any resource removals or unexpected resource additions that you make in the YAML will be ignored. Note that editing is disabled for resources in namespaces marked as system.'"
|
||||
heading="'Apply YAML changes'"
|
||||
button-text="'Apply changes'"
|
||||
></be-teaser-button>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user