Compare commits

..

15 Commits

Author SHA1 Message Date
Anthony Lapenna
ecf52616e2 feat(edge): update deployment instructions 2019-06-17 11:37:09 +12:00
Anthony Lapenna
69bc599b5b feat(edge): refactor key creation 2019-06-17 09:18:17 +12:00
Anthony Lapenna
e58b019ffa feat(edge): wip edge 2019-06-12 12:06:59 +12:00
Anthony Lapenna
1fc4e7bddb Merge branch 'develop' into edge 2019-06-05 09:24:22 +12:00
Anthony Lapenna
2cabfd574c Merge branch 'develop' into edge
# Conflicts:
#	api/portainer.go
2019-05-27 11:29:35 +12:00
Anthony Lapenna
3b946d84ac feat(endpoint-creation): update Edge agent deployment instructions 2019-05-24 18:01:50 +12:00
Anthony Lapenna
28abe55179 refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment 2019-05-24 12:02:46 +12:00
Anthony Lapenna
e31365c6a5 refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment 2019-05-24 11:46:18 +12:00
Anthony Lapenna
bedb4fc7f4 style(endpoint-creation): refactor IoT agent to Edge agent 2019-05-24 11:26:59 +12:00
Anthony Lapenna
8f05ba77b4 Merge branch 'develop' into edge 2019-05-24 11:11:57 +12:00
Anthony Lapenna
d03e22e26e feat(intel): add -v /:/host and --name portainer_agent_iot to agent command 2019-04-24 20:15:05 +12:00
Anthony Lapenna
ec667a19a0 feat(intel): add -e CAP_HOST_MANAGEMENT=1 to agent command 2019-04-24 20:05:28 +12:00
Anthony Lapenna
8afe1ac37b feat(intel): display agent features when connected to IoT endpoint 2019-04-24 19:35:26 +12:00
Anthony Lapenna
9dc3188cc0 feat(intel): fix webconsole and agent deployment command 2019-04-24 19:31:22 +12:00
Anthony Lapenna
9cf014adab feat(intel): POC Intel 2019-04-24 14:59:15 +12:00
2584 changed files with 55827 additions and 164747 deletions

12
.babelrc Normal file
View File

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

View File

@@ -1,44 +1,62 @@
version: "2"
checks:
argument-count:
enabled: false
enabled: true
config:
threshold: 4
complex-logic:
enabled: false
enabled: true
config:
threshold: 4
file-lines:
enabled: false
enabled: true
config:
threshold: 300
method-complexity:
enabled: false
method-count:
enabled: false
enabled: true
config:
threshold: 20
method-lines:
enabled: false
enabled: true
config:
threshold: 50
nested-control-flow:
enabled: false
enabled: true
config:
threshold: 4
return-statements:
enabled: false
similar-code:
enabled: false
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
identical-code:
enabled: false
enabled: true
config:
threshold: #language-specific defaults. overrides affect all languages.
plugins:
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
javascript:
mass_threshold: 80
eslint:
enabled: true
channel: "eslint-5"
config:
config: .eslintrc.yml
fixme:
enabled: true
exclude_patterns:
- assets/
- build/
- dist/
- distribution/
- node_modules
- test/
- webpack/
- gruntfile.js
- webpack.config.js
- api/
- "!app/kubernetes/**"
- .github/
- .tmp/

View File

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

View File

@@ -6,100 +6,287 @@ env:
globals:
angular: true
__CONFIG_GA_ID: true
extends:
- 'eslint:recommended'
- 'plugin:storybook/recommended'
- prettier
plugins:
- import
parserOptions:
ecmaVersion: 2018
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
modules: true
# # http://eslint.org/docs/rules/
rules:
no-control-regex: 'off'
# # Possible Errors
# no-await-in-loop: off
# no-cond-assign: error
# no-console: off
# no-constant-condition: error
# no-control-regex: error
# no-debugger: error
# no-dupe-args: error
# no-dupe-keys: error
# no-duplicate-case: error
# no-empty-character-class: error
no-empty: warn
# no-ex-assign: error
# no-extra-boolean-cast: error
# no-extra-parens: off
# no-extra-semi: error
# no-func-assign: error
# no-inner-declarations:
# - error
# - functions
# no-invalid-regexp: error
# no-irregular-whitespace: error
# no-negated-in-lhs: error
# no-obj-calls: error
# no-prototype-builtins: off
# no-regex-spaces: error
# no-sparse-arrays: error
# no-template-curly-in-string: off
# no-unexpected-multiline: error
# no-unreachable: error
# no-unsafe-finally: off
# no-unsafe-negation: off
# use-isnan: error
# valid-jsdoc: off
# valid-typeof: error
# # Best Practices
# accessor-pairs: error
# array-callback-return: off
# block-scoped-var: off
# class-methods-use-this: off
# complexity:
# - error
# - 6
# consistent-return: off
# curly: off
# default-case: off
# dot-location: off
# dot-notation: off
# eqeqeq: error
# guard-for-in: error
# no-alert: error
# no-caller: error
# no-case-declarations: error
# no-div-regex: error
# no-else-return: off
no-empty-function: warn
no-useless-escape: 'off'
import/order:
[
'error',
{
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
]
# no-empty-pattern: error
# no-eq-null: error
# no-eval: error
# no-extend-native: error
# no-extra-bind: error
# no-extra-label: off
# no-fallthrough: error
# no-floating-decimal: off
# no-global-assign: off
# no-implicit-coercion: off
# no-implied-eval: error
# no-invalid-this: off
# no-iterator: error
# no-labels:
# - error
# - allowLoop: true
# allowSwitch: true
# no-lone-blocks: error
# no-loop-func: error
# no-magic-number: off
# no-multi-spaces: off
# no-multi-str: off
# no-native-reassign: error
# no-new-func: error
# no-new-wrappers: error
# no-new: error
# no-octal-escape: error
# no-octal: error
# no-param-reassign: off
# no-proto: error
# no-redeclare: error
# no-restricted-properties: off
# no-return-assign: error
# no-return-await: off
# no-script-url: error
# no-self-assign: off
# no-self-compare: error
# no-sequences: off
# no-throw-literal: off
# no-unmodified-loop-condition: off
# no-unused-expressions: error
# no-unused-labels: off
# no-useless-call: error
# no-useless-concat: error
no-useless-escape: off
# no-useless-return: off
# no-void: error
# no-warning-comments: off
# no-with: error
# prefer-promise-reject-errors: off
# radix: error
# require-await: off
# vars-on-top: off
# wrap-iife: error
# yoda: off
settings:
'import/resolver':
alias:
map:
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
# # Strict
# strict: off
overrides:
- files:
- app/**/*.ts{,x}
parserOptions:
project: './tsconfig.json'
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
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' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
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' }]
'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 }]
- files:
- app/**/*.test.*
extends:
- 'plugin:jest/recommended'
- 'plugin:jest/style'
env:
'jest/globals': true
rules:
'react/jsx-no-constructed-context-values': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off
# # Variables
# init-declarations: off
# no-catch-shadow: error
# no-delete-var: error
# no-label-var: error
# no-restricted-globals: off
# no-shadow-restricted-names: error
# no-shadow: off
# no-undef-init: error
# no-undef: off
# no-undefined: off
# no-unused-vars:
# - warn
# -
# vars: local
# no-use-before-define: off
# # Node.js and CommonJS
# callback-return: error
# global-require: error
# handle-callback-err: error
# no-mixed-requires: off
# no-new-require: off
# no-path-concat: error
# no-process-env: off
# no-process-exit: error
# no-restricted-modules: off
# no-sync: off
# # Stylistic Issues
# array-bracket-spacing: off
# block-spacing: off
# brace-style: off
# camelcase: off
# capitalized-comments: off
# comma-dangle:
# - error
# - never
# comma-spacing: off
# comma-style: off
# computed-property-spacing: off
# consistent-this: off
# eol-last: off
# func-call-spacing: off
# func-name-matching: off
# func-names: off
# func-style: off
# id-length: off
# id-match: off
# indent: off
# jsx-quotes: off
# key-spacing: off
# keyword-spacing: off
# line-comment-position: off
# linebreak-style:
# - error
# - unix
# lines-around-comment: off
# lines-around-directive: off
# max-depth: off
# max-len: off
# max-nested-callbacks: off
# max-params: off
# max-statements-per-line: off
# max-statements:
# - error
# - 30
# multiline-ternary: off
# new-cap: off
# new-parens: off
# newline-after-var: off
# newline-before-return: off
# newline-per-chained-call: off
# no-array-constructor: off
# no-bitwise: off
# no-continue: off
# no-inline-comments: off
# no-lonely-if: off
# no-mixed-operators: off
# no-mixed-spaces-and-tabs: off
# no-multi-assign: off
# no-multiple-empty-lines: off
# no-negated-condition: off
# no-nested-ternary: off
# no-new-object: off
# no-plusplus: off
# no-restricted-syntax: off
# no-spaced-func: off
# no-tabs: off
# no-ternary: off
# no-trailing-spaces: off
# no-underscore-dangle: off
# no-unneeded-ternary: off
# object-curly-newline: off
# object-curly-spacing: off
# object-property-newline: off
# one-var-declaration-per-line: off
# one-var: off
# operator-assignment: off
# operator-linebreak: off
# padded-blocks: off
# quote-props: off
# quotes:
# - error
# - single
# require-jsdoc: off
# semi-spacing: off
# semi:
# - error
# - always
# sort-keys: off
# sort-vars: off
# space-before-blocks: off
# space-before-function-paren: off
# space-in-parens: off
# space-infix-ops: off
# space-unary-ops: off
# spaced-comment: off
# template-tag-spacing: off
# unicode-bom: off
# wrap-regex: off
# # ECMAScript 6
# arrow-body-style: off
# arrow-parens: off
# arrow-spacing: off
# constructor-super: off
# generator-star-spacing: off
# no-class-assign: off
# no-confusing-arrow: off
# no-const-assign: off
# no-dupe-class-members: off
# no-duplicate-imports: off
# no-new-symbol: off
# no-restricted-imports: off
# no-this-before-super: off
# no-useless-computed-key: off
# no-useless-constructor: off
# no-useless-rename: off
# no-var: off
# object-shorthand: off
# prefer-arrow-callback: off
# prefer-const: off
# prefer-destructuring: off
# prefer-numeric-literals: off
# prefer-rest-params: off
# prefer-reflect: off
# prefer-spread: off
# prefer-template: off
# require-yield: off
# rest-spread-spacing: off
# sort-imports: off
# symbol-description: off
# template-curly-spacing: off
# yield-star-spacing: off

View File

@@ -1,4 +0,0 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2 (put here after fix/EE-2344/fix-eslint-issues is merged)

View File

@@ -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:

View File

@@ -1,54 +1,49 @@
---
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/r/portainer-logs)
**Steps to reproduce the issue:**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
- Portainer version:
- Docker version (managed by Portainer):
- Kubernetes version (managed by Portainer):
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**
Add any other context about the problem here.
---
name: Bug report
about: Create a bug report
---
<!--
Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
-->
**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.
Briefly describe what you were expecting.
**Steps to reproduce the issue:**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Technical details:**
* Portainer version:
* Docker 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.

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Portainer Business Edition - Get 5 nodes free
url: https://portainer.io/pricing/take5
about: Portainer Business Edition has more features, more support and you can now get 5 nodes free for as long as you want.

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: 5
WAIT_MS: 5000

View File

@@ -1,41 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 12
# ESLint and Prettier must be in `package.json`
- name: Install Node.js dependencies
run: yarn
- 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/

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,27 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v4.0.0
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,11 +0,0 @@
name: Test Frontend
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: yarn
- name: Run tests
run: yarn test:client

View File

@@ -1,53 +0,0 @@
name: Validate
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node v14
uses: actions/setup-node@v2
with:
node-version: 14
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Setup Go v1.17.3
uses: actions/setup-go@v2
with:
go-version: '^1.17.3'
- name: Prebuild docs
run: yarn prebuild:docs
- name: Build OpenAPI 2.0 Spec
run: yarn build:docs
# Install dependencies globally to bypass installing all frontend deps
- name: Install swagger2openapi and swagger-cli
run: yarn global add swagger2openapi @apidevtools/swagger-cli
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
run: yarn validate:docs

12
.gitignore vendored
View File

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

View File

@@ -1 +0,0 @@
dist

View File

@@ -1,19 +0,0 @@
{
"printWidth": 180,
"singleQuote": true,
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "angular"
}
},
{
"files": ["*.{j,t}sx", "*.ts"],
"options": {
"printWidth": 80
}
}
]
}

View File

@@ -1,35 +0,0 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-postcss',
options: {
cssLoaderOptions: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
},
},
},
],
webpackFinal: (config) => {
config.resolve.plugins = [
...(config.resolve.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve.extensions,
}),
];
return config;
},
core: {
builder: 'webpack5',
},
staticDirs: ['./public'],
};

View File

@@ -1,41 +0,0 @@
import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
// Initialize MSW
initMSW({
onUnhandledRequest: ({ method, url }) => {
if (url.pathname.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
`);
}
},
});
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
};
export const decorators = [
(Story) => (
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
),
mswDecorator,
];

View File

@@ -1,328 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.3).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
const bypassHeaderName = 'x-msw-bypass';
const activeClientIds = new Set();
self.addEventListener('install', function () {
return self.skipWaiting();
});
self.addEventListener('activate', async function (event) {
return 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();
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;
}
}
});
// 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();
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 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 clonedResponse = response.clone();
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body: clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
}
return response;
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
}
// 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 await getOriginalResponse();
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
return fetch(originalRequest);
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay);
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse();
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
// Rejecting a request Promise emulates a network error.
throw networkError;
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body);
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
return respondWithMock(clientMessage);
}
}
return getOriginalResponse();
}
self.addEventListener('fetch', function (event) {
const { request } = event;
const accept = request.headers.get('accept') || '';
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return;
}
// 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;
}
const requestId = uuidv4();
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url);
return;
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`
);
})
);
});
function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value;
});
return reqHeaders;
}
function sendToClient(client, message) {
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(JSON.stringify(message), [channel.port2]);
});
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

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/main.go",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,
"args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"]
}
]
}

View File

@@ -1,182 +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"
// }
"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,5 +0,0 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"],
"gitlens.advanced.blame.customArguments": ["ignore-revs-file", ".git-blame-ignore-revs"]
}

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

@@ -6,16 +6,30 @@ Some basic conventions for contributing to this project.
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
- Develop in a topic branch, not master/develop
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
* Develop in a topic branch, not master/develop
When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
## Issues open to contribution
Want to contribute but don't know where to start? Have a look at the issues labeled with the `good first issue` label: https://github.com/portainer/portainer/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
Want to contribute but don't know where to start?
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
* **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
You can use Github filters to list these issues:
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
## Commit Message Format
@@ -37,14 +51,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to
Must be one of the following:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
- **refactor**: A code change that neither fixes a bug or adds a feature
- **test**: Adding missing tests
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
### Scope
@@ -57,9 +71,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta
The subject contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
## Contribution process
@@ -74,71 +88,3 @@ Our contribution process is described below. Some of the steps can be visualized
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build and run Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies with yarn:
```sh
$ yarn
```
Then build and run the project in a Docker container:
```sh
$ yarn start
```
Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
### Build customisation
You can customise 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: `""`).
## 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.

View File

@@ -1,16 +1,17 @@
<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/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")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![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 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.
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
- [Take5 get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
- [Portainer BE install guide](https://install.portainer.io)
**_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 your all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
## Demo
@@ -18,62 +19,49 @@ You can try out the public demo instance: http://demo.portainer.io/ (login with
Please note that the public demo cluster is **reset every 15min**.
## Latest Version
Alternatively, you can deploy a copy of the demo stack inside a [play-with-docker (PWD)](https://labs.play-with-docker.com) playground:
Portainer CE is updated regularly. We aim to do an update release every couple of months.
- 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.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
## Getting started
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
- [Documentation](https://documentation.portainer.io)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
## 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)
- [Portainer CE for Azure ACI](https://www.portainer.io/solutions/serverless-containers)
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
## 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
**NOTE**: You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
## 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/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.
* 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://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
## Limitations
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.
**_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to the latest version
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
## Licensing
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,70 +0,0 @@
package adminmonitor
import (
"context"
"log"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var logFatalf = log.Fatalf
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// 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,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
logFatalf("%s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
case <-m.shutdownCtx.Done():
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
}
}()
}
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
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
}

View File

@@ -1,50 +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_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_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
}

View File

@@ -1,53 +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).
**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,30 +0,0 @@
package apikey
import (
"crypto/rand"
"io"
portainer "github.com/portainer/portainer/api"
)
// APIKeyService represents a service for managing API keys.
type APIKeyService interface {
HashRaw(rawKey string) []byte
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 []byte) (portainer.User, portainer.APIKey, error)
UpdateAPIKey(apiKey *portainer.APIKey) error
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}
// generateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func generateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

View File

@@ -1,50 +0,0 @@
package apikey
import (
"testing"
"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 := 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 := 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 []byte) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(string(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 []byte, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(string(digest), entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest []byte) {
c.cache.Remove(string(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([]byte(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 []byte
found bool
}{
{
digest: []byte("foo"),
found: true,
},
{
digest: []byte(""),
found: true,
},
{
digest: []byte("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([]byte("bar"), portainer.User{ID: 2}, portainer.APIKey{})
keyCache.Set([]byte("foo"), portainer.User{ID: 1}, portainer.APIKey{})
// overwrite existing entry
keyCache.Set([]byte("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([]byte("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([]byte("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([]byte(key), portainer.User{ID: 1}, portainer.APIKey{})
}
for _, key := range test.foundKeys {
_, _, found := keyCache.Get([]byte(key))
is.True(found, "Key %s not found", key)
}
for _, key := range test.evictedKeys {
_, _, found := keyCache.Get([]byte(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,127 +0,0 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
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) []byte {
hashDigest := sha256.Sum256([]byte(rawKey))
return 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 := 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.CreateAPIKey(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.GetAPIKey(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 []byte) (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.User(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.UpdateAPIKey(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.GetAPIKey(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.DeleteAPIKey(apiKeyID)
}
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
return a.cache.InvalidateUserKeyCache(userId)
}

View File

@@ -1,309 +0,0 @@
package apikey
import (
"crypto/sha256"
"log"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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, generatedDigest[:])
})
}
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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.Println(apiKey)
log.Println(apiKeyGot)
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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, teardown := datastore.MustNewTestStore(true)
defer teardown()
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

@@ -1,119 +0,0 @@
package archive
import (
"archive/tar"
"compress/gzip"
"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 {
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = pathInArchive // use relative paths in archive
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
_, 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 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), 0744); 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: uknown type: %v in %s",
header.Typeflag,
header.Name)
}
}
return nil
}

View File

@@ -1,99 +0,0 @@
package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"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, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.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, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
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, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.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, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
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, _ := ioutil.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"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -20,94 +17,31 @@ func UnzipArchive(archiveData []byte, dest string) error {
}
for _, zipFile := range zipReader.File {
err := extractFileFromArchive(zipFile, dest)
f, err := zipFile.Open()
if err != nil {
return err
}
}
defer f.Close()
return nil
}
func extractFileFromArchive(file *zip.File, dest string) error {
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return err
}
fpath := filepath.Join(dest, file.Name)
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
_, err = io.Copy(outFile, bytes.NewReader(data))
if err != nil {
return err
}
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)
data, err := ioutil.ReadAll(f)
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")
fpath := filepath.Join(dest, zipFile.Name)
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
if err != nil {
return err
}
_, err = io.Copy(outFile, bytes.NewReader(data))
if err != nil {
return err
}
outFile.Close()
}
return nil

View File

@@ -1,32 +0,0 @@
package archive
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestUnzipFile(t *testing.T) {
dir, err := ioutil.TempDir("", "unzip-test-")
assert.NoError(t, err)
defer os.RemoveAll(dir)
/*
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,110 +0,0 @@
package backup
import (
"fmt"
"os"
"path"
"path/filepath"
"time"
"github.com/pkg/errors"
"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/sirupsen/logrus"
)
const rwxr__r__ os.FileMode = 0744
var filesToBackup = []string{
"certs",
"compose",
"config.json",
"custom_templates",
"edge_jobs",
"edge_stacks",
"extensions",
"portainer.key",
"portainer.pub",
"tls",
}
// 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 {
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
} else {
logrus.Debugf("exported to %s", exportFilename)
}
}
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 {
dbFileName := datastore.Connection().GetDatabaseFileName()
backupWriter, err := os.Create(filepath.Join(backupDirPath, dbFileName))
if err != nil {
return err
}
if err = datastore.BackupTo(backupWriter); err != nil {
return err
}
return backupWriter.Close()
}
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,85 +0,0 @@
package backup
import (
"context"
"io"
"os"
"path/filepath"
"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 = filesToBackup
// 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")
}
}
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")
}
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 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)
}

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

@@ -0,0 +1,248 @@
package bolt
import (
"log"
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"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/template"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
)
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
checkForDataMigration bool
fileService portainer.FileService
RoleService *role.Service
DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.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,
}
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if !databaseFileExists {
store.checkForDataMigration = false
} else {
store.checkForDataMigration = true
}
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
}
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == portainer.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,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TemplateService: store.TemplateService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
}
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
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
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
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
templateService, err := template.NewService(store.db)
if err != nil {
return err
}
store.TemplateService = templateService
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
}

View File

@@ -1,7 +1,10 @@
package dockerhub
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
@@ -12,22 +15,18 @@ const (
// Service represents a service for managing Dockerhub data.
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
db *bolt.DB
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
func NewService(db *bolt.DB) (*Service, error) {
err := internal.CreateBucket(db, BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
db: db,
}, nil
}
@@ -35,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := service.connection.GetObject(BucketName, []byte(dockerHubKey), &dockerhub)
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
@@ -45,5 +44,5 @@ func (service *Service) DockerHub() (*portainer.DockerHub, error) {
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub)
}

View File

@@ -0,0 +1,146 @@
package endpoint
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 = "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.UnmarshalObject(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,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)
}

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

@@ -0,0 +1,432 @@
package bolt
import portainer "github.com/portainer/portainer/api"
// Init creates the default data set.
func (store *Store) Init() error {
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{},
Tags: []string{},
}
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
if err != nil {
return err
}
}
roles, err := store.RoleService.Roles()
if err != nil {
return err
}
if len(roles) == 0 {
environmentAdministratorRole := &portainer.Role{
Name: "Endpoint administrator",
Description: "Full control of all resources in an endpoint",
Authorizations: map[portainer.Authorization]bool{
portainer.OperationDockerContainerArchiveInfo: true,
portainer.OperationDockerContainerList: true,
portainer.OperationDockerContainerExport: true,
portainer.OperationDockerContainerChanges: true,
portainer.OperationDockerContainerInspect: true,
portainer.OperationDockerContainerTop: true,
portainer.OperationDockerContainerLogs: true,
portainer.OperationDockerContainerStats: true,
portainer.OperationDockerContainerAttachWebsocket: true,
portainer.OperationDockerContainerArchive: true,
portainer.OperationDockerContainerCreate: true,
portainer.OperationDockerContainerPrune: true,
portainer.OperationDockerContainerKill: true,
portainer.OperationDockerContainerPause: true,
portainer.OperationDockerContainerUnpause: true,
portainer.OperationDockerContainerRestart: true,
portainer.OperationDockerContainerStart: true,
portainer.OperationDockerContainerStop: true,
portainer.OperationDockerContainerWait: true,
portainer.OperationDockerContainerResize: true,
portainer.OperationDockerContainerAttach: true,
portainer.OperationDockerContainerExec: true,
portainer.OperationDockerContainerRename: true,
portainer.OperationDockerContainerUpdate: true,
portainer.OperationDockerContainerPutContainerArchive: true,
portainer.OperationDockerContainerDelete: true,
portainer.OperationDockerImageList: true,
portainer.OperationDockerImageSearch: true,
portainer.OperationDockerImageGetAll: true,
portainer.OperationDockerImageGet: true,
portainer.OperationDockerImageHistory: true,
portainer.OperationDockerImageInspect: true,
portainer.OperationDockerImageLoad: true,
portainer.OperationDockerImageCreate: true,
portainer.OperationDockerImagePrune: true,
portainer.OperationDockerImagePush: true,
portainer.OperationDockerImageTag: true,
portainer.OperationDockerImageDelete: true,
portainer.OperationDockerImageCommit: true,
portainer.OperationDockerImageBuild: true,
portainer.OperationDockerNetworkList: true,
portainer.OperationDockerNetworkInspect: true,
portainer.OperationDockerNetworkCreate: true,
portainer.OperationDockerNetworkConnect: true,
portainer.OperationDockerNetworkDisconnect: true,
portainer.OperationDockerNetworkPrune: true,
portainer.OperationDockerNetworkDelete: true,
portainer.OperationDockerVolumeList: true,
portainer.OperationDockerVolumeInspect: true,
portainer.OperationDockerVolumeCreate: true,
portainer.OperationDockerVolumePrune: true,
portainer.OperationDockerVolumeDelete: true,
portainer.OperationDockerExecInspect: true,
portainer.OperationDockerExecStart: true,
portainer.OperationDockerExecResize: true,
portainer.OperationDockerSwarmInspect: true,
portainer.OperationDockerSwarmUnlockKey: true,
portainer.OperationDockerSwarmInit: true,
portainer.OperationDockerSwarmJoin: true,
portainer.OperationDockerSwarmLeave: true,
portainer.OperationDockerSwarmUpdate: true,
portainer.OperationDockerSwarmUnlock: true,
portainer.OperationDockerNodeList: true,
portainer.OperationDockerNodeInspect: true,
portainer.OperationDockerNodeUpdate: true,
portainer.OperationDockerNodeDelete: true,
portainer.OperationDockerServiceList: true,
portainer.OperationDockerServiceInspect: true,
portainer.OperationDockerServiceLogs: true,
portainer.OperationDockerServiceCreate: true,
portainer.OperationDockerServiceUpdate: true,
portainer.OperationDockerServiceDelete: true,
portainer.OperationDockerSecretList: true,
portainer.OperationDockerSecretInspect: true,
portainer.OperationDockerSecretCreate: true,
portainer.OperationDockerSecretUpdate: true,
portainer.OperationDockerSecretDelete: true,
portainer.OperationDockerConfigList: true,
portainer.OperationDockerConfigInspect: true,
portainer.OperationDockerConfigCreate: true,
portainer.OperationDockerConfigUpdate: true,
portainer.OperationDockerConfigDelete: true,
portainer.OperationDockerTaskList: true,
portainer.OperationDockerTaskInspect: true,
portainer.OperationDockerTaskLogs: true,
portainer.OperationDockerPluginList: true,
portainer.OperationDockerPluginPrivileges: true,
portainer.OperationDockerPluginInspect: true,
portainer.OperationDockerPluginPull: true,
portainer.OperationDockerPluginCreate: true,
portainer.OperationDockerPluginEnable: true,
portainer.OperationDockerPluginDisable: true,
portainer.OperationDockerPluginPush: true,
portainer.OperationDockerPluginUpgrade: true,
portainer.OperationDockerPluginSet: true,
portainer.OperationDockerPluginDelete: true,
portainer.OperationDockerSessionStart: true,
portainer.OperationDockerDistributionInspect: true,
portainer.OperationDockerBuildPrune: true,
portainer.OperationDockerBuildCancel: true,
portainer.OperationDockerPing: true,
portainer.OperationDockerInfo: true,
portainer.OperationDockerVersion: true,
portainer.OperationDockerEvents: true,
portainer.OperationDockerSystem: true,
portainer.OperationDockerUndefined: true,
portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseDelete: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationDockerAgentBrowsePut: true,
portainer.OperationDockerAgentBrowseRename: true,
portainer.OperationDockerAgentUndefined: true,
portainer.OperationPortainerResourceControlCreate: true,
portainer.OperationPortainerResourceControlUpdate: true,
portainer.OperationPortainerResourceControlDelete: true,
portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerStackCreate: true,
portainer.OperationPortainerStackMigrate: true,
portainer.OperationPortainerStackUpdate: true,
portainer.OperationPortainerStackDelete: true,
portainer.OperationPortainerWebsocketExec: true,
portainer.OperationPortainerWebhookList: true,
portainer.OperationPortainerWebhookCreate: true,
portainer.OperationPortainerWebhookDelete: true,
portainer.OperationIntegrationStoridgeAdmin: true,
portainer.EndpointResourcesAccess: true,
},
}
err = store.RoleService.CreateRole(environmentAdministratorRole)
if err != nil {
return err
}
environmentReadOnlyUserRole := &portainer.Role{
Name: "Helpdesk",
Description: "Read-only access of all resources in an endpoint",
Authorizations: map[portainer.Authorization]bool{
portainer.OperationDockerContainerArchiveInfo: true,
portainer.OperationDockerContainerList: true,
portainer.OperationDockerContainerChanges: true,
portainer.OperationDockerContainerInspect: true,
portainer.OperationDockerContainerTop: true,
portainer.OperationDockerContainerLogs: true,
portainer.OperationDockerContainerStats: true,
portainer.OperationDockerImageList: true,
portainer.OperationDockerImageSearch: true,
portainer.OperationDockerImageGetAll: true,
portainer.OperationDockerImageGet: true,
portainer.OperationDockerImageHistory: true,
portainer.OperationDockerImageInspect: true,
portainer.OperationDockerNetworkList: true,
portainer.OperationDockerNetworkInspect: true,
portainer.OperationDockerVolumeList: true,
portainer.OperationDockerVolumeInspect: true,
portainer.OperationDockerSwarmInspect: true,
portainer.OperationDockerNodeList: true,
portainer.OperationDockerNodeInspect: true,
portainer.OperationDockerServiceList: true,
portainer.OperationDockerServiceInspect: true,
portainer.OperationDockerServiceLogs: true,
portainer.OperationDockerSecretList: true,
portainer.OperationDockerSecretInspect: true,
portainer.OperationDockerConfigList: true,
portainer.OperationDockerConfigInspect: true,
portainer.OperationDockerTaskList: true,
portainer.OperationDockerTaskInspect: true,
portainer.OperationDockerTaskLogs: true,
portainer.OperationDockerPluginList: true,
portainer.OperationDockerDistributionInspect: true,
portainer.OperationDockerPing: true,
portainer.OperationDockerInfo: true,
portainer.OperationDockerVersion: true,
portainer.OperationDockerEvents: true,
portainer.OperationDockerSystem: true,
portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerWebhookList: true,
portainer.EndpointResourcesAccess: true,
},
}
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
if err != nil {
return err
}
standardUserRole := &portainer.Role{
Name: "Standard user",
Description: "Full control of assigned resources in an endpoint",
Authorizations: map[portainer.Authorization]bool{
portainer.OperationDockerContainerArchiveInfo: true,
portainer.OperationDockerContainerList: true,
portainer.OperationDockerContainerExport: true,
portainer.OperationDockerContainerChanges: true,
portainer.OperationDockerContainerInspect: true,
portainer.OperationDockerContainerTop: true,
portainer.OperationDockerContainerLogs: true,
portainer.OperationDockerContainerStats: true,
portainer.OperationDockerContainerAttachWebsocket: true,
portainer.OperationDockerContainerArchive: true,
portainer.OperationDockerContainerCreate: true,
portainer.OperationDockerContainerKill: true,
portainer.OperationDockerContainerPause: true,
portainer.OperationDockerContainerUnpause: true,
portainer.OperationDockerContainerRestart: true,
portainer.OperationDockerContainerStart: true,
portainer.OperationDockerContainerStop: true,
portainer.OperationDockerContainerWait: true,
portainer.OperationDockerContainerResize: true,
portainer.OperationDockerContainerAttach: true,
portainer.OperationDockerContainerExec: true,
portainer.OperationDockerContainerRename: true,
portainer.OperationDockerContainerUpdate: true,
portainer.OperationDockerContainerPutContainerArchive: true,
portainer.OperationDockerContainerDelete: true,
portainer.OperationDockerImageList: true,
portainer.OperationDockerImageSearch: true,
portainer.OperationDockerImageGetAll: true,
portainer.OperationDockerImageGet: true,
portainer.OperationDockerImageHistory: true,
portainer.OperationDockerImageInspect: true,
portainer.OperationDockerImageLoad: true,
portainer.OperationDockerImageCreate: true,
portainer.OperationDockerImagePush: true,
portainer.OperationDockerImageTag: true,
portainer.OperationDockerImageDelete: true,
portainer.OperationDockerImageCommit: true,
portainer.OperationDockerImageBuild: true,
portainer.OperationDockerNetworkList: true,
portainer.OperationDockerNetworkInspect: true,
portainer.OperationDockerNetworkCreate: true,
portainer.OperationDockerNetworkConnect: true,
portainer.OperationDockerNetworkDisconnect: true,
portainer.OperationDockerNetworkDelete: true,
portainer.OperationDockerVolumeList: true,
portainer.OperationDockerVolumeInspect: true,
portainer.OperationDockerVolumeCreate: true,
portainer.OperationDockerVolumeDelete: true,
portainer.OperationDockerExecInspect: true,
portainer.OperationDockerExecStart: true,
portainer.OperationDockerExecResize: true,
portainer.OperationDockerSwarmInspect: true,
portainer.OperationDockerSwarmUnlockKey: true,
portainer.OperationDockerSwarmInit: true,
portainer.OperationDockerSwarmJoin: true,
portainer.OperationDockerSwarmLeave: true,
portainer.OperationDockerSwarmUpdate: true,
portainer.OperationDockerSwarmUnlock: true,
portainer.OperationDockerNodeList: true,
portainer.OperationDockerNodeInspect: true,
portainer.OperationDockerNodeUpdate: true,
portainer.OperationDockerNodeDelete: true,
portainer.OperationDockerServiceList: true,
portainer.OperationDockerServiceInspect: true,
portainer.OperationDockerServiceLogs: true,
portainer.OperationDockerServiceCreate: true,
portainer.OperationDockerServiceUpdate: true,
portainer.OperationDockerServiceDelete: true,
portainer.OperationDockerSecretList: true,
portainer.OperationDockerSecretInspect: true,
portainer.OperationDockerSecretCreate: true,
portainer.OperationDockerSecretUpdate: true,
portainer.OperationDockerSecretDelete: true,
portainer.OperationDockerConfigList: true,
portainer.OperationDockerConfigInspect: true,
portainer.OperationDockerConfigCreate: true,
portainer.OperationDockerConfigUpdate: true,
portainer.OperationDockerConfigDelete: true,
portainer.OperationDockerTaskList: true,
portainer.OperationDockerTaskInspect: true,
portainer.OperationDockerTaskLogs: true,
portainer.OperationDockerPluginList: true,
portainer.OperationDockerPluginPrivileges: true,
portainer.OperationDockerPluginInspect: true,
portainer.OperationDockerPluginPull: true,
portainer.OperationDockerPluginCreate: true,
portainer.OperationDockerPluginEnable: true,
portainer.OperationDockerPluginDisable: true,
portainer.OperationDockerPluginPush: true,
portainer.OperationDockerPluginUpgrade: true,
portainer.OperationDockerPluginSet: true,
portainer.OperationDockerPluginDelete: true,
portainer.OperationDockerSessionStart: true,
portainer.OperationDockerDistributionInspect: true,
portainer.OperationDockerBuildPrune: true,
portainer.OperationDockerBuildCancel: true,
portainer.OperationDockerPing: true,
portainer.OperationDockerInfo: true,
portainer.OperationDockerVersion: true,
portainer.OperationDockerEvents: true,
portainer.OperationDockerSystem: true,
portainer.OperationDockerUndefined: true,
portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseDelete: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationDockerAgentBrowsePut: true,
portainer.OperationDockerAgentBrowseRename: true,
portainer.OperationDockerAgentUndefined: true,
portainer.OperationPortainerResourceControlCreate: true,
portainer.OperationPortainerResourceControlUpdate: true,
portainer.OperationPortainerResourceControlDelete: true,
portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerStackCreate: true,
portainer.OperationPortainerStackMigrate: true,
portainer.OperationPortainerStackUpdate: true,
portainer.OperationPortainerStackDelete: true,
portainer.OperationPortainerWebsocketExec: true,
portainer.OperationPortainerWebhookList: true,
portainer.OperationPortainerWebhookCreate: true,
},
}
err = store.RoleService.CreateRole(standardUserRole)
if err != nil {
return err
}
readOnlyUserRole := &portainer.Role{
Name: "Read-only user",
Description: "Read-only access of assigned resources in an endpoint",
Authorizations: map[portainer.Authorization]bool{
portainer.OperationDockerContainerArchiveInfo: true,
portainer.OperationDockerContainerList: true,
portainer.OperationDockerContainerChanges: true,
portainer.OperationDockerContainerInspect: true,
portainer.OperationDockerContainerTop: true,
portainer.OperationDockerContainerLogs: true,
portainer.OperationDockerContainerStats: true,
portainer.OperationDockerImageList: true,
portainer.OperationDockerImageSearch: true,
portainer.OperationDockerImageGetAll: true,
portainer.OperationDockerImageGet: true,
portainer.OperationDockerImageHistory: true,
portainer.OperationDockerImageInspect: true,
portainer.OperationDockerNetworkList: true,
portainer.OperationDockerNetworkInspect: true,
portainer.OperationDockerVolumeList: true,
portainer.OperationDockerVolumeInspect: true,
portainer.OperationDockerSwarmInspect: true,
portainer.OperationDockerNodeList: true,
portainer.OperationDockerNodeInspect: true,
portainer.OperationDockerServiceList: true,
portainer.OperationDockerServiceInspect: true,
portainer.OperationDockerServiceLogs: true,
portainer.OperationDockerSecretList: true,
portainer.OperationDockerSecretInspect: true,
portainer.OperationDockerConfigList: true,
portainer.OperationDockerConfigInspect: true,
portainer.OperationDockerTaskList: true,
portainer.OperationDockerTaskInspect: true,
portainer.OperationDockerTaskLogs: true,
portainer.OperationDockerPluginList: true,
portainer.OperationDockerDistributionInspect: true,
portainer.OperationDockerPing: true,
portainer.OperationDockerInfo: true,
portainer.OperationDockerVersion: true,
portainer.OperationDockerEvents: true,
portainer.OperationDockerSystem: true,
portainer.OperationDockerAgentPing: true,
portainer.OperationDockerAgentList: true,
portainer.OperationDockerAgentHostInfo: true,
portainer.OperationDockerAgentBrowseGet: true,
portainer.OperationDockerAgentBrowseList: true,
portainer.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerWebhookList: true,
},
}
err = store.RoleService.CreateRole(readOnlyUserRole)
if err != nil {
return err
}
}
return nil
}

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

@@ -0,0 +1,94 @@
package internal
import (
"encoding/binary"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
)
// 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 portainer.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.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id := bucket.Sequence()
identifier = int(id)
return nil
})
identifier++
return identifier
}

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

@@ -0,0 +1,15 @@
package internal
import (
"encoding/json"
)
// 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)
}

View File

@@ -0,0 +1,36 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"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 != portainer.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,35 @@
package migrator
import (
"strings"
"github.com/portainer/portainer/api"
)
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 {
legacyTemplates, err := m.templateService.Templates()
if err != nil {
return err
}
for _, template := range legacyTemplates {
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
err = m.templateService.UpdateTemplate(template.ID, &template)
if err != nil {
return err
}
}
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

@@ -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,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
}

View File

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

View File

@@ -0,0 +1,253 @@
package migrator
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
)
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
settingsService *settings.Service
stackService *stack.Service
templateService *template.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
}
// Parameters represents the required parameters to create a new Migrator instance.
Parameters struct {
DB *bolt.DB
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
TemplateService *template.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{
db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return err
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return err
}
err = m.updateTemplatesToVersion15()
if err != nil {
return err
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return err
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return err
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@@ -0,0 +1,95 @@
package registry
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 = "registries"
)
// 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
}
// Registry returns an registry by ID.
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 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 registry portainer.Registry
err := internal.UnmarshalObject(v, &registry)
if err != nil {
return err
}
registries = append(registries, registry)
}
return nil
})
return registries, err
}
// CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
registry.ID = portainer.RegistryID(id)
data, err := internal.MarshalObject(registry)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(registry.ID)), data)
})
}
// UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@@ -0,0 +1,134 @@
package resourcecontrol
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 = "resource_control"
)
// 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
}
// ResourceControl returns a ResourceControl object by ID
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
if err != nil {
return nil, err
}
return &resourceControl, nil
}
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
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 rc portainer.ResourceControl
err := internal.UnmarshalObject(v, &rc)
if err != nil {
return err
}
if rc.ResourceID == resourceID {
resourceControl = &rc
break
}
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = &rc
break
}
}
}
if resourceControl == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return resourceControl, err
}
// ResourceControls returns all the ResourceControl objects
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 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 resourceControl portainer.ResourceControl
err := internal.UnmarshalObject(v, &resourceControl)
if err != nil {
return err
}
rcs = append(rcs, resourceControl)
}
return nil
})
return rcs, err
}
// CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
resourceControl.ID = portainer.ResourceControlID(id)
data, err := internal.MarshalObject(resourceControl)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(resourceControl.ID)), data)
})
}
// UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl)
}
// DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

83
api/bolt/role/role.go Normal file
View File

@@ -0,0 +1,83 @@
package role
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 = "roles"
)
// 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
}
// Role returns a Role by ID
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
var set portainer.Role
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &set)
if err != nil {
return nil, err
}
return &set, nil
}
// Roles return an array containing all the sets.
func (service *Service) Roles() ([]portainer.Role, error) {
var sets = make([]portainer.Role, 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 set portainer.Role
err := internal.UnmarshalObject(v, &set)
if err != nil {
return err
}
sets = append(sets, set)
}
return nil
})
return sets, err
}
// CreateRole creates a new Role.
func (service *Service) CreateRole(set *portainer.Role) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
set.ID = portainer.RoleID(id)
data, err := internal.MarshalObject(set)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(set.ID)), data)
})
}

View File

@@ -0,0 +1,129 @@
package schedule
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 = "schedules"
)
// Service represents a service for managing schedule 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
}
// Schedule returns a schedule by ID.
func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule, error) {
var schedule portainer.Schedule
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
if err != nil {
return nil, err
}
return &schedule, nil
}
// UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, schedule)
}
// DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 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 schedule portainer.Schedule
err := internal.UnmarshalObject(v, &schedule)
if err != nil {
return err
}
schedules = append(schedules, schedule)
}
return nil
})
return schedules, err
}
// SchedulesByJobType return a array containing all the schedules
// with the specified JobType.
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 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 schedule portainer.Schedule
err := internal.UnmarshalObject(v, &schedule)
if err != nil {
return err
}
if schedule.JobType == jobType {
schedules = append(schedules, schedule)
}
}
return nil
})
return schedules, err
}
// CreateSchedule assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for schedules
err := bucket.SetSequence(uint64(schedule.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(schedule)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(schedule.ID)), data)
})
}
// GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@@ -0,0 +1,48 @@
package settings
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 = "settings"
settingsKey = "SETTINGS"
)
// 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
}
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings)
}

134
api/bolt/stack/stack.go Normal file
View File

@@ -0,0 +1,134 @@
package stack
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 = "stacks"
)
// 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
}
// Stack returns a stack object by ID.
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// StackByName returns a stack object by name.
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack
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 t portainer.Stack
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
stack = &t
break
}
}
if stack == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return stack, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 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.Stack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}
// CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks
err := bucket.SetSequence(uint64(stack.ID))
if err != nil {
return err
}
data, err := internal.MarshalObject(stack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(stack.ID)), data)
})
}
// UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack)
}
// DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

76
api/bolt/tag/tag.go Normal file
View File

@@ -0,0 +1,76 @@
package tag
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 = "tags"
)
// 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
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 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 tag portainer.Tag
err := internal.UnmarshalObject(v, &tag)
if err != nil {
return err
}
tags = append(tags, tag)
}
return nil
})
return tags, err
}
// CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
tag.ID = portainer.TagID(id)
data, err := internal.MarshalObject(tag)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(tag.ID)), data)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

126
api/bolt/team/team.go Normal file
View File

@@ -0,0 +1,126 @@
package team
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 = "teams"
)
// 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
}
// Team returns a Team by ID
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team)
if err != nil {
return nil, err
}
return &team, nil
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team
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 t portainer.Team
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
team = &t
break
}
}
if team == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return team, err
}
// Teams return an array containing all the teams.
func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 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 team portainer.Team
err := internal.UnmarshalObject(v, &team)
if err != nil {
return err
}
teams = append(teams, team)
}
return nil
})
return teams, err
}
// UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team)
}
// CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
team.ID = portainer.TeamID(id)
data, err := internal.MarshalObject(team)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(team.ID)), data)
})
}
// DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@@ -0,0 +1,197 @@
package teammembership
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 = "team_membership"
)
// 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
}
// TeamMembership returns a TeamMembership object by ID
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
var membership portainer.TeamMembership
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership)
if err != nil {
return nil, err
}
return &membership, nil
}
// TeamMemberships return an array containing all the TeamMembership objects.
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 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 membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
memberships = append(memberships, membership)
}
return nil
})
return memberships, err
}
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 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 membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 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 membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
memberships = append(memberships, membership)
}
}
return nil
})
return memberships, err
}
// UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership)
}
// CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
membership.ID = portainer.TeamMembershipID(id)
data, err := internal.MarshalObject(membership)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(membership.ID)), data)
})
}
// DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(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 membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.UserID == userID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(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 membership portainer.TeamMembership
err := internal.UnmarshalObject(v, &membership)
if err != nil {
return err
}
if membership.TeamID == teamID {
err := bucket.Delete(internal.Itob(int(membership.ID)))
if err != nil {
return err
}
}
}
return nil
})
}

View File

@@ -0,0 +1,95 @@
package template
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 = "templates"
)
// 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
}
// Templates return an array containing all the templates.
func (service *Service) Templates() ([]portainer.Template, error) {
var templates = make([]portainer.Template, 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 template portainer.Template
err := internal.UnmarshalObject(v, &template)
if err != nil {
return err
}
templates = append(templates, template)
}
return nil
})
return templates, err
}
// Template returns a template by ID.
func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) {
var template portainer.Template
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &template)
if err != nil {
return nil, err
}
return &template, nil
}
// CreateTemplate creates a new template.
func (service *Service) CreateTemplate(template *portainer.Template) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
template.ID = portainer.TemplateID(id)
data, err := internal.MarshalObject(template)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(template.ID)), data)
})
}
// UpdateTemplate saves a template.
func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, template)
}
// DeleteTemplate deletes a template.
func (service *Service) DeleteTemplate(ID portainer.TemplateID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

149
api/bolt/user/user.go Normal file
View File

@@ -0,0 +1,149 @@
package user
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 = "users"
)
// 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
}
// User returns a user by ID
func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UserByUsername returns a user by username.
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
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 u portainer.User
err := internal.UnmarshalObject(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
break
}
}
if user == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return user, err
}
// Users return an array containing all the users.
func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 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 user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
return users, err
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 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 user portainer.User
err := internal.UnmarshalObject(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
return users, err
}
// UpdateUser saves a user.
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, user)
}
// CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalObject(user)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(user.ID)), data)
})
}
// DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@@ -0,0 +1,66 @@
package version
import (
"strconv"
"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 = "version"
versionKey = "DB_VERSION"
)
// Service represents a service to manage stored versions.
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
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var data []byte
err := service.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey))
if value == nil {
return portainer.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
return strconv.Atoi(string(data))
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version))
return bucket.Put([]byte(versionKey), data)
})
}

151
api/bolt/webhook/webhook.go Normal file
View File

@@ -0,0 +1,151 @@
package webhook
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 = "webhooks"
)
// Service represents a service for managing webhook 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
}
//Webhooks returns an array of all webhooks
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 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 webhook portainer.Webhook
err := internal.UnmarshalObject(v, &webhook)
if err != nil {
return err
}
webhooks = append(webhooks, webhook)
}
return nil
})
return webhooks, err
}
// Webhook returns a webhook by ID.
func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, error) {
var webhook portainer.Webhook
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &webhook)
if err != nil {
return nil, err
}
return &webhook, nil
}
// WebhookByResourceID returns a webhook by the ResourceID it is associated with.
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
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 w portainer.Webhook
err := internal.UnmarshalObject(v, &w)
if err != nil {
return err
}
if w.ResourceID == ID {
webhook = &w
break
}
}
if webhook == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return webhook, err
}
// WebhookByToken returns a webhook by the random token it is associated with.
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook
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 w portainer.Webhook
err := internal.UnmarshalObject(v, &w)
if err != nil {
return err
}
if w.Token == token {
webhook = &w
break
}
}
if webhook == nil {
return portainer.ErrObjectNotFound
}
return nil
})
return webhook, err
}
// DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
webhook.ID = portainer.WebhookID(id)
data, err := internal.MarshalObject(webhook)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(webhook.ID)), data)
})
}

View File

@@ -1,24 +0,0 @@
package chisel
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
)
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
// The key represents the following data in this particular format:
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
// The key returned by this function is a base64 encoded version of the data.
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
keyInformation := []string{
url,
fmt.Sprintf("%s:%s", host, service.serverPort),
service.serverFingerprint,
strconv.Itoa(endpointIdentifier),
}
key := strings.Join(keyInformation, "|")
return base64.RawStdEncoding.EncodeToString([]byte(key))
}

View File

@@ -1,47 +0,0 @@
package chisel
import (
"strconv"
portainer "github.com/portainer/portainer/api"
)
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
tunnel := service.GetTunnelDetails(endpointID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
break
}
}
if existingJobIndex == -1 {
tunnel.Jobs = append(tunnel.Jobs, *edgeJob)
} else {
tunnel.Jobs[existingJobIndex] = *edgeJob
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnelDetails := item.Val.(*portainer.TunnelDetails)
updatedJobs := make([]portainer.EdgeJob, 0)
for _, edgeJob := range tunnelDetails.Jobs {
if edgeJob.ID == edgeJobID {
continue
}
updatedJobs = append(updatedJobs, edgeJob)
}
tunnelDetails.Jobs = updatedJobs
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
}
}

43
api/chisel/server.go Normal file
View File

@@ -0,0 +1,43 @@
package chisel
import (
chserver "github.com/jpillora/chisel/server"
)
type Server struct {
address string
port string
fingerprint string
}
func NewServer(address string, port string) *Server {
return &Server{
address: address,
port: port,
}
}
// Start starts the reverse tunnel server
func (server *Server) Start() error {
// TODO: keyseed management (persistence)
// + auth management
// Consider multiple users for auth?
config := &chserver.Config{
Reverse: true,
KeySeed: "keyseedexample",
Auth: "agent@randomstring",
}
chiselServer, err := chserver.NewServer(config)
if err != nil {
return err
}
server.fingerprint = chiselServer.GetFingerprint()
return chiselServer.Start(server.address, server.port)
}
func (server *Server) GetFingerprint() string {
return server.fingerprint
}

View File

@@ -1,246 +0,0 @@
package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"strconv"
"time"
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
const (
tunnelCleanupInterval = 10 * time.Second
requiredTimeout = 15 * time.Second
activeTimeout = 4*time.Minute + 30*time.Second
)
// Service represents a service to manage the state of multiple reverse tunnels.
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
// connected to the tunnel server.
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap cmap.ConcurrentMap
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
}
// pingAgent ping the given agent so that the agent can keep the tunnel alive
func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
tunnel := service.GetTunnelDetails(endpointID)
requestURL := fmt.Sprintf("http://127.0.0.1:%d/ping", tunnel.Port)
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
if err != nil {
return err
}
httpClient := &http.Client{
Timeout: 3 * time.Second,
}
_, err = httpClient.Do(req)
if err != nil {
return err
}
return nil
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
go func() {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: start for %.0f minutes]\n", endpointID, maxAlive.Minutes())
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
for {
select {
case <-pingTicker.C:
service.SetTunnelStatusToActive(endpointID)
err := service.pingAgent(endpointID)
if err != nil {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [warning: ping agent err=%s]\n", endpointID, err)
}
case <-maxAliveTicker.C:
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as %.0f minutes timeout]\n", endpointID, maxAlive.Minutes())
return
case <-ctx.Done():
err := ctx.Err()
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as err=%s]\n", endpointID, err)
return
}
}
}()
}
// StartTunnelServer starts a tunnel server on the specified addr and port.
// It uses a seed to generate a new private/public key pair. If the seed cannot
// be found inside the database, it will generate a new one randomly and persist it.
// It starts the tunnel status verification process in the background.
// The snapshotter is used in the tunnel status verification process.
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
keySeed, err := service.retrievePrivateKeySeed()
if err != nil {
return err
}
config := &chserver.Config{
Reverse: true,
KeySeed: keySeed,
}
chiselServer, err := chserver.NewServer(config)
if err != nil {
return err
}
service.serverFingerprint = chiselServer.GetFingerprint()
service.serverPort = port
err = chiselServer.Start(addr, port)
if err != nil {
return err
}
service.chiselServer = chiselServer
// TODO: work-around Chisel default behavior.
// By default, Chisel will allow anyone to connect if no user exists.
username, password := generateRandomCredentials()
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
if err != nil {
return err
}
service.snapshotService = snapshotService
go service.startTunnelVerificationLoop()
return nil
}
// StopTunnelServer stops tunnel http server
func (service *Service) StopTunnelServer() error {
return service.chiselServer.Close()
}
func (service *Service) retrievePrivateKeySeed() (string, error) {
var serverInfo *portainer.TunnelServerInfo
serverInfo, err := service.dataStore.TunnelServer().Info()
if service.dataStore.IsErrObjectNotFound(err) {
keySeed := uniuri.NewLen(16)
serverInfo = &portainer.TunnelServerInfo{
PrivateKeySeed: keySeed,
}
err := service.dataStore.TunnelServer().UpdateInfo(serverInfo)
if err != nil {
return "", err
}
} else if err != nil {
return "", err
}
return serverInfo.PrivateKeySeed, nil
}
func (service *Service) startTunnelVerificationLoop() {
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
ticker := time.NewTicker(tunnelCleanupInterval)
for {
select {
case <-ticker.C:
service.checkTunnels()
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] Shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Printf("Stopped tunnel service: %s", err)
}
ticker.Stop()
return
}
}
}
func (service *Service) checkTunnels() {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
}
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
}
}
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
}
}
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
endpointURL := endpoint.URL
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
err = service.snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
return err
}
endpoint.URL = endpointURL
return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
}

View File

@@ -1,178 +0,0 @@
package chisel
import (
"encoding/base64"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/portainer/libcrypto"
"github.com/dchest/uniuri"
portainer "github.com/portainer/portainer/api"
)
const (
minAvailablePort = 49152
maxAvailablePort = 65535
)
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
port := randomInt(minAvailablePort, maxAvailablePort)
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
if tunnel.Port == port {
return service.getUnusedPort()
}
}
return port
}
func randomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
key := strconv.Itoa(int(endpointID))
if item, ok := service.tunnelDetailsMap.Get(key); ok {
tunnelDetails := item.(*portainer.TunnelDetails)
return tunnelDetails
}
jobs := make([]portainer.EdgeJob, 0)
return &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
Port: 0,
Jobs: jobs,
Credentials: "",
}
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
// update the LastActivity
service.SetTunnelStatusToActive(endpoint.ID)
}
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
credentials := tunnel.Credentials
if credentials != "" {
tunnel.Credentials = ""
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.ProxyManager.DeleteEndpointProxy(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to REQUIRED.
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
tunnel := service.GetTunnelDetails(endpointID)
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
tunnel.Status = portainer.EdgeAgentManagementRequired
tunnel.Port = service.getUnusedPort()
tunnel.LastActivity = time.Now()
username, password := generateRandomCredentials()
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
err = service.chiselServer.AddUser(username, password, authorizedRemote)
if err != nil {
return err
}
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
if err != nil {
return err
}
tunnel.Credentials = credentials
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
return nil
}
func generateRandomCredentials() (string, string) {
username := uniuri.NewLen(8)
password := uniuri.NewLen(8)
return username, password
}
func encryptCredentials(username, password, key string) (string, error) {
credentials := fmt.Sprintf("%s:%s", username, password)
encryptedCredentials, err := libcrypto.Encrypt([]byte(credentials), []byte(key))
if err != nil {
return "", err
}
return base64.RawStdEncoding.EncodeToString(encryptedCredentials), nil
}

View File

@@ -1,11 +1,9 @@
package cli
import (
"errors"
"log"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"os"
"path/filepath"
@@ -17,11 +15,16 @@ import (
// Service implements the CLIService interface
type Service struct{}
var (
errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
const (
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
@@ -29,38 +32,32 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
InitialMmapSize: kingpin.Flag("initial-mmap-size", "Initial mmap size of the database in bytes").Int(),
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(),
}
kingpin.Parse()
@@ -79,9 +76,26 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpointURL(*flags.EndpointURL)
err := validateTemplateFile(*flags.TemplateFile)
if err != nil {
return err
}
err = validateEndpointURL(*flags.EndpointURL)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
if err != nil {
return err
}
@@ -91,6 +105,10 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return err
}
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
return errNoAuthExcludeAdminPassword
}
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return errAdminPassExcludeAdminPassFile
}
@@ -98,16 +116,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return nil
}
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.NoAnalytics {
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
}
if *flags.SSL {
log.Println("Warning: SSL is enabled by default and there is no need for the --ssl flag. It has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
}
}
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
@@ -128,8 +136,40 @@ func validateEndpointURL(endpointURL string) error {
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
return nil
}
func validateTemplateFile(templateFile string) error {
if _, err := os.Stat(templateFile); err != nil {
if os.IsNotExist(err) {
return errTemplateFileNotFound
}
return err
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != "" {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval

View File

@@ -1,24 +0,0 @@
package cli
import (
"bufio"
"log"
"os"
"strings"
)
// Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false, err
}
answer = strings.Replace(answer, "\n", "", -1)
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes", nil
}

View File

@@ -1,25 +1,25 @@
//go:build !windows
// +build !windows
package cli
const (
defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
)

View File

@@ -2,22 +2,22 @@ package cli
const (
defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLS = "false"
defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSyncInterval = "60s"
defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
defaultTemplateFile = "/templates.json"
)

View File

@@ -1,12 +1,11 @@
package cli
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"fmt"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
type pairList []portainer.Pair

View File

@@ -1,45 +0,0 @@
package cli
import (
portainer "github.com/portainer/portainer/api"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
type pairListBool []portainer.Pair
// Set implementation for a list of portainer.Pair
func (l *pairListBool) Set(value string) error {
p := new(portainer.Pair)
// default to true. example setting=true is equivalent to setting
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
p.Name = parts[0]
p.Value = "true"
} else {
p.Name = parts[0]
p.Value = parts[1]
}
*l = append(*l, *p)
return nil
}
// String implementation for a list of pair
func (l *pairListBool) String() string {
return ""
}
// IsCumulative implementation for a list of pair
func (l *pairListBool) IsCumulative() bool {
return true
}
func BoolPairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairListBool)(target))
return
}

View File

@@ -1,29 +0,0 @@
package main
import (
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/sirupsen/logrus"
)
func importFromJson(fileService portainer.FileService, store *datastore.Store) {
// EXPERIMENTAL - if used with an incomplete json file, it will fail, as we don't have a way to default the model values
importFile := "/data/import.json"
if exists, _ := fileService.FileExists(importFile); exists {
if err := store.Import(importFile); err != nil {
logrus.WithError(err).Debugf("Import %s failed", importFile)
// TODO: should really rollback on failure, but then we have nothing.
} else {
logrus.Printf("Successfully imported %s to new portainer database", importFile)
}
// TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatalf("Failed initializing data store: %v", err)
}
}
}

View File

@@ -1,42 +0,0 @@
package main
import (
"fmt"
"log"
"strings"
"github.com/sirupsen/logrus"
)
type portainerFormatter struct {
logrus.TextFormatter
}
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var levelColor int
switch entry.Level {
case logrus.DebugLevel, logrus.TraceLevel:
levelColor = 31 // gray
case logrus.WarnLevel:
levelColor = 33 // yellow
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
levelColor = 31 // red
default:
levelColor = 36 // blue
}
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
}
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatterLogrus)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}

File diff suppressed because it is too large Load Diff

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