Compare commits

..

1 Commits

Author SHA1 Message Date
xAt0mZ
f21b6c84d9 sec(app): remove unused and vulnerable dependencies 2021-02-01 17:56:17 +01:00
4209 changed files with 72507 additions and 232086 deletions

12
.babelrc Normal file
View File

@@ -0,0 +1,12 @@
{
"plugins": ["lodash", "angularjs-annotate"],
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry"
}
]
]
}

44
.codeclimate.yml Normal file
View File

@@ -0,0 +1,44 @@
version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
gofmt:
enabled: true
eslint:
enabled: true
channel: "eslint-5"
config:
config: .eslintrc.yml
exclude_patterns:
- assets/
- build/
- dist/
- distribution/
- node_modules
- test/
- webpack/
- gruntfile.js
- webpack.config.js
- api/
- "!app/kubernetes/**"
- .github/
- .tmp/

View File

@@ -1,5 +1,3 @@
*
!dist
!build
!metadata.json
!docker-extension/build

View File

@@ -1 +0,0 @@
PORTAINER_EDITION=CE

View File

@@ -9,8 +9,6 @@ globals:
extends:
- 'eslint:recommended'
- 'plugin:storybook/recommended'
- 'plugin:import/typescript'
- prettier
plugins:
@@ -19,130 +17,12 @@ plugins:
parserOptions:
ecmaVersion: 2018
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
modules: true
rules:
no-console: error
no-alert: error
no-control-regex: 'off'
no-control-regex: off
no-empty: warn
no-empty-function: warn
no-useless-escape: 'off'
import/named: error
import/order:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
]
no-restricted-imports:
- error
- patterns:
- group:
- '@/react/test-utils/*'
message: 'These utils are just for test files'
settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
typescript: true
node: true
overrides:
- files:
- app/**/*.ts{,x}
parserOptions:
project: './tsconfig.json'
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
extends:
- airbnb
- airbnb-typescript
- 'plugin:eslint-comments/recommended'
- 'plugin:react-hooks/recommended'
- 'plugin:react/jsx-runtime'
- 'plugin:@typescript-eslint/recommended'
- 'plugin:@typescript-eslint/eslint-recommended'
- 'plugin:promise/recommended'
- 'plugin:storybook/recommended'
- prettier # should be last
settings:
react:
version: 'detect'
rules:
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
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-shadow: 'off'
'@typescript-eslint/no-shadow': off
jsx-a11y/no-autofocus: warn
react/forbid-prop-types: off
react/require-default-props: off
react/no-array-index-key: off
no-underscore-dangle: off
react/jsx-filename-extension: [0]
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
'@typescript-eslint/explicit-module-boundary-types': off
'@typescript-eslint/no-unused-vars': 'error'
'@typescript-eslint/no-explicit-any': 'error'
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
'@typescript-eslint/no-restricted-imports':
- error
- patterns:
- group:
- '@/react/test-utils/*'
message: 'These utils are just for test files'
overrides: # allow props spreading for hoc files
- files:
- app/**/with*.ts{,x}
rules:
'react/jsx-props-no-spreading': off
- files:
- app/**/*.test.*
extends:
- 'plugin:vitest/recommended'
env:
'vitest/env': true
rules:
'react/jsx-no-constructed-context-values': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
- files:
- app/**/*.stories.*
rules:
'no-alert': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
no-useless-escape: off
import/order: error

View File

@@ -1,8 +0,0 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
# tailwind prettier
58d66d3142950bb90a7d85511c034ac9fabba9ba

View File

@@ -1,11 +0,0 @@
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

View File

@@ -1,38 +0,0 @@
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

View File

@@ -2,7 +2,7 @@
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
If you are reporting 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
@@ -28,15 +28,17 @@ Briefly describe the problem you are having in a few paragraphs.
**Steps to reproduce the issue:**
1. 2. 3.
1.
2.
3.
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
**Technical details:**
- Portainer version:
- Target Docker version (the host/cluster you manage):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Target Swarm version (if applicable):
- Browser:
* Portainer version:
* Target Docker version (the host/cluster you manage):
* Platform (windows/linux):
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
* Target Swarm version (if applicable):
* Browser:

53
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@@ -0,0 +1,53 @@
---
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 http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**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/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
**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 9000:9000 portainer/portainer`):
- Browser:
**Additional context**
Add any other context about the problem here.

20
.github/ISSUE_TEMPLATE/Custom.md vendored Normal file
View File

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

View File

@@ -1,25 +0,0 @@
---
name: Question
about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
Before you start, we need a little bit more information from you:
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
<!--
You can find more information about Portainer support framework policy here: https://old.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/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Question**:
How can I deploy Portainer on... ?

View File

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

View File

@@ -1,33 +0,0 @@
---
name: Feature request
about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack 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/
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,167 +0,0 @@
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.4'
- '2.19.3'
- '2.19.2'
- '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

View File

@@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- 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.

View File

@@ -1,4 +0,0 @@
closes #0 <!-- Github issue number (remove if unknown) -->
closes [CE-0] <!-- Jira link number (remove if unknown). Please also add the same [CE-XXX] at the back of the PR title -->
### Changes:

54
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
# Config for Stalebot, limited to only `issues`
only: issues
# Issues config
issues:
daysUntilStale: 60
daysUntilClose: 7
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Issues with these labels will never be considered stale
exemptLabels:
- kind/enhancement
- kind/question
- kind/style
- kind/workaround
- kind/refactor
- bug/need-confirmation
- bug/confirmed
- status/discuss
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking an issue as stale
staleLabel: status/stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been marked as stale as it has not had recent activity,
it will be closed if no further activity occurs in the next 7 days.
If you believe that it has been incorrectly labelled as stale,
leave a comment and the label will be removed.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Since no further activity has appeared on this issue it will be closed.
If you believe that it has been incorrectly closed, leave a comment
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
Note - If it is an old bug report, make sure that it is reproduceable in the
latest version of Portainer as it may have already been fixed.

View File

@@ -1,176 +0,0 @@
name: ci
on:
workflow_dispatch:
push:
branches:
- 'develop'
- 'release/*'
pull_request:
branches:
- 'develop'
- 'release/*'
- 'feat/*'
- 'fix/*'
- 'refactor/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
build_images:
strategy:
matrix:
config:
- { platform: linux, arch: amd64, version: "" }
- { platform: linux, arch: arm64, version: "" }
- { platform: linux, arch: arm, version: "" }
- { platform: linux, arch: ppc64le, version: "" }
- { platform: linux, arch: s390x, version: "" }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: '[preparation] checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: '[preparation] set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
- name: '[preparation] set up node.js'
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- name: '[preparation] set up qemu'
uses: docker/setup-qemu-action@v3.0.0
- name: '[preparation] set up docker context for buildx'
run: docker context create builders
- name: '[preparation] set up docker buildx'
uses: docker/setup-buildx-action@v3.0.0
with:
endpoint: builders
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: '[preparation] set the container image tag'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
fi
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $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}
GIT_COMMIT_HASH_LONG=${{ github.sha }}
export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7}
NODE_ENV="testing"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
NODE_ENV="production"
fi
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 .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_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 "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
env:
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
build_manifests:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
needs: [build_images]
steps:
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.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@v3.0.0
with:
endpoint: builders
- name: '[execution] build and push manifests'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
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}-linux-arm" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x"
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
fi

View File

@@ -1,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 10
WAIT_MS: 60000

View File

@@ -1,55 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.55.2
args: --timeout=10m -c .golangci.yaml

View File

@@ -1,252 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
env:
GO_VERSION: 1.21.9
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- 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)
echo "js_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- 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)
echo "go_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
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
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 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 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-trivy-result")
- name: upload html file as Trivy artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-trivy-result.html
- 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_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
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
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-trivy.status }}"
echo "${{ matrix.image-docker-scout.status }}"
echo "${{ matrix.js.summary }}"
echo "${{ matrix.go.summary }}"
echo "${{ matrix.image-trivy.summary }}"
echo "${{ matrix.image-docker-scout.summary }}"
- name: send message to Slack
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image-trivy.status == 'failure' ||
matrix.image-docker-scout.status == 'failure'
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
}
}
],
"attachments": [
{
"color": "#FF0000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"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"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,298 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'go.mod'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./js-snyk-feature.json
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./js-snyk-develop.json
else
echo "null" > ./js-snyk-develop.json
fi
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
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
with:
name: go-security-scan-feature-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./go-snyk-feature.json
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./go-snyk-develop.json
else
echo "null" > ./go-snyk-develop.json
fi
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
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
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build-all
- name: set up docker buildx
uses: docker/setup-buildx-action@v2
- name: build and compress image
uses: docker/build-push-action@v4
with:
context: .
file: build/linux/Dockerfile
tags: local-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/local-portainer-image.tar
- name: load docker image
run: |
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 local-portainer:${{ github.sha }}
- 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 Trivy artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-trivy.json ./image-trivy-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-trivy.json ]]; then
mv ./image-trivy.json ./image-trivy-develop.json
else
echo "null" > ./image-trivy-develop.json
fi
- 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-trivy-result")
- 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-trivy-result.html
- 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_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
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
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-trivy.status == 'failure' ||
matrix.imagediff-docker-scout.status == 'failure'
run: |
echo "${{ matrix.jsdiff.status }}"
echo "${{ matrix.godiff.status }}"
echo "${{ matrix.imagediff-trivy.status }}"
echo "${{ matrix.imagediff-docker-scout.status }}"
echo "${{ matrix.jsdiff.summary }}"
echo "${{ matrix.godiff.summary }}"
echo "${{ matrix.imagediff-trivy.summary }}"
echo "${{ matrix.imagediff-docker-scout.summary }}"
exit 1

View File

@@ -1,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,28 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

View File

@@ -1,56 +0,0 @@
name: Test
env:
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
on:
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
push:
branches:
- master
- develop
- release/*
jobs:
test-client:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
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
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Run tests
run: make test-server

View File

@@ -1,39 +0,0 @@
name: Validate OpenAPI specs
on:
pull_request:
branches:
- master
- develop
- 'release/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.21.9
NODE_VERSION: 18.x
jobs:
openapi-spec:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
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: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

10
.gitignore vendored
View File

@@ -3,18 +3,8 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache
__debug_bin*
api/docs
.idea
.env
go.work.sum

View File

@@ -1,40 +0,0 @@
linters:
# Disable all linters, the defaults don't pass on our code yet
disable-all: true
# Enable these for now
enable:
- unused
- depguard
- gosimple
- govet
- errorlint
- exportloopref
linters-settings:
depguard:
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
issues:
exclude-rules:
- path: ./
linters:
- typecheck

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged

View File

@@ -1,2 +0,0 @@
dist
api/datastore/test_data

View File

@@ -2,24 +2,12 @@
"printWidth": 180,
"singleQuote": true,
"htmlWhitespaceSensitivity": "strict",
"trailingComma": "es5",
"overrides": [
{
"files": [
"*.html"
],
"files": ["*.html"],
"options": {
"parser": "angular"
}
},
{
"files": [
"*.{j,t}sx",
"*.ts"
],
"options": {
"printWidth": 80
}
}
]
}
}

View File

@@ -1,93 +0,0 @@
import { StorybookConfig } from '@storybook/react-webpack5';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import postcss from 'postcss';
const config: StorybookConfig = {
stories: ['../app/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-styling',
options: {
cssLoaderOptions: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
},
postCss: {
implementation: postcss,
},
},
},
],
webpackFinal: (config) => {
const rules = config?.module?.rules || [];
const imageRule = rules.find((rule) => {
const test = (rule as { test: RegExp }).test;
if (!test) {
return false;
}
return test.test('.svg');
}) as { [key: string]: any };
imageRule.exclude = /\.svg$/;
rules.unshift({
test: /\.svg$/i,
type: 'asset',
resourceQuery: {
not: [/c/],
}, // exclude react component if *.svg?url
});
rules.unshift({
test: /\.svg$/i,
issuer: /\.(js|ts)(x)?$/,
resourceQuery: /c/,
// *.svg?c
use: [
{
loader: '@svgr/webpack',
options: {
icon: true,
},
},
],
});
return {
...config,
resolve: {
...config.resolve,
plugins: [
...(config.resolve?.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve?.extensions,
}),
],
},
module: {
...config.module,
rules,
},
} satisfies Configuration;
},
staticDirs: ['./public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
},
framework: {
name: '@storybook/react-webpack5',
options: {},
},
};
export default config;

View File

@@ -1,51 +0,0 @@
import '../app/assets/css';
import React from 'react';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../app/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
initMSW(
{
onUnhandledRequest: ({ method, url }) => {
if (url.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
`);
}
},
},
handlers
);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
export const decorators = [
(Story) => (
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
),
];
export const loaders = [mswLoader];

View File

@@ -1,284 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (2.0.11).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39';
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
const activeClientIds = new Set();
self.addEventListener('install', function () {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
}
const client = await self.clients.get(clientId);
if (!client) {
return;
}
const allClients = await self.clients.matchAll({
type: 'window',
});
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
});
break;
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
});
break;
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId);
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
});
break;
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}
break;
}
}
});
self.addEventListener('fetch', function (event) {
const { request } = event;
// Bypass navigation requests.
if (request.mode === 'navigate') {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
// Generate unique request ID.
const requestId = crypto.randomUUID();
event.respondWith(handleRequest(event, requestId));
});
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const responseClone = response.clone();
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseClone.body]
);
})();
}
return response;
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (client?.frameType === 'top-level') {
return client;
}
const allClients = await self.clients.matchAll({
type: 'window',
});
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible';
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
}
async function getResponse(event, client, requestId) {
const { request } = event;
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone();
function passthrough() {
const headers = Object.fromEntries(requestClone.headers.entries());
// Remove internal MSW request header so the passthrough request
// complies with any potential CORS preflight checks on the server.
// Some servers forbid unknown request headers.
delete headers['x-msw-intention'];
return fetch(requestClone, { headers });
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough();
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough();
}
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
const mswIntention = request.headers.get('x-msw-intention');
if (['bypass', 'passthrough'].includes(mswIntention)) {
return passthrough();
}
// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer();
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
[requestBuffer]
);
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data);
}
case 'MOCK_NOT_FOUND': {
return passthrough();
}
}
return passthrough();
}
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
}
resolve(event.data);
};
client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean)));
});
}
async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error();
}
const mockedResponse = new Response(response.body, response);
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
});
return mockedResponse;
}

View File

@@ -1,19 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,
"args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"]
}
]
}

View File

@@ -1,191 +0,0 @@
{
// Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"React Named Export Component": {
"prefix": "rnec",
"body": [
"export function $TM_FILENAME_BASE() {",
" return <div>$TM_FILENAME_BASE</div>;",
"}"
],
"description": "React Named Export Component"
},
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller,",
"});",
""
]
},
"Controller": {
"scope": "javascript",
"prefix": "mycontroller",
"body": [
"class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {",
"\t/* @ngInject */",
"\tconstructor($0) {",
"\t}",
"}",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Dummy ES6+ controller"
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
"description": "Dummy ES6+ service",
"body": [
"import angular from 'angular';",
"import PortainerError from 'Portainer/error';",
"",
"class $1 {",
" /* @ngInject */",
" constructor(\\$async, $0) {",
" this.\\$async = \\$async;",
"",
" this.getAsync = this.getAsync.bind(this);",
" this.getAllAsync = this.getAllAsync.bind(this);",
" this.createAsync = this.createAsync.bind(this);",
" this.updateAsync = this.updateAsync.bind(this);",
" this.deleteAsync = this.deleteAsync.bind(this);",
" }",
"",
" /**",
" * GET",
" */",
" async getAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" async getAllAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" get() {",
" if () {",
" return this.\\$async(this.getAsync);",
" }",
" return this.\\$async(this.getAllAsync);",
" }",
"",
" /**",
" * CREATE",
" */",
" async createAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" create() {",
" return this.\\$async(this.createAsync);",
" }",
"",
" /**",
" * UPDATE",
" */",
" async updateAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" update() {",
" return this.\\$async(this.updateAsync);",
" }",
"",
" /**",
" * DELETE",
" */",
" async deleteAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" delete() {",
" return this.\\$async(this.deleteAsync);",
" }",
"}",
"",
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
},
"swagger-api-doc": {
"prefix": "swapi",
"scope": "go",
"description": "Snippet for a api doc",
"body": [
"// @id ",
"// @summary ",
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",
"// @param id path int true \"identifier\"",
"// @param body body Object true \"details\"",
"// @success 200 {object} portainer. \"Success\"",
"// @success 204 \"Success\"",
"// @failure 400 \"Invalid request\"",
"// @failure 403 \"Permission denied\"",
"// @failure 404 \" not found\"",
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

@@ -1,8 +0,0 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"],
"gopls": {
"build.expandWorkspaceToModule": false
},
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
}

162
.vscode/portainer.code-snippets vendored Normal file
View File

@@ -0,0 +1,162 @@
{
// Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller,",
"});",
""
]
},
"Controller": {
"scope": "javascript",
"prefix": "mycontroller",
"body": [
"class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {",
"\t/* @ngInject */",
"\tconstructor($0) {",
"\t}",
"}",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Dummy ES6+ controller"
},
"Model": {
"scope": "javascript",
"prefix": "mymodel",
"description": "Dummy ES6+ model",
"body": [
"/**",
" * $1 Model",
" */",
"const _$1 = Object.freeze({",
" $0",
"});",
"",
"export class $1 {",
" constructor() {",
" Object.assign(this, JSON.parse(JSON.stringify(_$1)));",
" }",
"}"
]
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
"description": "Dummy ES6+ service",
"body": [
"import angular from 'angular';",
"import PortainerError from 'Portainer/error';",
"",
"class $1 {",
" /* @ngInject */",
" constructor(\\$async, $0) {",
" this.\\$async = \\$async;",
"",
" this.getAsync = this.getAsync.bind(this);",
" this.getAllAsync = this.getAllAsync.bind(this);",
" this.createAsync = this.createAsync.bind(this);",
" this.updateAsync = this.updateAsync.bind(this);",
" this.deleteAsync = this.deleteAsync.bind(this);",
" }",
"",
" /**",
" * GET",
" */",
" async getAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" async getAllAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" get() {",
" if () {",
" return this.\\$async(this.getAsync);",
" }",
" return this.\\$async(this.getAllAsync);",
" }",
"",
" /**",
" * CREATE",
" */",
" async createAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" create() {",
" return this.\\$async(this.createAsync);",
" }",
"",
" /**",
" * UPDATE",
" */",
" async updateAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" update() {",
" return this.\\$async(this.updateAsync);",
" }",
"",
" /**",
" * DELETE",
" */",
" async deleteAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" delete() {",
" return this.\\$async(this.deleteAsync);",
" }",
"}",
"",
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
}
}

View File

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

View File

@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(containers): add exposed ports in the containers view
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
## Contribution process
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
### Bug report
@@ -75,82 +75,22 @@ The feature request process is similar to the bug report process but has an extr
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build and run Portainer locally
## Build Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies:
Install dependencies with yarn:
```sh
$ make deps
$ yarn
```
Then build and run the project in a Docker container:
Then build and run the project:
```sh
$ make dev
$ yarn start
```
Portainer server can now be accessed at <https://localhost:9443>. and UI dev server runs on <http://localhost:8999>.
Portainer can now be accessed at <http://localhost:9000>.
if you want to build the project you can run:
```sh
make build-all
```
For additional make commands, run `make help`.
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
### Build customization
You can customize the following settings:
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
## Testing your build
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
```
// @tag.name <Name of resource>
// @tag.description a short description
```
When adding a new route to an existing handler use the following as a template (you can use `swapi` snippet if you're using vscode):
```
// @id
// @summary
// @description
// @description **Access policy**:
// @tags
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "identifier"
// @param body body Object true "details"
// @success 200 {object} portainer. "Success"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 " not found"
// @failure 500 "Server error"
// @router /{id} [get]
```
explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation]
## Licensing
See the [LICENSE](https://github.com/portainer/portainer/blob/develop/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.

126
Makefile
View File

@@ -1,126 +0,0 @@
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
# For a list of valid GOOS and GOARCH values
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
PLATFORM=$(shell go env GOOS)
ARCH=$(shell go env GOARCH)
# build target, can be one of "production", "testing", "development"
ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
TAG=local
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
GOTESTSUM=go run gotest.tools/gotestsum@latest
# Don't change anything below this line unless you know what you're doing
.DEFAULT_GOAL := help
##@ Building
.PHONY: init-dist build-storybook build build-client build-server build-image devops
init-dist:
@mkdir -p dist
build-all: deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
build-client: init-dist ## Build the client
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
yarn storybook:build
devops: clean deps build-client ## Build the everything target specifically for CI
echo "Building the devops binary..."
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
##@ Build dependencies
.PHONY: deps server-deps client-deps tidy
deps: server-deps client-deps ## Download all client and server build dependancies
server-deps: init-dist ## Download dependant server binaries
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
client-deps: ## Install client dependencies
yarn
tidy: ## Tidy up the go.mod file
cd api && go mod tidy
##@ Cleanup
.PHONY: clean
clean: ## Remove all build and download artifacts
@echo "Clearing the dist directory..."
@rm -rf dist/*
##@ Testing
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-client: ## Run client tests
yarn test $(ARGS)
test-server: ## Run server tests
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
##@ Dev
.PHONY: dev dev-client dev-server
dev: ## Run both the client and server in development mode
make dev-server
make dev-client
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
##@ Format
.PHONY: format format-client format-server
format: format-client format-server ## Format all code
format-client: ## Format client code
yarn format
format-server: ## Format server code
go fmt ./...
##@ Lint
.PHONY: lint lint-client lint-server
lint: lint-client lint-server ## Lint all code
lint-client: ## Lint client code
yarn lint
lint-server: ## Lint server code
golangci-lint run --timeout=10m -c .golangci.yaml
##@ Extension
.PHONY: dev-extension
dev-extension: build-server build-client ## Run the extension in development mode
make local -f build/docker-extension/Makefile
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
yarn swagger-cli validate dist/docs/openapi.yaml
##@ Helpers
.PHONY: help
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

View File

@@ -1,65 +1,61 @@
<p align="center">
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/logo_alt.png?raw=true' />
</p>
**Portainer Community Edition** is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as simple to deploy as it is to use. The application allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a smart GUI and/or an extensive API.
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
Portainer consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
## Demo
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
- [Take3 get 3 free nodes of Portainer Business for as long as you want them](https://www.portainer.io/take-3)
- [Portainer BE install guide](https://install.portainer.io)
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
## Latest Version
Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
[![latest version](https://img.shields.io/github/v/release/portainer/portainer?color=%2344cc11&label=Latest%20release&style=for-the-badge)](https://github.com/portainer/portainer/releases/latest)
- Browse [PWD/?stack=portainer-demo/play-with-docker/docker-stack.yml](http://play-with-docker.com/?stack=https://raw.githubusercontent.com/portainer/portainer-demo/master/play-with-docker/docker-stack.yml)
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
## Getting started
- [Deploy Portainer](https://docs.portainer.io/start/install)
- [Documentation](https://docs.portainer.io)
- [Contribute to the project](https://docs.portainer.io/contribute/contribute)
## Features & Functions
View [this](https://www.portainer.io/products) table to see all of the Portainer CE functionality and compare to Portainer Business.
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://documentation.portainer.io)
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
## Getting help
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
Learn more about Portainer's community support channels [here.](https://www.portainer.io/get-support-for-portainer)
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
You can join the Portainer Community by visiting [https://www.portainer.io/join-our-community](https://www.portainer.io/join-our-community). This will give you advance notice of events, content and other related Portainer content.
- FAQ: https://documentation.portainer.io
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://docs.portainer.io/contribute/contribute) to build it locally and make a pull request.
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
## Security
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Work for us
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and/or visit our [careers page](https://portainer.io/careers).
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/privacy-policy). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
## Limitations
@@ -69,4 +65,8 @@ Portainer supports "Current - 2 docker versions only. Prior versions may operate
Portainer is licensed under the zlib license. See [LICENSE](./LICENSE) for reference.
Portainer also contains code from open source projects. See [ATTRIBUTIONS.md](./ATTRIBUTIONS.md) for a list.
Portainer also contains the following code, which is licensed under the [MIT license](https://opensource.org/licenses/MIT):
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@@ -1,120 +0,0 @@
package adminmonitor
import (
"context"
"net/http"
"strings"
"sync"
"time"
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"
)
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.RWMutex
adminInitDisabled bool
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Debug().Msg("start initialization monitor")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
return
}
if !initialized {
log.Info().Msg("the Portainer instance timed out for security purposes, to re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
}
case <-cancellationCtx.Done():
log.Debug().Msg("canceling initialization monitor")
case <-m.shutdownCtx.Done():
log.Debug().Msg("shutting down initialization monitor")
}
}()
}
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
// WasInitialized is a system initialization check
func (m *Monitor) WasInitialized() (bool, error) {
users, err := m.datastore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.adminInitDisabled
}
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
// Otherwise, it will pass through the request to next
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.WasInstanceDisabled() && strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -1,54 +0,0 @@
package adminmonitor
import (
"context"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stopWithoutStarting(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
}
func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
time.Sleep(2 * time.Second)
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor")
monitor.Stop()
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
}

View File

@@ -1,74 +0,0 @@
package agent
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil {
return 0, "", err
}
parsedURL.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return 0, "", err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, "", err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
version := resp.Header.Get(portainer.PortainerAgentHeader)
if version == "" {
return 0, "", errors.New("Version Header is missing")
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, "", errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, "", err
}
if agentPlatformNumber == 0 {
return 0, "", errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), version, nil
}

View File

@@ -1,64 +0,0 @@
Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API.
Examples are available at https://documentation.portainer.io/api/api-examples/
You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
# Authentication
Most of the API environments(endpoints) require to be authenticated as well as some level of authorization to be used.
Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request
with the **Bearer** authentication mechanism.
Example:
```
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE
```
# Security
Each API environment(endpoint) has an associated access policy, it is documented in the description of each environment(endpoint).
Different access policies are available:
- Public access
- Authenticated access
- Restricted access
- Administrator access
### Public access
No authentication is required to access the environments(endpoints) with this access policy.
### Authenticated access
Authentication is required to access the environments(endpoints) with this access policy.
### Restricted access
Authentication is required to access the environments(endpoints) with this access policy.
Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered.
### Administrator access
Authentication as well as an administrator role are required to access the environments(endpoints) with this access policy.
# Execute Docker requests
Portainer **DO NOT** expose specific environments(endpoints) to manage your Docker resources (create a container, remove a volume, etc...).
Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API.
To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(endpoint) (which is not documented below due to Swagger limitations). This environment(endpoint) has a restricted access policy so you still need to be authenticated to be able to query this environment(endpoint). Any query on this environment(endpoint) will be proxied to the Docker API of the associated environment(endpoint) (requests and responses objects are the same as documented in the Docker API).
# Private Registry
Using private registry, you will need to pass a based64 encoded JSON string {"registryId":\<registryID value\>} inside the Request Header. The parameter name is "X-Registry-Auth".
\<registryID value\> - The registry ID where the repository was created.
Example:
```
eyJyZWdpc3RyeUlkIjoxfQ==
```
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).

View File

@@ -1,17 +0,0 @@
package apikey
import (
portainer "github.com/portainer/portainer/api"
)
// APIKeyService represents a service for managing API keys.
type APIKeyService interface {
HashRaw(rawKey string) string
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error)
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error)
UpdateAPIKey(apiKey *portainer.APIKey) error
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}

View File

@@ -1,51 +0,0 @@
package apikey
import (
"testing"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/stretchr/testify/assert"
)
func Test_generateRandomKey(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
wantLenth int
}{
{
name: "Generate a random key of length 16",
wantLenth: 16,
},
{
name: "Generate a random key of length 32",
wantLenth: 32,
},
{
name: "Generate a random key of length 64",
wantLenth: 64,
},
{
name: "Generate a random key of length 128",
wantLenth: 128,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := securecookie.GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := securecookie.GenerateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})
}

View File

@@ -1,69 +0,0 @@
package apikey
import (
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
)
const defaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry struct {
user portainer.User
apiKey portainer.APIKey
}
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type apiKeyCache struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
cache, _ := lru.New(cacheSize)
return &apiKeyCache{cache: cache}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *apiKeyCache) Get(digest string) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(digest)
if !ok {
return portainer.User{}, portainer.APIKey{}, false
}
tuple := val.(entry)
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *apiKeyCache) Set(digest string, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(digest, entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest string) {
c.cache.Remove(digest)
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get(k.(string))
if user.ID == userId {
present = c.cache.Remove(k)
}
}
return present
}

View File

@@ -1,181 +0,0 @@
package apikey
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest string
found bool
}{
{
digest: "foo",
found: true,
},
{
digest: "",
found: true,
},
{
digest: "bar",
found: false,
},
}
for _, test := range tests {
t.Run(string(test.digest), func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
}
}
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.Set("bar", portainer.User{ID: 2}, portainer.APIKey{})
keyCache.Set("foo", portainer.User{ID: 1}, portainer.APIKey{})
// overwrite existing entry
keyCache.Set("foo", portainer.User{ID: 3}, portainer.APIKey{})
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry)
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry)
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete("foo")
_, ok := keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Delete a non-existing entry", func(t *testing.T) {
nonPanicFunc := func() { keyCache.Delete("non-existent-key") }
is.NotPanics(nonPanicFunc)
})
}
func Test_apiKeyCacheLRU(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
cacheLen int
key []string
foundKeys []string
evictedKeys []string
}{
{
name: "Cache length is 1, add 2 keys",
cacheLen: 1,
key: []string{"foo", "bar"},
foundKeys: []string{"bar"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 1, add 3 keys",
cacheLen: 1,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"baz"},
evictedKeys: []string{"foo", "bar"},
},
{
name: "Cache length is 2, add 3 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"bar", "baz"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 2, add 4 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz", "qux"},
foundKeys: []string{"baz", "qux"},
evictedKeys: []string{"foo", "bar"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen)
for _, key := range test.key {
keyCache.Set(key, portainer.User{ID: 1}, portainer.APIKey{})
}
for _, key := range test.foundKeys {
_, _, found := keyCache.Get(key)
is.True(found, "Key %s not found", key)
}
for _, key := range test.evictedKeys {
_, _, found := keyCache.Get(key)
is.False(found, "key %s should have been evicted", key)
}
})
}
}
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
ok = keyCache.InvalidateUserKeyCache(1)
is.False(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
_, ok = keyCache.cache.Get(string("bar"))
is.True(ok)
})
}

View File

@@ -1,128 +0,0 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/pkg/errors"
)
const portainerAPIKeyPrefix = "ptr_"
var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository dataservices.APIKeyRepository
userRepository dataservices.UserService
cache *apiKeyCache
}
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) string {
hashDigest := sha256.Sum256([]byte(rawKey))
return base64.StdEncoding.EncodeToString(hashDigest[:])
}
// 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 := securecookie.GenerateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
UserID: user.ID,
Description: description,
Prefix: prefixedAPIKey[:7],
DateCreated: time.Now().Unix(),
Digest: hashDigest,
}
err := a.apiKeyRepository.Create(apiKey)
if err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
// persist api-key to cache
a.cache.Set(apiKey.Digest, user, *apiKey)
return prefixedAPIKey, apiKey, nil
}
// GetAPIKey returns an API key by its ID.
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
return a.apiKeyRepository.Read(apiKeyID)
}
// GetAPIKeys returns all the API keys associated to a user.
func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error) {
return a.apiKeyRepository.GetAPIKeysByUserID(userID)
}
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
}
apiKey, err := a.apiKeyRepository.GetAPIKeyByDigest(digest)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
}
user, err := a.userRepository.Read(apiKey.UserID)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
}
// persist api-key to cache - for quicker future lookups
a.cache.Set(apiKey.Digest, *user, *apiKey)
return *user, *apiKey, nil
}
// UpdateAPIKey updates an API key and in cache and database.
func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
user, _, err := a.GetDigestUserAndKey(apiKey.Digest)
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.Delete(apiKeyID)
}
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
return a.cache.InvalidateUserKeyCache(userId)
}

View File

@@ -1,303 +0,0 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"github.com/rs/zerolog/log"
)
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
is := assert.New(t)
is.Implements((*APIKeyService)(nil), NewAPIKeyService(nil, nil))
}
func Test_GenerateApiKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully generates API key", func(t *testing.T) {
desc := "test-1"
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, desc)
is.NoError(err)
is.NotEmpty(rawKey)
is.NotEmpty(apiKey)
is.Equal(desc, apiKey.Description)
})
t.Run("Api key prefix is 7 chars", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-2")
is.NoError(err)
is.Equal(rawKey[:7], apiKey.Prefix)
is.Len(apiKey.Prefix, 7)
})
t.Run("Api key has 'ptr_' as prefix", func(t *testing.T) {
rawKey, _, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x")
is.NoError(err)
is.Equal(portainerAPIKeyPrefix, "ptr_")
is.True(strings.HasPrefix(rawKey, "ptr_"))
})
t.Run("Successfully caches API key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-3")
is.NoError(err)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(user, userFromCache)
is.Equal(apiKey, &apiKeyFromCache)
})
t.Run("Decoded raw api-key digest matches generated digest", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-4")
is.NoError(err)
generatedDigest := sha256.Sum256([]byte(rawKey))
is.Equal(apiKey.Digest, base64.StdEncoding.EncodeToString(generatedDigest[:]))
})
}
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
apiKeyGot, err := service.GetAPIKey(apiKey.ID)
is.NoError(err)
is.Equal(apiKey, apiKeyGot)
})
}
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, _, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, _, err = service.GenerateApiKey(user, "test-2")
is.NoError(err)
keys, err := service.GetAPIKeys(user.ID)
is.NoError(err)
is.Len(keys, 2)
})
}
func Test_GetDigestUserAndKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
})
t.Run("Successfully caches user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(userGot, userFromCache)
is.Equal(apiKeyGot, apiKeyFromCache)
})
}
func Test_UpdateAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key LastUsed time", func(t *testing.T) {
user := portainer.User{ID: 1}
store.User().Create(&user)
_, apiKey, err := service.GenerateApiKey(user, "test-x")
is.NoError(err)
apiKey.LastUsed = time.Now().UTC().Unix()
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
log.Debug().Str("wanted", fmt.Sprintf("%+v", apiKey)).Str("got", fmt.Sprintf("%+v", apiKeyGot)).Msg("")
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
})
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
_, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x2")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
apiKey.LastUsed = time.Now().UTC().Unix()
is.NotEqual(*apiKey, apiKeyFromCache)
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, updatedAPIKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, updatedAPIKeyFromCache)
})
}
func Test_DeleteAPIKey(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(*apiKey, apiKeyGot)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, err = service.GetDigestUserAndKey(apiKey.Digest)
is.Error(err)
})
t.Run("Successfully removes api-key from cache upon deletion", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, ok = service.cache.Get(apiKey.Digest)
is.False(ok)
})
}
func Test_InvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates evicts keys from cache", func(t *testing.T) {
// generate api keys
user := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKey2, err := service.GenerateApiKey(user, "test-2")
is.NoError(err)
// verify api keys are present in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict cache
ok = service.InvalidateUserKeyCache(user.ID)
is.True(ok)
// verify users keys have been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
_, _, ok = service.cache.Get(apiKey2.Digest)
is.False(ok)
})
t.Run("User key eviction does not affect other users keys", func(t *testing.T) {
// generate keys for 2 users
user1 := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user1, "test-1")
is.NoError(err)
user2 := portainer.User{ID: 2}
_, apiKey2, err := service.GenerateApiKey(user2, "test-2")
is.NoError(err)
// verify keys in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict key of single user from cache
ok = service.cache.InvalidateUserKeyCache(user1.ID)
is.True(ok)
// verify user1 key has been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
// verify user2 key is still in cache
_, _, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
})
}

View File

@@ -34,45 +34,3 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
return buffer.Bytes(), nil
}
// tarFileInBuffer represents a tar archive buffer.
type tarFileInBuffer struct {
b *bytes.Buffer
w *tar.Writer
}
func NewTarFileInBuffer() *tarFileInBuffer {
var b bytes.Buffer
return &tarFileInBuffer{
b: &b,
w: tar.NewWriter(&b),
}
}
// Put puts a single file to tar archive buffer.
func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) error {
hdr := &tar.Header{
Name: fileName,
Mode: mode,
Size: int64(len(fileContent)),
}
if err := t.w.WriteHeader(hdr); err != nil {
return err
}
if _, err := t.w.Write(fileContent); err != nil {
return err
}
return nil
}
// Bytes returns the archive as a byte array.
func (t *tarFileInBuffer) Bytes() []byte {
return t.b.Bytes()
}
func (t *tarFileInBuffer) Close() error {
return t.w.Close()
}

View File

@@ -1,128 +0,0 @@
package archive
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TarGzDir creates a tar.gz archive and returns it's path.
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
outFile, err := os.Create(targzPath)
if err != nil {
return "", err
}
defer outFile.Close()
zipWriter := gzip.NewWriter(outFile)
defer zipWriter.Close()
tarWriter := tar.NewWriter(zipWriter)
defer tarWriter.Close()
err = filepath.Walk(absolutePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == targzPath {
return nil // skip archive file
}
pathInArchive := filepath.Clean(strings.TrimPrefix(path, absolutePath))
if pathInArchive == "" {
return nil // skip root dir
}
return addToArchive(tarWriter, pathInArchive, path, info)
})
return targzPath, err
}
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
stat, err := file.Stat()
if err != nil {
return err
}
header, err := tar.FileInfoHeader(stat, stat.Name())
if err != nil {
return err
}
header.Name = pathInArchive // use relative paths in archive
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if stat.IsDir() {
return nil
}
_, err = io.Copy(tarWriter, file)
return err
}
// ExtractTarGz reads a .tar.gz archive from the reader and extracts it into outputDirPath directory
func ExtractTarGz(r io.Reader, outputDirPath string) error {
zipReader, err := gzip.NewReader(r)
if err != nil {
return err
}
defer zipReader.Close()
tarReader := tar.NewReader(zipReader)
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
// skip, dir will be created with a file
case tar.TypeReg:
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
if err := os.MkdirAll(filepath.Dir(p), 0o744); err != nil {
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
}
outFile, err := os.Create(p)
if err != nil {
return fmt.Errorf("Failed to create file %s", header.Name)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("Failed to extract file %s", header.Name)
}
outFile.Close()
default:
return fmt.Errorf("tar: unknown type: %v in %s",
header.Typeflag,
header.Name)
}
}
return nil
}

View File

@@ -1,89 +0,0 @@
package archive
import (
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir := t.TempDir()
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir := t.TempDir()
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}

Binary file not shown.

View File

@@ -3,13 +3,10 @@ package archive
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -36,7 +33,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
}
defer f.Close()
data, err := io.ReadAll(f)
data, err := ioutil.ReadAll(f)
if err != nil {
return err
}
@@ -55,60 +52,3 @@ func extractFileFromArchive(file *zip.File, dest string) error {
return outFile.Close()
}
// UnzipFile will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2).
func UnzipFile(src string, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
p := filepath.Join(dest, f.Name)
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
if !strings.HasPrefix(p, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", p)
}
if f.FileInfo().IsDir() {
// Make Folder
os.MkdirAll(p, os.ModePerm)
continue
}
err = unzipFile(f, p)
if err != nil {
return err
}
}
return nil
}
func unzipFile(f *zip.File, p string) error {
// Make File
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
}
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
}
defer outFile.Close()
rc, err := f.Open()
if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
}
defer rc.Close()
_, err = io.Copy(outFile, rc)
if err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
}
return nil
}

View File

@@ -1,29 +0,0 @@
package archive
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnzipFile(t *testing.T) {
dir := t.TempDir()
/*
Archive structure.
├── 0
│ ├── 1
│ │ └── 2.txt
│ └── 1.txt
└── 0.txt
*/
err := UnzipFile("./testdata/sample_archive.zip", dir)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"
assert.FileExists(t, filepath.Join(archiveDir, "0.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1", "2.txt"))
}

View File

@@ -1,61 +0,0 @@
package ecr
import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
)
func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Time, err error) {
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(context.TODO(), nil)
if err != nil {
return
}
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
err = fmt.Errorf("AuthorizationData is empty")
return
}
authData := getAuthorizationTokenOutput.AuthorizationData[0]
token = authData.AuthorizationToken
expiry = authData.ExpiresAt
return
}
func (s *Service) GetAuthorizationToken() (token *string, expiry *time.Time, err error) {
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken()
if err != nil {
return
}
tokenByte, err := base64.StdEncoding.DecodeString(*tokenEncodedStr)
if err != nil {
return
}
tokenStr := string(tokenByte)
token = &tokenStr
return
}
func (s *Service) ParseAuthorizationToken(token string) (username string, password string, err error) {
if len(token) == 0 {
return
}
splitToken := strings.Split(token, ":")
if len(splitToken) < 2 {
err = fmt.Errorf("invalid ECR authorization token")
return
}
username = splitToken[0]
password = splitToken[1]
return
}

View File

@@ -1,32 +0,0 @@
package ecr
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/ecr"
)
type (
Service struct {
accessKey string
secretKey string
region string
client *ecr.Client
}
)
func NewService(accessKey, secretKey, region string) *Service {
options := ecr.Options{
Region: region,
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
}
client := ecr.New(options)
return &Service{
accessKey: accessKey,
secretKey: secretKey,
region: region,
client: client,
}
}

View File

@@ -1,105 +0,0 @@
package backup
import (
"fmt"
"os"
"path"
"path/filepath"
"time"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const rwxr__r__ os.FileMode = 0o744
var filesToBackup = []string{
"certs",
"compose",
"config.json",
"custom_templates",
"edge_jobs",
"edge_stacks",
"extensions",
"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.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
{
// new export
exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
err := datastore.Export(exportFilename)
if err != nil {
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
} else {
log.Debug().Str("filename", exportFilename).Msg("file exported")
}
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
archivePath, err := archive.TarGzDir(backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to make an archive")
}
if password != "" {
archivePath, err = encrypt(archivePath, password)
if err != nil {
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
}
}
return archivePath, nil
}
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
_, err := datastore.Backup(filepath.Join(backupDirPath, "portainer.db"))
return err
}
func encrypt(path string, passphrase string) (string, error) {
in, err := os.Open(path)
if err != nil {
return "", err
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
out, err := os.Create(outFileName)
if err != nil {
return "", err
}
err = crypto.AesEncrypt(in, out, []byte(passphrase))
return outFileName, err
}

View File

@@ -1,113 +0,0 @@
package backup
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"time"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
)
var filesToRestore = append(filesToBackup, "portainer.db")
// Restores system state from backup archive, will trigger system shutdown, when finished.
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
var err error
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive. Please ensure the password is correct and try again")
}
}
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer os.RemoveAll(filepath.Dir(restorePath))
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
unlock := gate.Lock()
defer unlock()
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
// At some point, backups were created containing a subdirectory, now we need to handle both
restorePath, err = getRestoreSourcePath(restorePath)
if err != nil {
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
shutdownTrigger()
return nil
}
func decrypt(r io.Reader, password string) (io.Reader, error) {
return crypto.AesDecrypt(r, []byte(password))
}
func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func getRestoreSourcePath(dir string) (string, error) {
// find portainer.db or portainer.edb file. Return the parent directory
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
backupDirPath := dir
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if portainerdbRegex.MatchString(d.Name()) {
backupDirPath = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
return backupDirPath, err
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
}

View File

@@ -0,0 +1,96 @@
package customtemplate
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "customtemplates"
)
// Service represents a service for managing custom template data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// CustomTemplates return an array containing all the custom templates.
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var customTemplate portainer.CustomTemplate
err := internal.UnmarshalObjectWithJsoniter(v, &customTemplate)
if err != nil {
return err
}
customTemplates = append(customTemplates, customTemplate)
}
return nil
})
return customTemplates, err
}
// CustomTemplate returns an custom template by ID.
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
return &customTemplate, nil
}
// UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, customTemplate)
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateCustomTemplate assign an ID to a new custom template and saves it.
func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(customTemplate.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

401
api/bolt/datastore.go Normal file
View File

@@ -0,0 +1,401 @@
package bolt
import (
"log"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgejob"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
"github.com/portainer/portainer/api/internal/authorization"
)
const (
databaseFileName = "portainer.db"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
path string
db *bolt.DB
isNew bool
fileService portainer.FileService
CustomTemplateService *customtemplate.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
path: storePath,
fileService: fileService,
isNew: true,
}
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if databaseFileExists {
store.isNew = false
}
return store, nil
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() error {
databasePath := path.Join(store.path, databaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
store.db = db
return store.initServices()
}
// Close closes the BoltDB database.
func (store *Store) Close() error {
if store.db != nil {
return store.db.Close()
}
return nil
}
// IsNew returns true if the database was just created and false if it is re-using
// existing data.
func (store *Store) IsNew() bool {
return store.isNew
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error {
if store.isNew {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == errors.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.db,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
AuthorizationService: authorization.NewService(store),
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.db)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.db)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.db)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.db)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.db)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.db)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.db)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
}

View File

@@ -0,0 +1,48 @@
package dockerhub
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "dockerhub"
dockerHubKey = "DOCKERHUB"
)
// Service represents a service for managing Dockerhub data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// DockerHub returns the DockerHub object.
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -0,0 +1,94 @@
package edgegroup
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// Service represents a service for managing Edge group data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var group portainer.EdgeGroup
err := internal.UnmarshalObjectWithJsoniter(v, &group)
if err != nil {
return err
}
groups = append(groups, group)
}
return nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an Edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, group)
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
group.ID = portainer.EdgeGroupID(id)
data, err := internal.MarshalObject(group)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(group.ID)), data)
})
}

101
api/bolt/edgejob/edgejob.go Normal file
View File

@@ -0,0 +1,101 @@
package edgejob
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgejobs"
)
// Service represents a service for managing edge jobs data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var edgeJob portainer.EdgeJob
err := internal.UnmarshalObject(v, &edgeJob)
if err != nil {
return err
}
edgeJobs = append(edgeJobs, edgeJob)
}
return nil
})
return edgeJobs, err
}
// EdgeJob returns an Edge job by ID
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 {
id, _ := bucket.NextSequence()
edgeJob.ID = portainer.EdgeJobID(id)
}
data, err := internal.MarshalObject(edgeJob)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeJob.ID)), data)
})
}
// UpdateEdgeJob updates an Edge job by ID
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeJob)
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@@ -0,0 +1,101 @@
package edgestack
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// Service represents a service for managing Edge stack data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack portainer.EdgeStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// EdgeStack returns an Edge stack by ID.
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
id, _ := bucket.NextSequence()
edgeStack.ID = portainer.EdgeStackID(id)
}
data, err := internal.MarshalObject(edgeStack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeStack.ID)), data)
})
}
// UpdateEdgeStack updates an Edge stack.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack)
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@@ -0,0 +1,145 @@
package endpoint
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Endpoint returns an endpoint by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint)
}
// DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpoint portainer.Endpoint
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
if err != nil {
return err
}
endpoints = append(endpoints, endpoint)
}
return nil
})
return endpoints, err
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints
err := bucket.SetSequence(uint64(endpoint.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpoint.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
data, err := internal.MarshalObject(endpoint)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return nil
})
}

View File

@@ -0,0 +1,95 @@
package endpointgroup
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_groups"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EndpointGroup returns an endpoint group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var endpointGroup portainer.EndpointGroup
err := internal.UnmarshalObject(v, &endpointGroup)
if err != nil {
return err
}
endpointGroups = append(endpointGroups, endpointGroup)
}
return nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
endpointGroup.ID = portainer.EndpointGroupID(id)
data, err := internal.MarshalObject(endpointGroup)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointGroup.ID)), data)
})
}

View File

@@ -0,0 +1,68 @@
package endpointrelation
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// Service represents a service for managing endpoint relation data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// EndpointRelation returns a Endpoint relation object by EndpointID
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointRelation.EndpointID)), data)
})
}
// UpdateEndpointRelation updates an Endpoint relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := internal.Itob(int(EndpointID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation)
}
// DeleteEndpointRelation deletes an Endpoint relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := internal.Itob(int(EndpointID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@@ -0,0 +1,7 @@
package errors
import "errors"
var (
ErrObjectNotFound = errors.New("Object not found inside the database")
)

View File

@@ -0,0 +1,86 @@
package extension
import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "extension"
)
// Service represents a service for managing endpoint data.
type Service struct {
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
db: db,
}, nil
}
// Extension returns a extension by ID
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
var extension portainer.Extension
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &extension)
if err != nil {
return nil, err
}
return &extension, nil
}
// Extensions return an array containing all the extensions.
func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0)
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var extension portainer.Extension
err := internal.UnmarshalObject(v, &extension)
if err != nil {
return err
}
extensions = append(extensions, extension)
}
return nil
})
return extensions, err
}
// Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(extension.ID)), data)
})
}
// DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

103
api/bolt/init.go Normal file
View File

@@ -0,0 +1,103 @@
package bolt
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
if err == errors.ErrObjectNotFound {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.SettingsService.Settings()
if err == errors.ErrObjectNotFound {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowHostNamespaceForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
EnableHostManagementFeatures: false,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
}
_, err = store.DockerHubService.DockerHub()
if err == errors.ErrObjectNotFound {
defaultDockerHub := &portainer.DockerHub{
Authentication: false,
Username: "",
Password: "",
}
err := store.DockerHubService.UpdateDockerHub(defaultDockerHub)
if err != nil {
return err
}
} else if err != nil {
return err
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
}
if len(groups) == 0 {
unassignedGroup := &portainer.EndpointGroup{
Name: "Unassigned",
Description: "Unassigned endpoints",
Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
}
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
if err != nil {
return err
}
}
return nil
}

96
api/bolt/internal/db.go Normal file
View File

@@ -0,0 +1,96 @@
package internal
import (
"encoding/binary"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/bolt/errors"
)
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
func Itob(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
// CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return nil
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
var data []byte
err := db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return err
}
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
}
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
var identifier int
db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
return err
}
identifier = int(id)
return nil
})
return identifier
}

25
api/bolt/internal/json.go Normal file
View File

@@ -0,0 +1,25 @@
package internal
import (
"encoding/json"
jsoniter "github.com/json-iterator/go"
)
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
return json.Unmarshal(data, object)
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate endpoint
// decoding at the moment.
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
return jsoni.Unmarshal(data, &object)
}

View File

@@ -0,0 +1,37 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/user"
)
func (m *Migrator) updateAdminUserToDBVersion1() error {
u, err := m.userService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.userService.CreateUser(admin)
if err != nil {
return err
}
err = m.removeLegacyAdminUser()
if err != nil {
return err
}
} else if err != nil && err != errors.ErrObjectNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(user.BucketName))
return bucket.Delete([]byte("admin"))
})
}

View File

@@ -0,0 +1,103 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
func (m *Migrator) updateResourceControlsToDBVersion2() error {
legacyResourceControls, err := m.retrieveLegacyResourceControls()
if err != nil {
return err
}
for _, resourceControl := range legacyResourceControls {
resourceControl.SubResourceIDs = []string{}
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
owner, err := m.userService.User(resourceControl.OwnerID)
if err != nil {
return err
}
if owner.Role == portainer.AdministratorRole {
resourceControl.AdministratorsOnly = true
resourceControl.UserAccesses = []portainer.UserResourceAccess{}
} else {
resourceControl.AdministratorsOnly = false
userAccess := portainer.UserResourceAccess{
UserID: resourceControl.OwnerID,
AccessLevel: portainer.ReadWriteAccessLevel,
}
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
}
err = m.resourceControlService.CreateResourceControl(&resourceControl)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsToDBVersion2() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.AuthorizedTeams = []portainer.TeamID{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
legacyResourceControls := make([]portainer.ResourceControl, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("containerResourceControl"))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ContainerResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("serviceResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.ServiceResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
bucket = tx.Bucket([]byte("volumeResourceControl"))
cursor = bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
resourceControl.Type = portainer.VolumeResourceControl
legacyResourceControls = append(legacyResourceControls, resourceControl)
}
return nil
})
return legacyResourceControls, err
}

View File

@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndpointsToVersion11() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
if endpoint.Type == portainer.AgentOnDockerEnvironment {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = true
} else {
if endpoint.TLSConfig.TLSSkipVerify && !endpoint.TLSConfig.TLS {
endpoint.TLSConfig.TLSSkipVerify = false
}
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,127 @@
package migrator
import (
"strconv"
"strings"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/stack"
)
func (m *Migrator) updateEndpointsToVersion12() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Tags = []string{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToVersion12() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, group := range legacyEndpointGroups {
group.Tags = []string{}
err = m.endpointGroupService.UpdateEndpointGroup(group.ID, &group)
if err != nil {
return err
}
}
return nil
}
type legacyStack struct {
ID string `json:"Id"`
Name string `json:"Name"`
EndpointID portainer.EndpointID `json:"EndpointId"`
SwarmID string `json:"SwarmId"`
EntryPoint string `json:"EntryPoint"`
Env []portainer.Pair `json:"Env"`
ProjectPath string
}
func (m *Migrator) updateStacksToVersion12() error {
legacyStacks, err := m.retrieveLegacyStacks()
if err != nil {
return err
}
for _, legacyStack := range legacyStacks {
err := m.convertLegacyStack(&legacyStack)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) convertLegacyStack(s *legacyStack) error {
stackID := m.stackService.GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Name: s.Name,
Type: portainer.DockerSwarmStack,
SwarmID: s.SwarmID,
EndpointID: 0,
EntryPoint: s.EntryPoint,
Env: s.Env,
}
stack.ProjectPath = strings.Replace(s.ProjectPath, s.ID, strconv.Itoa(stackID), 1)
err := m.fileService.Rename(s.ProjectPath, stack.ProjectPath)
if err != nil {
return err
}
err = m.deleteLegacyStack(s.ID)
if err != nil {
return err
}
return m.stackService.CreateStack(stack)
}
func (m *Migrator) deleteLegacyStack(legacyID string) error {
return m.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
return bucket.Delete([]byte(legacyID))
})
}
func (m *Migrator) retrieveLegacyStacks() ([]legacyStack, error) {
var legacyStacks = make([]legacyStack, 0)
err := m.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(stack.BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var stack legacyStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
legacyStacks = append(legacyStacks, stack)
}
return nil
})
return legacyStacks, err
}

View File

@@ -0,0 +1,17 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToVersion13() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.LDAPSettings.AutoCreateUsers = false
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateResourceControlsToDBVersion14() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range resourceControls {
if resourceControl.AdministratorsOnly == true {
err = m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,16 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion15() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.EnableHostManagementFeatures = false
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateTemplatesToVersion15() error {
// Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707
return nil
}

View File

@@ -0,0 +1,14 @@
package migrator
func (m *Migrator) updateSettingsToDBVersion16() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.SnapshotInterval == "" {
legacySettings.SnapshotInterval = "5m"
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateExtensionsToDBVersion17() error {
legacyExtensions, err := m.extensionService.Extensions()
if err != nil {
return err
}
for _, extension := range legacyExtensions {
extension.License.Valid = true
err = m.extensionService.Persist(&extension)
if err != nil {
return err
}
}
return nil
}

View File

@@ -2,14 +2,10 @@ package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
func (m *Migrator) updateUsersToDBVersion18() error {
log.Info().Msg("updating users")
legacyUsers, err := m.userService.ReadAll()
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
@@ -33,7 +29,7 @@ func (m *Migrator) updateUsersToDBVersion18() error {
portainer.OperationPortainerUserMemberships: true,
}
err = m.userService.Update(user.ID, &user)
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
@@ -43,8 +39,6 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
log.Info().Msg("updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -75,9 +69,7 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
log.Info().Msg("updating endpoint groups")
legacyEndpointGroups, err := m.endpointGroupService.ReadAll()
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
@@ -97,7 +89,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
}
err = m.endpointGroupService.Update(endpointGroup.ID, &endpointGroup)
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
@@ -107,9 +99,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
log.Info().Msg("updating registries")
legacyRegistries, err := m.registryService.ReadAll()
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err
}
@@ -125,7 +115,7 @@ func (m *Migrator) updateRegistriesToDBVersion18() error {
registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{}
}
err = m.registryService.Update(registry.ID, &registry)
err = m.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}

View File

@@ -0,0 +1,16 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion19() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.EdgeAgentCheckinInterval == 0 {
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -2,20 +2,15 @@ package migrator
import (
"strings"
"github.com/rs/zerolog/log"
)
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
log.Info().Msg("updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
func (m *Migrator) updateSettingsToDBVersion20() error {
log.Info().Msg("updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
@@ -27,8 +22,6 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
log.Info().Msg("updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AuthenticationMethod = portainer.AuthenticationInternal
legacySettings.LDAPSettings = portainer.LDAPSettings{
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
}
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,85 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range legacyResourceControls {
resourceControl.AdministratorsOnly = false
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
if err != nil {
return err
}
endpointAdministratorRole.Priority = 1
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
if err != nil {
return err
}
helpDeskRole.Priority = 2
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
if err != nil {
return err
}
standardUserRole.Priority = 3
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
if err != nil {
return err
}
readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
if err != nil {
return err
}
return m.authorizationService.UpdateUsersAuthorizations()
}

View File

@@ -1,15 +1,9 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
import "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
log.Info().Msg("updating tags")
tags, err := m.tagService.ReadAll()
tags, err := m.tagService.Tags()
if err != nil {
return err
}
@@ -17,19 +11,16 @@ func (m *Migrator) updateTagsToDBVersion23() error {
for _, tag := range tags {
tag.EndpointGroups = make(map[portainer.EndpointGroupID]bool)
tag.Endpoints = make(map[portainer.EndpointID]bool)
err = m.tagService.Update(tag.ID, &tag)
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
log.Info().Msg("updating endpoints and endpoint groups")
tags, err := m.tagService.ReadAll()
tags, err := m.tagService.Tags()
if err != nil {
return err
}
@@ -64,13 +55,13 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = m.endpointRelationService.Create(relation)
err = m.endpointRelationService.CreateEndpointRelation(relation)
if err != nil {
return err
}
}
endpointGroups, err := m.endpointGroupService.ReadAll()
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
@@ -85,18 +76,17 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
}
}
endpointGroup.TagIDs = endpointGroupTags
err = m.endpointGroupService.Update(endpointGroup.ID, &endpointGroup)
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
for _, tag := range tagsNameMap {
err = m.tagService.Update(tag.ID, &tag)
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,34 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDB24() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowHostNamespaceForRegularUsers = true
legacySettings.AllowDeviceMappingForRegularUsers = true
legacySettings.AllowStackManagementForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateStacksToDB24() error {
stacks, err := m.stackService.Stacks()
if err != nil {
return err
}
for idx := range stacks {
stack := &stacks[idx]
stack.Status = portainer.StackStatusActive
err := m.stackService.UpdateStack(stack.ID, stack)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,23 @@
package migrator
import (
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDB25() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.TemplatesURL == "" {
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
}
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,28 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndpointsToDBVersion4() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.TLSConfig = portainer.TLSConfiguration{}
if endpoint.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion5() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowBindMountsForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion6() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowPrivilegedModeForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,11 @@
package migrator
func (m *Migrator) updateSettingsToVersion7() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.DisplayDonationHeader = true
return m.settingsService.UpdateSettings(legacySettings)
}

View File

@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndpointsToVersion8() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.Extensions = []portainer.EndpointExtension{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,20 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateEndpointsToVersion9() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.GroupID = portainer.EndpointGroupID(1)
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

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