Compare commits
253 Commits
2.19.0
...
release/2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ac29b498 | ||
|
|
e73b7fe0fd | ||
|
|
e761a00098 | ||
|
|
9041880bdb | ||
|
|
e4ddd8048a | ||
|
|
e6ef913bb1 | ||
|
|
3fd696d6b5 | ||
|
|
24c9959ca4 | ||
|
|
e72671e4ab | ||
|
|
47c9e498f9 | ||
|
|
0c323b48e7 | ||
|
|
103d908e63 | ||
|
|
2972022523 | ||
|
|
4ffeefd267 | ||
|
|
c8bdf21d07 | ||
|
|
b6f9777bbf | ||
|
|
f6b78312f4 | ||
|
|
948486df77 | ||
|
|
600c8a3025 | ||
|
|
8daf77c3b6 | ||
|
|
8bb5129be0 | ||
|
|
1140804fe9 | ||
|
|
7d868d1dc9 | ||
|
|
247f358b94 | ||
|
|
f10356641a | ||
|
|
9e60723e4d | ||
|
|
47fa1626c6 | ||
|
|
26036c05f2 | ||
|
|
8ee718f808 | ||
|
|
30e4b3e68c | ||
|
|
0d56504268 | ||
|
|
6a5f44b5ba | ||
|
|
3964852fda | ||
|
|
403fdf7ce3 | ||
|
|
afa3e7477b | ||
|
|
d9effb3597 | ||
|
|
ee80e3d252 | ||
|
|
824706e4e9 | ||
|
|
09f9c09706 | ||
|
|
577eef5de0 | ||
|
|
ae1726cece | ||
|
|
e4e66dac9c | ||
|
|
08fdebfbd9 | ||
|
|
860890046d | ||
|
|
96ead31a8d | ||
|
|
6c55cac52a | ||
|
|
e110856003 | ||
|
|
10c3ed42f0 | ||
|
|
14129632a3 | ||
|
|
1fa63f6ab7 | ||
|
|
70455320be | ||
|
|
b933bee95e | ||
|
|
0dc1805881 | ||
|
|
6b5c24faff | ||
|
|
1ad9488ca7 | ||
|
|
d970f0e2bc | ||
|
|
bc0050a7b4 | ||
|
|
03155685ab | ||
|
|
7e4d113fda | ||
|
|
7c91780eb7 | ||
|
|
877dc1e236 | ||
|
|
56f3bd8417 | ||
|
|
776be2e022 | ||
|
|
0e47f22c0a | ||
|
|
b346fd7f39 | ||
|
|
35448c7f48 | ||
|
|
07ec2ffe5e | ||
|
|
bcb3f918d1 | ||
|
|
7840e0bfe1 | ||
|
|
44d66cc633 | ||
|
|
148bd4d997 | ||
|
|
7c4c985247 | ||
|
|
57c45838d5 | ||
|
|
5a73605df2 | ||
|
|
ff5b311eee | ||
|
|
7218eb0892 | ||
|
|
31bcba96c6 | ||
|
|
6a5f5aa424 | ||
|
|
da5a4d6714 | ||
|
|
35dfde70de | ||
|
|
9e57530bde | ||
|
|
5c37ed328f | ||
|
|
8e1417b4e9 | ||
|
|
b80fcb0467 | ||
|
|
66ca73f98b | ||
|
|
a0dbabcc5f | ||
|
|
b468070945 | ||
|
|
9885694df6 | ||
|
|
95f3cf6e5b | ||
|
|
da346cba60 | ||
|
|
5f9687a361 | ||
|
|
20823a7f27 | ||
|
|
9bf2957ea7 | ||
|
|
b895e88075 | ||
|
|
671f74ce0d | ||
|
|
56ab19433a | ||
|
|
9440aa733d | ||
|
|
ada6b31f69 | ||
|
|
d678b155ba | ||
|
|
99625cd35f | ||
|
|
95ca1d396b | ||
|
|
e28322459a | ||
|
|
3ff2f64930 | ||
|
|
702391cf88 | ||
|
|
d437cde046 | ||
|
|
7acde18930 | ||
|
|
b4b44e6fa4 | ||
|
|
2dfa4a7c45 | ||
|
|
3d19c46326 | ||
|
|
57e04c3544 | ||
|
|
9dde610da3 | ||
|
|
26cb75def9 | ||
|
|
3c4660bbf3 | ||
|
|
13c48ab961 | ||
|
|
ffac83864d | ||
|
|
ec091efe3b | ||
|
|
fb7a2fbbe6 | ||
|
|
dfce48cd5e | ||
|
|
2b47b84e5e | ||
|
|
e92f067e42 | ||
|
|
16ccf5871e | ||
|
|
54112b56f2 | ||
|
|
a66942aa5a | ||
|
|
c18504d6f1 | ||
|
|
25d5e62f5c | ||
|
|
a5f60c64ef | ||
|
|
d6d532473e | ||
|
|
af7834174a | ||
|
|
14853f6da0 | ||
|
|
cc37ccfe4d | ||
|
|
e3a4b7ad17 | ||
|
|
0a02f6b02e | ||
|
|
dcdf5e1837 | ||
|
|
bf85a8861d | ||
|
|
fbdbd277f7 | ||
|
|
0a80f4dc51 | ||
|
|
5a0cb4d0e8 | ||
|
|
f17da30d31 | ||
|
|
291625959b | ||
|
|
4c16594a25 | ||
|
|
60477ae287 | ||
|
|
09aa1d35a8 | ||
|
|
7669a3c8c6 | ||
|
|
dde4b95426 | ||
|
|
dfd415c62e | ||
|
|
b40b305e63 | ||
|
|
c8a1f0fa77 | ||
|
|
c3d266931f | ||
|
|
c47a804c97 | ||
|
|
b15812a74d | ||
|
|
776f6a62c3 | ||
|
|
ae3e612a24 | ||
|
|
6a8ff7c076 | ||
|
|
4a39122415 | ||
|
|
c748385879 | ||
|
|
e83aa4d88d | ||
|
|
91d2132264 | ||
|
|
e5f7641e46 | ||
|
|
515b02813b | ||
|
|
0f1e77a6d5 | ||
|
|
a02f9f1f07 | ||
|
|
d75a8027a5 | ||
|
|
6a08bbe7e9 | ||
|
|
e82b34b775 | ||
|
|
f7366d9788 | ||
|
|
46e73ee524 | ||
|
|
e5880b3e34 | ||
|
|
0e2eb17220 | ||
|
|
cb7377ead6 | ||
|
|
440f4e8dda | ||
|
|
490e4ec655 | ||
|
|
7be8619ab7 | ||
|
|
4a6b7e2654 | ||
|
|
8cc5e0796c | ||
|
|
090fa4aeb3 | ||
|
|
9a234204fa | ||
|
|
4560a53317 | ||
|
|
1b0fd60115 | ||
|
|
cd3c6e3089 | ||
|
|
4654978567 | ||
|
|
6d203033c1 | ||
|
|
4ca45e89c5 | ||
|
|
a8c6bd8082 | ||
|
|
841ca1ebd4 | ||
|
|
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 |
@@ -23,6 +23,8 @@ parserOptions:
|
||||
modules: true
|
||||
|
||||
rules:
|
||||
no-console: error
|
||||
no-alert: error
|
||||
no-control-regex: 'off'
|
||||
no-empty: warn
|
||||
no-empty-function: warn
|
||||
@@ -86,8 +88,8 @@ overrides:
|
||||
no-plusplus: off
|
||||
func-style: [error, 'declaration']
|
||||
import/prefer-default-export: off
|
||||
no-use-before-define: "off"
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }]
|
||||
no-use-before-define: 'off'
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, 'allowNamedExports': true }]
|
||||
no-shadow: 'off'
|
||||
'@typescript-eslint/no-shadow': off
|
||||
jsx-a11y/no-autofocus: warn
|
||||
|
||||
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.19.1'
|
||||
- '2.19.0'
|
||||
- '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'
|
||||
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.
|
||||
|
||||
148
.github/workflows/ci.yaml
vendored
Normal file
148
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- '!release/*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'release/*'
|
||||
- 'feat/*'
|
||||
- 'fix/*'
|
||||
- 'refactor/*'
|
||||
|
||||
env:
|
||||
DOCKER_HUB_REPO: portainerci/portainer
|
||||
NODE_ENV: testing
|
||||
GO_VERSION: 1.21.3
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
build_images:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { platform: linux, arch: amd64 }
|
||||
- { platform: linux, arch: arm64 }
|
||||
- { platform: windows, arch: amd64, version: 1809 }
|
||||
- { platform: windows, arch: amd64, version: ltsc2022 }
|
||||
runs-on: arc-runner-set
|
||||
steps:
|
||||
- name: '[preparation] checkout the current branch'
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: '[preparation] set up golang'
|
||||
uses: actions/setup-go@v4.0.1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
- name: '[preparation] cache paths'
|
||||
id: cache-dir-path
|
||||
run: |
|
||||
echo "yarn-cache-dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-build-dir=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
|
||||
echo "go-mod-dir=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
|
||||
- name: '[preparation] cache go'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ steps.cache-dir-path.outputs.go-build-dir }}
|
||||
${{ steps.cache-dir-path.outputs.go-mod-dir }}
|
||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-
|
||||
enableCrossOsArchive: true
|
||||
- name: '[preparation] set up node.js'
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: ''
|
||||
- name: '[preparation] cache yarn'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
${{ steps.cache-dir-path.outputs.yarn-cache-dir }}
|
||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-
|
||||
enableCrossOsArchive: true
|
||||
- name: '[preparation] set up qemu'
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: '[preparation] set up docker context for buildx'
|
||||
run: docker context create builders
|
||||
- name: '[preparation] set up docker buildx'
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
endpoint: builders
|
||||
- name: '[preparation] docker login'
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: '[preparation] set the container image tag'
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||
else
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}"
|
||||
else
|
||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}-${{ matrix.config.arch }}"
|
||||
fi
|
||||
|
||||
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}" >> $GITHUB_ENV
|
||||
- name: '[execution] build linux & windows portainer binaries'
|
||||
run: |
|
||||
export YARN_VERSION=$(yarn --version)
|
||||
export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}')
|
||||
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
|
||||
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
|
||||
env:
|
||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||
- name: '[execution] build and push docker images'
|
||||
run: |
|
||||
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
||||
mv dist/portainer dist/portainer.exe
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||
else
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
||||
fi
|
||||
env:
|
||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||
build_manifests:
|
||||
runs-on: arc-runner-set
|
||||
needs: [build_images]
|
||||
steps:
|
||||
- name: '[preparation] docker login'
|
||||
uses: docker/login-action@v2.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: '[preparation] set up docker context for buildx'
|
||||
run: docker version && docker context create builders
|
||||
- name: '[preparation] set up docker buildx'
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
endpoint: builders
|
||||
- name: '[execution] build and push manifests'
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||
else
|
||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||
fi
|
||||
|
||||
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
|
||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
|
||||
7
.github/workflows/lint.yml
vendored
7
.github/workflows/lint.yml
vendored
@@ -12,6 +12,9 @@ on:
|
||||
- develop
|
||||
- release/*
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
@@ -25,7 +28,7 @@ jobs:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
@@ -41,6 +44,6 @@ jobs:
|
||||
- name: GolangCI-Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.52.2
|
||||
version: v1.54.1
|
||||
working-directory: api
|
||||
args: --timeout=10m -c .golangci.yaml
|
||||
|
||||
93
.github/workflows/nightly-security-scan.yml
vendored
93
.github/workflows/nightly-security-scan.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
- cron: '0 20 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
@@ -25,7 +28,7 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
@@ -41,7 +44,7 @@ jobs:
|
||||
name: html-js-result-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: analyse vulnerabilities
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
@@ -58,10 +61,10 @@ jobs:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: install Go
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
@@ -72,9 +75,9 @@ jobs:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
@@ -102,35 +105,68 @@ jobs:
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
image-trivy: ${{ steps.set-trivy-matrix.outputs.image_trivy_result }}
|
||||
image-docker-scout: ${{ steps.set-docker-scout-matrix.outputs.image_docker_scout_result }}
|
||||
steps:
|
||||
- name: scan vulnerabilities by Trivy
|
||||
- name: scan vulnerabilities by Trivy
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
|
||||
|
||||
- name: upload image security scan result as artifact
|
||||
- name: upload Trivy image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
- name: develop Trivy scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-trivy-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: upload html file as Trivy artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-result.html
|
||||
path: image-trivy-result.html
|
||||
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
- name: analyse vulnerabilities from Trivy
|
||||
id: set-trivy-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "image_trivy_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: scan vulnerabilities by Docker Scout
|
||||
uses: docker/scout-action@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: cves
|
||||
image: portainerci/portainer:develop
|
||||
sarif-file: image-docker-scout.json
|
||||
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: upload Docker Scout image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-develop-result
|
||||
path: image-docker-scout.json
|
||||
|
||||
- name: develop Docker Scout scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
|
||||
|
||||
- name: upload html file as Docker Scout artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-docker-scout-result.html
|
||||
|
||||
- name: analyse vulnerabilities from Docker Scout
|
||||
id: set-docker-scout-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=matrix)
|
||||
echo "image_docker_scout_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
result-analysis:
|
||||
name: Analyse Scan Results
|
||||
@@ -142,22 +178,26 @@ jobs:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||
image-trivy: ${{fromJson(needs.image-vulnerability.outputs.image-trivy)}}
|
||||
image-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.image-docker-scout)}}
|
||||
steps:
|
||||
- name: display the results of js, Go, and image scan
|
||||
run: |
|
||||
echo "${{ matrix.js.status }}"
|
||||
echo "${{ matrix.go.status }}"
|
||||
echo "${{ matrix.image.status }}"
|
||||
echo "${{ matrix.image-trivy.status }}"
|
||||
echo "${{ matrix.image-docker-scout.status }}"
|
||||
echo "${{ matrix.js.summary }}"
|
||||
echo "${{ matrix.go.summary }}"
|
||||
echo "${{ matrix.image.summary }}"
|
||||
echo "${{ matrix.image-trivy.summary }}"
|
||||
echo "${{ matrix.image-docker-scout.summary }}"
|
||||
|
||||
- name: send message to Slack
|
||||
if: >-
|
||||
- name: send message to Slack
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
matrix.image-trivy.status == 'failure' ||
|
||||
matrix.image-docker-scout.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
with:
|
||||
payload: |
|
||||
@@ -193,7 +233,14 @@ jobs:
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n"
|
||||
"text": "*Image Trivy vulnerability check*: *${{ matrix.image-trivy.status }}*\n${{ matrix.image-trivy.summary }}\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Image Docker Scout vulnerability check*: *${{ matrix.image-docker-scout.status }}*\n${{ matrix.image-docker-scout.summary }}\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
104
.github/workflows/pr-security.yml
vendored
104
.github/workflows/pr-security.yml
vendored
@@ -7,13 +7,16 @@ on:
|
||||
- edited
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'api/go.mod'
|
||||
- 'gruntfile.js'
|
||||
- 'go.mod'
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
- '.github/workflows/pr-security.yml'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
@@ -84,7 +87,7 @@ jobs:
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
@@ -95,7 +98,7 @@ jobs:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: upload scan result as pull-request artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -138,20 +141,21 @@ jobs:
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }}
|
||||
imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }}
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: install Go 1.19.5
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: install Node.js 18.x
|
||||
- name: install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
@@ -167,26 +171,26 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
tags: local-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/local-portainer-image.tar
|
||||
|
||||
- name: load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
docker load --input /tmp/local-portainer-image.tar
|
||||
|
||||
- name: scan vulnerabilities by Trivy
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress local-portainer:${{ github.sha }}
|
||||
|
||||
- name: upload image security scan result as artifact
|
||||
- name: upload Trivy image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-feature-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
- name: download Trivy artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -198,21 +202,65 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
- name: pr vs develop Trivy scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-trivy-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: upload html file as Trivy artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-result.html
|
||||
path: image-trivy-result.html
|
||||
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
- name: analyse different vulnerabilities against develop branch by Trivy
|
||||
id: set-diff-trivy-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
|
||||
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "image_diff_trivy_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: scan vulnerabilities by Docker Scout
|
||||
uses: docker/scout-action@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: cves
|
||||
image: local-portainer:${{ github.sha }}
|
||||
sarif-file: image-docker-scout.json
|
||||
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: upload Docker Scout image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-feature-result
|
||||
path: image-docker-scout.json
|
||||
|
||||
- name: download Docker Scout artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mv ./image-docker-scout.json ./image-docker-scout-feature.json
|
||||
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
|
||||
if [[ -e ./image-docker-scout.json ]]; then
|
||||
mv ./image-docker-scout.json ./image-docker-scout-develop.json
|
||||
else
|
||||
echo "null" > ./image-docker-scout-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop Docker Scout scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
|
||||
|
||||
- name: upload html file as Docker Scout artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-docker-scout-result.html
|
||||
|
||||
- name: analyse different vulnerabilities against develop branch by Docker Scout
|
||||
id: set-diff-docker-scout-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=matrix)
|
||||
echo "image_diff_docker_scout_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
result-analysis:
|
||||
name: Analyse Scan Result Against develop Branch
|
||||
@@ -225,18 +273,22 @@ jobs:
|
||||
matrix:
|
||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
||||
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
||||
imagediff-trivy: ${{fromJson(needs.image-vulnerability.outputs.imagediff-trivy)}}
|
||||
imagediff-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.imagediff-docker-scout)}}
|
||||
steps:
|
||||
- name: check job status of diff result
|
||||
if: >-
|
||||
matrix.jsdiff.status == 'failure' ||
|
||||
matrix.godiff.status == 'failure' ||
|
||||
matrix.imagediff.status == 'failure'
|
||||
matrix.imagediff-trivy.status == 'failure' ||
|
||||
matrix.imagediff-docker-scout.status == 'failure'
|
||||
run: |
|
||||
echo "${{ matrix.jsdiff.status }}"
|
||||
echo "${{ matrix.godiff.status }}"
|
||||
echo "${{ matrix.imagediff.status }}"
|
||||
echo "${{ matrix.imagediff-trivy.status }}"
|
||||
echo "${{ matrix.imagediff-docker-scout.status }}"
|
||||
echo "${{ matrix.jsdiff.summary }}"
|
||||
echo "${{ matrix.godiff.summary }}"
|
||||
echo "${{ matrix.imagediff.summary }}"
|
||||
echo "${{ matrix.imagediff-trivy.summary }}"
|
||||
echo "${{ matrix.imagediff-docker-scout.summary }}"
|
||||
exit 1
|
||||
|
||||
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 }}
|
||||
|
||||
|
||||
19
.github/workflows/test.yaml
vendored
19
.github/workflows/test.yaml
vendored
@@ -1,5 +1,11 @@
|
||||
name: Test
|
||||
|
||||
on: push
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
test-client:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -8,18 +14,25 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn jest --maxWorkers=2
|
||||
run: make test-client ARGS="--maxWorkers=2"
|
||||
test-server:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { platform: linux, arch: amd64 }
|
||||
- { platform: linux, arch: arm64 }
|
||||
- { platform: windows, arch: amd64, version: 1809 }
|
||||
- { platform: windows, arch: amd64, version: ltsc2022 }
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Run tests
|
||||
run: make test-server
|
||||
|
||||
8
.github/workflows/validate-openapi-spec.yaml
vendored
8
.github/workflows/validate-openapi-spec.yaml
vendored
@@ -7,6 +7,10 @@ on:
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.21.3
|
||||
NODE_VERSION: 18.x
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,13 +19,13 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Download golang modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
|
||||
12
.prettierrc
12
.prettierrc
@@ -2,18 +2,24 @@
|
||||
"printWidth": 180,
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.{j,t}sx", "*.ts"],
|
||||
"files": [
|
||||
"*.{j,t}sx",
|
||||
"*.ts"
|
||||
],
|
||||
"options": {
|
||||
"printWidth": 80
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
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,23 @@ linters:
|
||||
- exportloopref
|
||||
linters-settings:
|
||||
depguard:
|
||||
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'
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: 'encoding/json'
|
||||
desc: 'use github.com/segmentio/encoding/json'
|
||||
- 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'
|
||||
- pkg: 'github.com/portainer/libcrypto'
|
||||
desc: 'use github.com/portainer/portainer/pkg/libcrypto'
|
||||
- pkg: 'github.com/portainer/libhttp'
|
||||
desc: 'use github.com/portainer/portainer/pkg/libhttp'
|
||||
files:
|
||||
- '!**/*_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
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
@@ -18,13 +15,3 @@ type APIKeyService interface {
|
||||
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
||||
InvalidateUserKeyCache(userId portainer.UserID) bool
|
||||
}
|
||||
|
||||
// generateRandomKey generates a random key of specified length
|
||||
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
|
||||
func generateRandomKey(length int) []byte {
|
||||
k := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||
return nil
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package apikey
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/securecookie"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -33,7 +34,7 @@ func Test_generateRandomKey(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := generateRandomKey(tt.wantLenth)
|
||||
got := securecookie.GenerateRandomKey(tt.wantLenth)
|
||||
is.Equal(tt.wantLenth, len(got))
|
||||
})
|
||||
}
|
||||
@@ -41,7 +42,7 @@ func Test_generateRandomKey(t *testing.T) {
|
||||
t.Run("Generated keys are unique", func(t *testing.T) {
|
||||
keys := make(map[string]bool)
|
||||
for i := 0; i < 100; i++ {
|
||||
key := generateRandomKey(8)
|
||||
key := securecookie.GenerateRandomKey(8)
|
||||
_, ok := keys[string(key)]
|
||||
is.False(ok)
|
||||
keys[string(key)] = true
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/securecookie"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -39,7 +40,7 @@ func (a *apiKeyService) HashRaw(rawKey string) []byte {
|
||||
// GenerateApiKey generates a raw API key for a user (for one-time display).
|
||||
// The generated API key is stored in the cache and database.
|
||||
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
|
||||
randKey := generateRandomKey(32)
|
||||
randKey := securecookie.GenerateRandomKey(32)
|
||||
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
||||
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ 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,10 +75,11 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("max_alive_minutes", maxAlive.Minutes()).
|
||||
Msg("start")
|
||||
Msg("KeepTunnelAlive: start")
|
||||
|
||||
maxAliveTicker := time.NewTicker(maxAlive)
|
||||
defer maxAliveTicker.Stop()
|
||||
|
||||
pingTicker := time.NewTicker(tunnelCleanupInterval)
|
||||
defer pingTicker.Stop()
|
||||
|
||||
@@ -91,13 +92,13 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("ping agent")
|
||||
Msg("KeepTunnelAlive: ping agent")
|
||||
}
|
||||
case <-maxAliveTicker.C:
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("timeout_minutes", maxAlive.Minutes()).
|
||||
Msg("tunnel keep alive timeout")
|
||||
Msg("KeepTunnelAlive: tunnel keep alive timeout")
|
||||
|
||||
return
|
||||
case <-ctx.Done():
|
||||
@@ -105,7 +106,7 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("tunnel stop")
|
||||
Msg("KeepTunnelAlive: tunnel stop")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -126,8 +127,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)
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
"github.com/portainer/portainer/pkg/libcrypto"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
@@ -42,6 +43,7 @@ import (
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/pendingactions"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
@@ -119,11 +121,15 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
log.Fatal().Err(err).Msg("failed generating instance id")
|
||||
}
|
||||
|
||||
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
|
||||
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
|
||||
|
||||
// from MigrateData
|
||||
v := models.Version{
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
Edition: int(portainer.PortainerCE),
|
||||
InstanceID: instanceId.String(),
|
||||
MigratorCount: migratorCount,
|
||||
}
|
||||
store.VersionService.UpdateVersion(&v)
|
||||
|
||||
@@ -152,6 +158,16 @@ 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 {
|
||||
@@ -248,11 +264,12 @@ func initSnapshotService(
|
||||
dockerClientFactory *dockerclient.ClientFactory,
|
||||
kubernetesClientFactory *kubecli.ClientFactory,
|
||||
shutdownCtx context.Context,
|
||||
pendingActionsService *pendingactions.PendingActionsService,
|
||||
) (portainer.SnapshotService, error) {
|
||||
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
|
||||
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
|
||||
|
||||
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
|
||||
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx, pendingActionsService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -383,6 +400,11 @@ 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")
|
||||
@@ -434,15 +456,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||
authorizationService := authorization.NewService(dataStore)
|
||||
authorizationService.K8sClientFactory = kubernetesClientFactory
|
||||
|
||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
||||
}
|
||||
snapshotService.Start()
|
||||
|
||||
authorizationService := authorization.NewService(dataStore)
|
||||
authorizationService.K8sClientFactory = kubernetesClientFactory
|
||||
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
|
||||
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
||||
@@ -602,6 +626,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
DemoService: demoService,
|
||||
UpgradeService: upgradeService,
|
||||
AdminCreationDone: adminCreationDone,
|
||||
PendingActionsService: pendingActionsService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
"github.com/portainer/portainer/pkg/libcrypto"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -255,7 +255,7 @@ func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte,
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
err := connection.UnmarshalObjectWithJsoniter(data, object)
|
||||
err := connection.UnmarshalObject(data, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
|
||||
|
||||
// MarshalObject encodes an object to binary format
|
||||
func (connection *DbConnection) MarshalObject(object interface{}) (data []byte, err error) {
|
||||
func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Special case for the VERSION bucket. Here we're not using json
|
||||
if v, ok := object.(string); ok {
|
||||
data = []byte(v)
|
||||
buf.WriteString(v)
|
||||
} else {
|
||||
data, err = json.Marshal(object)
|
||||
if err != nil {
|
||||
return data, err
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetSortMapKeys(false)
|
||||
enc.SetAppendNewline(false)
|
||||
|
||||
if err := enc.Encode(object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if connection.getEncryptionKey() == nil {
|
||||
return data, nil
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
return encrypt(data, connection.getEncryptionKey())
|
||||
|
||||
return encrypt(buf.Bytes(), connection.getEncryptionKey())
|
||||
}
|
||||
|
||||
// UnmarshalObject decodes an object from binary data
|
||||
@@ -54,31 +61,6 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object interface{})
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
|
||||
// decoding at the moment.
|
||||
func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||
if connection.getEncryptionKey() != nil {
|
||||
var err error
|
||||
data, err = decrypt(data, connection.getEncryptionKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err := jsoni.Unmarshal(data, &object)
|
||||
if err != nil {
|
||||
if s, ok := object.(*string); ok {
|
||||
*s = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mmm, don't have a KMS .... aes GCM seems the most likely from
|
||||
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interfa
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
return tx.conn.UnmarshalObjectWithJsoniter(value, object)
|
||||
return tx.conn.UnmarshalObject(value, object)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||
@@ -134,7 +134,7 @@ func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{},
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
err := tx.conn.UnmarshalObject(v, obj)
|
||||
if err == nil {
|
||||
obj, err = appendFn(obj)
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte
|
||||
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
|
||||
|
||||
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
err := tx.conn.UnmarshalObject(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.
|
||||
@@ -144,6 +145,23 @@ 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,6 +122,23 @@ 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)
|
||||
|
||||
@@ -5,10 +5,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoint_groups"
|
||||
)
|
||||
const BucketName = "endpoint_groups"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
|
||||
@@ -35,6 +35,7 @@ type (
|
||||
User() UserService
|
||||
Version() VersionService
|
||||
Webhook() WebhookService
|
||||
PendingActions() PendingActionsService
|
||||
}
|
||||
|
||||
DataStore interface {
|
||||
@@ -72,6 +73,11 @@ type (
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
PendingActionsService interface {
|
||||
BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
// EdgeStackService represents a service to manage Edge stacks
|
||||
EdgeStackService interface {
|
||||
EdgeStacks() ([]portainer.EdgeStack, error)
|
||||
@@ -89,6 +95,7 @@ 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)
|
||||
|
||||
74
api/dataservices/pendingactions/pendingactions.go
Normal file
74
api/dataservices/pendingactions/pendingactions.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package pendingactions
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
BucketName = "pending_actions"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]
|
||||
}
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]
|
||||
}
|
||||
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s Service) Create(config *portainer.PendingActions) error {
|
||||
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Create(config)
|
||||
})
|
||||
}
|
||||
|
||||
func (s Service) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
|
||||
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Update(ID, config)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s ServiceTx) Create(config *portainer.PendingActions) error {
|
||||
return s.Tx.CreateObject(BucketName, func(id uint64) (int, interface{}) {
|
||||
config.ID = portainer.PendingActionsID(id)
|
||||
config.CreatedAt = time.Now().Unix()
|
||||
|
||||
return int(config.ID), config
|
||||
})
|
||||
}
|
||||
|
||||
func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
|
||||
return s.BaseDataServiceTx.Update(ID, config)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a custom template.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -5,9 +5,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
BucketName = "snapshots"
|
||||
)
|
||||
const BucketName = "snapshots"
|
||||
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Snapshot, portainer.EndpointID]
|
||||
|
||||
@@ -106,7 +106,6 @@ func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// RefreshableStacks returns stacks that are configured for a periodic update
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "tags"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "tags"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
|
||||
@@ -22,7 +22,7 @@ func (service ServiceTx) Create(tag *portainer.Tag) error {
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateTagFunc is a no-op inside a transaction
|
||||
// UpdateTagFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ func (service *Service) IsUpdating() (bool, error) {
|
||||
|
||||
// StoreIsUpdating store the database updating status.
|
||||
func (service *Service) StoreIsUpdating(isUpdating bool) error {
|
||||
if isUpdating {
|
||||
return service.connection.UpdateObject(BucketName, []byte(updatingKey), isUpdating)
|
||||
}
|
||||
|
||||
return service.connection.DeleteObject(BucketName, []byte(updatingKey))
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ func (store *Store) MigrateData() error {
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Msg("migration failed, restoring database to previous version")
|
||||
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore 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")
|
||||
}
|
||||
|
||||
log.Info().Msg("database restored to previous version")
|
||||
|
||||
@@ -2,7 +2,6 @@ package datastore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -11,10 +10,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
portainer "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(portaineree.PortainerCE)
|
||||
edition := int(portainer.PortainerCE)
|
||||
instanceId := ""
|
||||
|
||||
// If we already have a version key, we don't need to migrate
|
||||
|
||||
@@ -115,10 +115,16 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Ok {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRunning,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
statusArray = append(statusArray,
|
||||
portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusDeploymentReceived,
|
||||
Time: time.Now().Unix(),
|
||||
},
|
||||
portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRunning,
|
||||
Time: time.Now().Unix(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if environmentStatus.Details.ImagesPulled {
|
||||
|
||||
@@ -245,7 +245,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.VolumeListOKBody, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.ListResponse, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
volumes := volumesData.Volumes
|
||||
for _, volume := range volumes {
|
||||
volumeName := volume.Name
|
||||
|
||||
@@ -148,6 +148,17 @@ func (m *Migrator) LatestMigrations() Migrations {
|
||||
return m.migrations[len(m.migrations)-1]
|
||||
}
|
||||
|
||||
func (m *Migrator) GetMigratorCountOfCurrentAPIVersion() int {
|
||||
migratorCount := 0
|
||||
latestMigrations := m.LatestMigrations()
|
||||
|
||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||
migratorCount = len(latestMigrations.MigrationFuncs)
|
||||
}
|
||||
|
||||
return migratorCount
|
||||
}
|
||||
|
||||
// !NOTE: Migration funtions should ideally be idempotent.
|
||||
// ! Which simply means the function can run over the same data many times but only transform it once.
|
||||
// ! In practice this really just means an extra check or two to ensure we're not destroying valid data.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/extension"
|
||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
||||
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
|
||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||
"github.com/portainer/portainer/api/dataservices/registry"
|
||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/dataservices/role"
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/webhook"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
@@ -72,6 +73,7 @@ type Store struct {
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
PendingActionsService *pendingactions.Service
|
||||
}
|
||||
|
||||
func (store *Store) initServices() error {
|
||||
@@ -238,9 +240,20 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.ScheduleService = scheduleService
|
||||
|
||||
pendingActionsService, err := pendingactions.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.PendingActionsService = pendingActionsService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PendingActions gives access to the PendingActions data management layer
|
||||
func (store *Store) PendingActions() dataservices.PendingActionsService {
|
||||
return store.PendingActionsService
|
||||
}
|
||||
|
||||
// CustomTemplate gives access to the CustomTemplate data management layer
|
||||
func (store *Store) CustomTemplate() dataservices.CustomTemplateService {
|
||||
return store.CustomTemplateService
|
||||
|
||||
@@ -16,6 +16,8 @@ func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
|
||||
|
||||
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
|
||||
|
||||
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService { return nil }
|
||||
|
||||
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
|
||||
return tx.store.EdgeGroupService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
@@ -46,12 +46,10 @@
|
||||
},
|
||||
"EdgeCheckinInterval": 0,
|
||||
"EdgeKey": "",
|
||||
"EnableGPUManagement": false,
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
"Kubernetes": {
|
||||
"Configuration": {
|
||||
"AllowNoneIngressClass": false,
|
||||
@@ -101,8 +99,7 @@
|
||||
"TeamAccessPolicies": {},
|
||||
"Type": 1,
|
||||
"URL": "unix:///var/run/docker.sock",
|
||||
"UserAccessPolicies": {},
|
||||
"UserTrusted": false
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
],
|
||||
"registries": [
|
||||
@@ -124,8 +121,7 @@
|
||||
"Name": "canister.io",
|
||||
"Password": "MjWbx8A6YK7cw7",
|
||||
"Quay": {
|
||||
"OrganisationName": "",
|
||||
"UseOrganisation": false
|
||||
"OrganisationName": ""
|
||||
},
|
||||
"RegistryAccesses": {
|
||||
"1": {
|
||||
@@ -584,11 +580,8 @@
|
||||
"AllowHostNamespaceForRegularUsers": true,
|
||||
"AllowPrivilegedModeForRegularUsers": true,
|
||||
"AllowStackManagementForRegularUsers": true,
|
||||
"AllowVolumeBrowserForRegularUsers": false,
|
||||
"AuthenticationMethod": 1,
|
||||
"BlackListedLabels": [],
|
||||
"DisplayDonationHeader": false,
|
||||
"DisplayExternalContributors": false,
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
@@ -598,15 +591,16 @@
|
||||
"EdgeAgentCheckinInterval": 5,
|
||||
"EdgePortainerUrl": "",
|
||||
"EnableEdgeComputeFeatures": false,
|
||||
"EnableHostManagementFeatures": false,
|
||||
"EnableTelemetry": true,
|
||||
"EnforceEdgeID": false,
|
||||
"FeatureFlagSettings": null,
|
||||
"GlobalDeploymentOptions": {
|
||||
"hideStacksFunctionality": false
|
||||
},
|
||||
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
||||
"InternalAuthSettings": {
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"IsDockerDesktopExtension": false,
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell",
|
||||
"LDAPSettings": {
|
||||
@@ -909,7 +903,6 @@
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
"Username": "admin"
|
||||
},
|
||||
{
|
||||
@@ -939,11 +932,10 @@
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
"Username": "prabhat"
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":3,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -57,20 +57,20 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
|
||||
func CreateClientFromEnv() (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.FromEnv,
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
|
||||
func CreateSimpleClient() (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*cli
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHTTPClient(httpCli),
|
||||
)
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpointURL),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
@@ -144,7 +144,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package images
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -9,7 +10,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
consts "github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/patrickmn/go-cache"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
@@ -174,7 +174,12 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
if !snapshot.Swarm {
|
||||
return err
|
||||
} else {
|
||||
log.Info().Str("container", container.ID).Err(err).Msg("unable to inspect container in other Swarm nodes")
|
||||
if !strings.Contains(err.Error(), "No such container") {
|
||||
return err
|
||||
}
|
||||
// It is common to have containers running on different Swarm nodes,
|
||||
// so we just log the error in the debug level
|
||||
log.Debug().Str("container", container.ID).Err(err).Msg("unable to inspect container in other Swarm nodes")
|
||||
}
|
||||
} else {
|
||||
var gpuOptions *_container.DeviceRequest = nil
|
||||
@@ -240,7 +245,7 @@ func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) erro
|
||||
}
|
||||
|
||||
func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
|
||||
volumes, err := cli.VolumeList(context.Background(), filters.Args{})
|
||||
volumes, err := cli.VolumeList(context.Background(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,48 +3,3 @@ package exec
|
||||
import "regexp"
|
||||
|
||||
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
||||
|
||||
type StringSet map[string]bool
|
||||
|
||||
func NewStringSet() StringSet {
|
||||
return make(StringSet)
|
||||
}
|
||||
|
||||
func (s StringSet) Add(x string) {
|
||||
s[x] = true
|
||||
}
|
||||
|
||||
func (s StringSet) Remove(x string) {
|
||||
if s.Contains(x) {
|
||||
delete(s, x)
|
||||
}
|
||||
}
|
||||
|
||||
func (s StringSet) Contains(x string) bool {
|
||||
_, ok := s[x]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s StringSet) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s StringSet) List() []string {
|
||||
list := make([]string, s.Len())
|
||||
|
||||
i := 0
|
||||
for k := range s {
|
||||
list[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (s StringSet) Union(x StringSet) {
|
||||
if x.Len() != 0 {
|
||||
for k := range x {
|
||||
s.Add(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,6 +14,9 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
@@ -64,16 +66,35 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
if registry.Authentication {
|
||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to validate registry token. Skip logging with this registry.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to get effective credential. Skip logging with this registry.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to login.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package filesystem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -302,6 +302,38 @@ 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)
|
||||
|
||||
@@ -49,8 +49,8 @@ func FilterDirForEntryFile(dirEntries []DirEntry, entryFile string) []DirEntry {
|
||||
return filteredDirEntries
|
||||
}
|
||||
|
||||
// FilterDirForCompatibility returns the content of the entry file if agent version is less than 2.19.0
|
||||
func FilterDirForCompatibility(dirEntries []DirEntry, entryFilePath, agentVersion string) (string, error) {
|
||||
|
||||
if semver.Compare(fmt.Sprintf("v%s", agentVersion), "v2.19.0") == -1 {
|
||||
for _, dirEntry := range dirEntries {
|
||||
if dirEntry.IsFile {
|
||||
|
||||
@@ -9,6 +9,39 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type MultiFilterArgs []struct {
|
||||
FilterKey string
|
||||
FilterType portainer.PerDevConfigsFilterType
|
||||
}
|
||||
|
||||
// MultiFilterDirForPerDevConfigs filers the given dirEntries with multiple filter args, returns the merged entries for the given device
|
||||
func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs) []DirEntry {
|
||||
var filteredDirEntries []DirEntry
|
||||
|
||||
for _, multiFilterArg := range multiFilterArgs {
|
||||
tmp := FilterDirForPerDevConfigs(dirEntries, multiFilterArg.FilterKey, configPath, multiFilterArg.FilterType)
|
||||
filteredDirEntries = append(filteredDirEntries, tmp...)
|
||||
}
|
||||
|
||||
return deduplicate(filteredDirEntries)
|
||||
}
|
||||
|
||||
func deduplicate(dirEntries []DirEntry) []DirEntry {
|
||||
var deduplicatedDirEntries []DirEntry
|
||||
|
||||
marks := make(map[string]struct{})
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
_, ok := marks[dirEntry.Name]
|
||||
if !ok {
|
||||
marks[dirEntry.Name] = struct{}{}
|
||||
deduplicatedDirEntries = append(deduplicatedDirEntries, dirEntry)
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicatedDirEntries
|
||||
}
|
||||
|
||||
// FilterDirForPerDevConfigs filers the given dirEntries, returns entries for the given device
|
||||
// For given configPath A/B/C, return entries:
|
||||
// 1. all entries outside of dir A
|
||||
@@ -47,10 +80,14 @@ func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filter
|
||||
return shouldIncludeFile(dirEntry, deviceName, configPath)
|
||||
}
|
||||
|
||||
// Include:
|
||||
// dir entry A/B/C/<deviceName>
|
||||
// all entries A/B/C/<deviceName>/*
|
||||
return shouldIncludeDir(dirEntry, deviceName, configPath)
|
||||
if filterType == portainer.PerDevConfigsTypeDir {
|
||||
// Include:
|
||||
// dir entry A/B/C/<deviceName>
|
||||
// all entries A/B/C/<deviceName>/*
|
||||
return shouldIncludeDir(dirEntry, deviceName, configPath)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isInConfigRootDir(dirEntry DirEntry, configPath string) bool {
|
||||
|
||||
91
api/filesystem/serialize_per_dev_configs_test.go
Normal file
91
api/filesystem/serialize_per_dev_configs_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
|
||||
type args struct {
|
||||
dirEntries []DirEntry
|
||||
configPath string
|
||||
multiFilterArgs MultiFilterArgs
|
||||
}
|
||||
|
||||
baseDirEntries := []DirEntry{
|
||||
{".env", "", true, 420},
|
||||
{"docker-compose.yaml", "", true, 420},
|
||||
{"configs", "", false, 420},
|
||||
{"configs/file1.conf", "", true, 420},
|
||||
{"configs/file2.conf", "", true, 420},
|
||||
{"configs/folder1", "", false, 420},
|
||||
{"configs/folder1/config1", "", true, 420},
|
||||
{"configs/folder2", "", false, 420},
|
||||
{"configs/folder2/config2", "", true, 420},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []DirEntry
|
||||
}{
|
||||
{
|
||||
name: "filter file1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]},
|
||||
},
|
||||
{
|
||||
name: "filter folder1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
},
|
||||
{
|
||||
name: "filter file1 and folder1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
},
|
||||
{
|
||||
name: "filter file1 and file2",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"file1", portainer.PerDevConfigsTypeFile},
|
||||
{"file2", portainer.PerDevConfigsTypeFile},
|
||||
},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
|
||||
},
|
||||
{
|
||||
name: "filter folder1 and folder2",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"folder1", portainer.PerDevConfigsTypeDir},
|
||||
{"folder2", portainer.PerDevConfigsTypeDir},
|
||||
},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, MultiFilterDirForPerDevConfigs(tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs), "MultiFilterDirForPerDevConfigs(%v, %v, %v)", tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,12 +10,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -2,12 +2,13 @@ package openamt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type authenticationResponse struct {
|
||||
|
||||
@@ -2,7 +2,6 @@ package openamt
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,6 +10,8 @@ import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type CIRAConfig struct {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type ActionResponse struct {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
func (service *Service) enableDeviceFeatures(configuration portainer.OpenAMTConfiguration, deviceGUID string, features portainer.OpenAMTDeviceEnabledFeatures) error {
|
||||
|
||||
@@ -2,7 +2,6 @@ package openamt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
var errInvalidResponseStatus = errors.New("invalid response status (expecting 200)")
|
||||
|
||||
@@ -3,7 +3,7 @@ package errors
|
||||
import (
|
||||
"errors"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
func TxResponse(err error, validResponse func() *httperror.HandlerError) *httperror.HandlerError {
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
@@ -74,7 +74,7 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal ||
|
||||
settings.AuthenticationMethod == portainer.AuthenticationOAuth ||
|
||||
(settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: httperrors.ErrUnauthorized}
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +83,14 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Only initial admin is allowed to login without oauth", Err: httperrors.ErrUnauthorized}
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Only initial admin is allowed to login without oauth", httperrors.ErrUnauthorized)
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
return handler.authenticateLDAP(rw, user, payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Login method is not supported", Err: httperrors.ErrUnauthorized}
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Login method is not supported", httperrors.ErrUnauthorized)
|
||||
}
|
||||
|
||||
func isUserInitialAdmin(user *portainer.User) bool {
|
||||
@@ -100,7 +100,7 @@ func isUserInitialAdmin(user *portainer.User) bool {
|
||||
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
|
||||
err := handler.CryptoService.CompareHashAndData(user.Password, password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusUnprocessableEntity, Message: "Invalid credentials", Err: httperrors.ErrUnauthorized}
|
||||
return httperror.NewError(http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized)
|
||||
}
|
||||
|
||||
forceChangePassword := !handler.passwordStrengthChecker.Check(password)
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
@@ -3,12 +3,12 @@ package auth
|
||||
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/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -24,6 +24,7 @@ type Handler struct {
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
passwordStrengthChecker security.PasswordStrengthChecker
|
||||
bouncer security.BouncerService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
@@ -31,6 +32,7 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
bouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
@@ -38,7 +40,6 @@ 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.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -3,27 +3,28 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/logoutcontext"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: public
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
// @success 204 "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /auth/logout [post]
|
||||
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
operations "github.com/portainer/portainer/api/backup"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
operations "github.com/portainer/portainer/api/backup"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
)
|
||||
|
||||
type restorePayload struct {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -472,3 +474,29 @@ 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
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package customtemplates
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type fileResponse struct {
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -20,6 +19,8 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id CustomTemplateInspect
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id CustomTemplateList
|
||||
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
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"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||
@@ -32,6 +33,7 @@ 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,15 +2,16 @@ package containers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
type containerGpusResponse struct {
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
||||
@@ -3,14 +3,14 @@ package containers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker/consts"
|
||||
"github.com/portainer/portainer/api/docker/images"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/containers"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/images"
|
||||
"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"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
|
||||
@@ -45,6 +46,9 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||
|
||||
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dataStore, dockerClientFactory, containerService)
|
||||
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
|
||||
|
||||
imagesHandler := images.NewHandler("/{id}/images", bouncer, dockerClientFactory)
|
||||
endpointRouter.PathPrefix("/images").Handler(imagesHandler)
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
32
api/http/handler/docker/images/handler.go
Normal file
32
api/http/handler/docker/images/handler.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
dockerClientFactory *client.ClientFactory
|
||||
bouncer security.BouncerService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||
func NewHandler(routePrefix string, bouncer security.BouncerService, dockerClientFactory *client.ClientFactory) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
bouncer: bouncer,
|
||||
}
|
||||
|
||||
router := h.PathPrefix(routePrefix).Subrouter()
|
||||
router.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
router.Handle("", httperror.LoggerHandler(h.imagesList)).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
79
api/http/handler/docker/images/images_list.go
Normal file
79
api/http/handler/docker/images/images_list.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/utils"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type ImageResponse struct {
|
||||
Created int64 `json:"created"`
|
||||
NodeName string `json:"nodeName"`
|
||||
ID string `json:"id"`
|
||||
Size int64 `json:"size"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
// Used is true if the image is used by at least one container
|
||||
// supplied only when withUsage is true
|
||||
Used bool `json:"used"`
|
||||
}
|
||||
|
||||
// @id dockerImagesList
|
||||
// @summary Fetch images
|
||||
// @description
|
||||
// @description **Access policy**:
|
||||
// @tags docker
|
||||
// @security jwt
|
||||
// @param environmentId path int true "Environment identifier"
|
||||
// @param withUsage query boolean false "Include image usage information"
|
||||
// @produce json
|
||||
// @success 200 {array} ImageResponse "Success"
|
||||
// @failure 400 "Bad request"
|
||||
// @failure 500 "Internal server error"
|
||||
// @router /docker/{environmentId}/images [get]
|
||||
func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
cli, httpErr := utils.GetClient(r, handler.dockerClientFactory)
|
||||
if httpErr != nil {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
images, err := cli.ImageList(r.Context(), types.ImageListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker images", err)
|
||||
}
|
||||
|
||||
withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: withUsage", err)
|
||||
}
|
||||
|
||||
imageUsageSet := set.Set[string]{}
|
||||
if withUsage {
|
||||
containers, err := cli.ContainerList(r.Context(), types.ContainerListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
imageUsageSet.Add(container.ImageID)
|
||||
}
|
||||
}
|
||||
|
||||
imagesList := make([]ImageResponse, len(images))
|
||||
for i, image := range images {
|
||||
imagesList[i] = ImageResponse{
|
||||
Created: image.Created,
|
||||
ID: image.ID,
|
||||
Size: image.Size,
|
||||
Tags: image.RepoTags,
|
||||
Used: imageUsageSet.Contains(image.ID),
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, imagesList)
|
||||
}
|
||||
28
api/http/handler/docker/utils/get_client.go
Normal file
28
api/http/handler/docker/utils/get_client.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
prclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
// GetClient returns a Docker client based on the request context
|
||||
func GetClient(r *http.Request, dockerClientFactory *prclient.ClientFactory) (*dockerclient.Client, *httperror.HandlerError) {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return nil, httperror.NotFound("Unable to find an environment on request context", err)
|
||||
}
|
||||
|
||||
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
cli, err := dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to connect to the Docker daemon", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
@@ -49,6 +49,24 @@ func GetEndpointsByTags(tx dataservices.DataStoreTx, tagIDs []portainer.TagID, p
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func getTrustedEndpoints(tx dataservices.DataStoreTx, endpointIDs []portainer.EndpointID) ([]portainer.EndpointID, error) {
|
||||
results := []portainer.EndpointID{}
|
||||
for _, endpointID := range endpointIDs {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !endpoint.UserTrusted {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, endpoint.ID)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
|
||||
groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
@@ -4,12 +4,11 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EdgeGroupDelete
|
||||
@@ -29,14 +28,9 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid Edge group identifier route variable", err)
|
||||
}
|
||||
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
err = deleteEdgeGroup(handler.DataStore, portainer.EdgeGroupID(edgeGroupID))
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return deleteEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
})
|
||||
}
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return deleteEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
})
|
||||
if err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
@@ -65,7 +59,7 @@ func deleteEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) erro
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for _, groupID := range edgeStack.EdgeGroups {
|
||||
if groupID == ID {
|
||||
return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge stack", errors.New("edge group is used by an Edge stack"))
|
||||
return httperror.Conflict("Edge group is used by an Edge stack", errors.New("edge group is used by an Edge stack"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +72,7 @@ func deleteEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) erro
|
||||
for _, edgeJob := range edgeJobs {
|
||||
for _, groupID := range edgeJob.EdgeGroups {
|
||||
if groupID == ID {
|
||||
return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge job", errors.New("edge group is used by an Edge job"))
|
||||
return httperror.Conflict("Edge group is used by an Edge job", errors.New("edge group is used by an Edge job"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package edgegroups
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
)
|
||||
|
||||
// @id EdgeGroupInspect
|
||||
@@ -29,14 +28,10 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
edgeGroup, err = getEdgeGroup(handler.DataStore, portainer.EdgeGroupID(edgeGroupID))
|
||||
} else {
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = getEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
return err
|
||||
})
|
||||
}
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = getEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
return err
|
||||
})
|
||||
|
||||
return txResponse(w, edgeGroup, err)
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ package edgegroups
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
type decoratedEdgeGroup struct {
|
||||
portainer.EdgeGroup
|
||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||
HasEdgeJob bool `json:"HasEdgeJob"`
|
||||
EndpointTypes []portainer.EndpointType
|
||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||
HasEdgeJob bool `json:"HasEdgeJob"`
|
||||
EndpointTypes []portainer.EndpointType
|
||||
TrustedEndpoints []portainer.EndpointID `json:"TrustedEndpoints"`
|
||||
}
|
||||
|
||||
// @id EdgeGroupList
|
||||
@@ -33,14 +33,10 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h
|
||||
var decoratedEdgeGroups []decoratedEdgeGroup
|
||||
var err error
|
||||
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
decoratedEdgeGroups, err = getEdgeGroupList(handler.DataStore)
|
||||
} else {
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
decoratedEdgeGroups, err = getEdgeGroupList(tx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
decoratedEdgeGroups, err = getEdgeGroupList(tx)
|
||||
return err
|
||||
})
|
||||
|
||||
return txResponse(w, decoratedEdgeGroups, err)
|
||||
}
|
||||
@@ -90,6 +86,14 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
edgeGroup.TrustedEndpoints = endpointIDs
|
||||
} else {
|
||||
trustedEndpoints, err := getTrustedEndpoints(tx, edgeGroup.Endpoints)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve environments for Edge group", err)
|
||||
}
|
||||
|
||||
edgeGroup.TrustedEndpoints = trustedEndpoints
|
||||
}
|
||||
|
||||
endpointTypes, err := getEndpointTypes(tx, edgeGroup.Endpoints)
|
||||
|
||||
@@ -3,15 +3,15 @@ package edgegroups
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"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"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user