Compare commits
228 Commits
test/node_
...
revert-618
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e9ba29e4d | ||
|
|
f360392d39 | ||
|
|
fa44a62c4a | ||
|
|
2a384d4c64 | ||
|
|
b6fbf8eecc | ||
|
|
69c17986d9 | ||
|
|
120584909c | ||
|
|
c24dc3112b | ||
|
|
1e80061186 | ||
|
|
c267355759 | ||
|
|
47c1af93ea | ||
|
|
ab0849d0f3 | ||
|
|
3f31d4b00b | ||
|
|
18c323185e | ||
|
|
7768d27cfc | ||
|
|
97b8da9d10 | ||
|
|
0928d1832d | ||
|
|
d091b343b9 | ||
|
|
2555dfc78b | ||
|
|
761d2a11d3 | ||
|
|
6255e8d4b5 | ||
|
|
830286c332 | ||
|
|
9ad626b36e | ||
|
|
a598b2d72d | ||
|
|
6be1ff4d9c | ||
|
|
c0a4727114 | ||
|
|
cea634a7aa | ||
|
|
5f2e3452e4 | ||
|
|
aa15b34add | ||
|
|
06d25d1491 | ||
|
|
8e83a95996 | ||
|
|
17a20cb2c6 | ||
|
|
b596d0febd | ||
|
|
33871eb447 | ||
|
|
183304853e | ||
|
|
0042c7c1d9 | ||
|
|
80af93afec | ||
|
|
988069df56 | ||
|
|
0ee403c1b2 | ||
|
|
b280eb6997 | ||
|
|
761e102b2f | ||
|
|
5bd157f8fc | ||
|
|
bcaf20caca | ||
|
|
1a6af5d58f | ||
|
|
41993ad378 | ||
|
|
6b91a813f0 | ||
|
|
d64cab0c50 | ||
|
|
048613a0c5 | ||
|
|
22b72fb6e3 | ||
|
|
7d92aa1971 | ||
|
|
9e9a4ca4cc | ||
|
|
a2886115b8 | ||
|
|
cc3b1face2 | ||
|
|
1157849b70 | ||
|
|
98b8d6d0b2 | ||
|
|
e126f63965 | ||
|
|
af0d637414 | ||
|
|
ebfabe6c47 | ||
|
|
85a6a80722 | ||
|
|
b285219a58 | ||
|
|
3fb8a232b8 | ||
|
|
28f71e486a | ||
|
|
c763219f74 | ||
|
|
8f4589e535 | ||
|
|
0caf5ca59e | ||
|
|
cec8f34ae9 | ||
|
|
71de07bbea | ||
|
|
76ced401f0 | ||
|
|
33001a8654 | ||
|
|
f738af0f34 | ||
|
|
5c85c563e1 | ||
|
|
db00390cd2 | ||
|
|
32756f9e1b | ||
|
|
5ba80c3a44 | ||
|
|
77f73378ea | ||
|
|
734f077861 | ||
|
|
b5ec8c52fb | ||
|
|
988efe6b02 | ||
|
|
40a6645e23 | ||
|
|
cf60235696 | ||
|
|
65cc5342a7 | ||
|
|
90a18b5ded | ||
|
|
b29961e01e | ||
|
|
d17e7c8160 | ||
|
|
d3cc1a24cc | ||
|
|
fb7cdacbaa | ||
|
|
ec24826228 | ||
|
|
f0efc4f904 | ||
|
|
d18c8d0e88 | ||
|
|
4f350ab6f5 | ||
|
|
623079442f | ||
|
|
1ff5f25e40 | ||
|
|
ff87e687ec | ||
|
|
d4fd295c86 | ||
|
|
62f418836f | ||
|
|
ce5ea28727 | ||
|
|
00c7464c25 | ||
|
|
5eced421d5 | ||
|
|
006634e007 | ||
|
|
3cde10bcac | ||
|
|
9dcd5651e8 | ||
|
|
ba1f0f4018 | ||
|
|
41999e149f | ||
|
|
dfe0b3f69d | ||
|
|
588ce549ad | ||
|
|
edb25ee10d | ||
|
|
12e7aa6b60 | ||
|
|
f544d4447c | ||
|
|
158cdf596a | ||
|
|
3d6c6e2604 | ||
|
|
1ee363f8c9 | ||
|
|
109b27594a | ||
|
|
54d47ebc76 | ||
|
|
e6d690e31e | ||
|
|
6a67e8142d | ||
|
|
d93d88fead | ||
|
|
8383bc05c5 | ||
|
|
685552a661 | ||
|
|
1b0e58a4e8 | ||
|
|
0200a668df | ||
|
|
151dfe7e65 | ||
|
|
dcd1e902cd | ||
|
|
ed89587cb9 | ||
|
|
c93ec8d08c | ||
|
|
dad762de9f | ||
|
|
661931d8b0 | ||
|
|
b7841e7fc3 | ||
|
|
84e57cebc9 | ||
|
|
8096c5e8bc | ||
|
|
fd9427cd0b | ||
|
|
e60dbba93b | ||
|
|
551d287982 | ||
|
|
8421113d49 | ||
|
|
6bd72d21a8 | ||
|
|
fc4ff59bfd | ||
|
|
885ae16278 | ||
|
|
cd651f2cba | ||
|
|
328abfd74e | ||
|
|
fbcf67bc1e | ||
|
|
7fb2e44146 | ||
|
|
0cb5656db6 | ||
|
|
e4fd43e4fc | ||
|
|
34c2a16363 | ||
|
|
0f33e4ae99 | ||
|
|
75071dfade | ||
|
|
34f6e11f1d | ||
|
|
2ecc8ab5c9 | ||
|
|
fce885901f | ||
|
|
fe8f50512c | ||
|
|
e3b6e4a1d3 | ||
|
|
01529203f1 | ||
|
|
af98660a55 | ||
|
|
50f63ae865 | ||
|
|
7b72130433 | ||
|
|
7611cc415a | ||
|
|
9045e17cba | ||
|
|
46ffca92fd | ||
|
|
f0a88b7367 | ||
|
|
7437006359 | ||
|
|
9c80501738 | ||
|
|
377326085d | ||
|
|
03d34076d8 | ||
|
|
09cf4c1bbe | ||
|
|
9c279e7fae | ||
|
|
db04bc9f38 | ||
|
|
7d40a83d03 | ||
|
|
d4f581a596 | ||
|
|
5ad3cacefd | ||
|
|
6ac9c4367e | ||
|
|
8aa03bb81b | ||
|
|
d14c7b0309 | ||
|
|
cbeb13636c | ||
|
|
a6138dd5a3 | ||
|
|
5752e74be6 | ||
|
|
cb37497444 | ||
|
|
0b64250647 | ||
|
|
45af1f3d8b | ||
|
|
fc52830c7d | ||
|
|
4890f50443 | ||
|
|
6d510c4f30 | ||
|
|
cad530ec04 | ||
|
|
e63732484a | ||
|
|
ec3233fb09 | ||
|
|
bcdc342cbd | ||
|
|
e1f725d01a | ||
|
|
b876f2d17d | ||
|
|
b0ec67826c | ||
|
|
b89d828878 | ||
|
|
e59df8134d | ||
|
|
092d217985 | ||
|
|
ad94162019 | ||
|
|
0efbf5bbf3 | ||
|
|
c26ba23c53 | ||
|
|
69096f664d | ||
|
|
48c762c98b | ||
|
|
488d86d200 | ||
|
|
f10e0e4124 | ||
|
|
5316cca3de | ||
|
|
4267304e50 | ||
|
|
deecbadce1 | ||
|
|
ecc9813750 | ||
|
|
24f11902b2 | ||
|
|
33118babdd | ||
|
|
2aec348814 | ||
|
|
4d63459d67 | ||
|
|
483559af09 | ||
|
|
1796545d2e | ||
|
|
a50795063c | ||
|
|
7c9f7a2a8b | ||
|
|
af8065e8c2 | ||
|
|
49d2c68a19 | ||
|
|
dc769b4c4d | ||
|
|
50393519ba | ||
|
|
dd808bb7bd | ||
|
|
16dc58a5f1 | ||
|
|
d911c50f1b | ||
|
|
f6f31b8872 | ||
|
|
414f2c8c60 | ||
|
|
1f4a7b32e3 | ||
|
|
689c2193c0 | ||
|
|
a781021072 | ||
|
|
9121e8e69c | ||
|
|
53a2205f06 | ||
|
|
9492e30dc2 | ||
|
|
d2cbdf935a | ||
|
|
a098e24cca | ||
|
|
05efac44f6 | ||
|
|
555c9f238f |
13
.babelrc
13
.babelrc
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"plugins": ["lodash", "angularjs-annotate"],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": "2"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -17,12 +17,76 @@ plugins:
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
sourceType: module
|
||||
project: './tsconfig.json'
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
|
||||
rules:
|
||||
no-control-regex: off
|
||||
no-control-regex: 'off'
|
||||
no-empty: warn
|
||||
no-empty-function: warn
|
||||
no-useless-escape: off
|
||||
import/order: error
|
||||
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'],
|
||||
},
|
||||
]
|
||||
|
||||
settings:
|
||||
'import/resolver':
|
||||
alias:
|
||||
map:
|
||||
- ['@', './app']
|
||||
extensions: ['.js', '.ts', '.tsx']
|
||||
|
||||
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'
|
||||
- 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
|
||||
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' }]
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
- 'plugin:jest/recommended'
|
||||
- 'plugin:jest/style'
|
||||
env:
|
||||
'jest/globals': true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -30,7 +30,7 @@ A clear and concise description of what you expected to happen.
|
||||
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
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.
|
||||
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,9 +3,11 @@ bower_components
|
||||
dist
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
storybook-static
|
||||
.tmp
|
||||
**/.vscode/settings.json
|
||||
**/.vscode/tasks.json
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
__debug_bin
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
dist
|
||||
14
.prettierrc
14
.prettierrc
@@ -4,10 +4,20 @@
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.{j,t}sx"
|
||||
],
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
31
.storybook/main.js
Normal file
31
.storybook/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
11
.storybook/preview.js
Normal file
11
.storybook/preview.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import '../app/assets/css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -150,6 +150,7 @@
|
||||
"// @description ",
|
||||
"// @description **Access policy**: ",
|
||||
"// @tags ",
|
||||
"// @security ApiKeyAuth",
|
||||
"// @security jwt",
|
||||
"// @accept json",
|
||||
"// @produce json",
|
||||
|
||||
@@ -75,7 +75,7 @@ The feature request process is similar to the bug report process but has an extr
|
||||
|
||||

|
||||
|
||||
## Build Portainer locally
|
||||
## Build and run Portainer locally
|
||||
|
||||
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
||||
|
||||
@@ -85,7 +85,7 @@ Install dependencies with yarn:
|
||||
$ yarn
|
||||
```
|
||||
|
||||
Then build and run the project:
|
||||
Then build and run the project in a Docker container:
|
||||
|
||||
```sh
|
||||
$ yarn start
|
||||
@@ -95,6 +95,14 @@ 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:
|
||||
@@ -112,6 +120,7 @@ When adding a new route to an existing handler use the following as a template (
|
||||
// @description
|
||||
// @description **Access policy**:
|
||||
// @tags
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
23
README.md
23
README.md
@@ -2,13 +2,15 @@
|
||||
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
|
||||
</p>
|
||||
|
||||
**Portainer CE** is a lightweight ‘universal’ management GUI that can be used to **easily** manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as **simple** to deploy as it is to use.
|
||||
**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.
|
||||
|
||||
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** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
|
||||
**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.
|
||||
|
||||
A fully supported version of Portainer is available for business use. Visit http://www.portainer.io to learn more
|
||||
- [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)
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -20,12 +22,11 @@ Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
Portainer CE is updated regularly. We aim to do an update release every couple of months.
|
||||
|
||||
**The latest version of Portainer is 2.6.x** And you can find the release notes [here.](https://www.portainer.io/blog/new-portainer-ce-2.6.0-release)
|
||||
Portainer is on version 2, the second number denotes the month of release.
|
||||
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [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/)
|
||||
|
||||
@@ -41,25 +42,25 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
|
||||
|
||||
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
||||
|
||||
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
|
||||
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.io/slack/
|
||||
- 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.
|
||||
|
||||
## 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. We need all the help we can get!
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
|
||||
|
||||
## 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
|
||||
## 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 we will be in touch.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ You can find out more about Portainer at [http://portainer.io](http://portainer.
|
||||
|
||||
# Authentication
|
||||
|
||||
Most of the API endpoints require to be authenticated as well as some level of authorization to be used.
|
||||
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.
|
||||
|
||||
@@ -16,7 +16,7 @@ Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIs
|
||||
|
||||
# Security
|
||||
|
||||
Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
|
||||
Each API environment(endpoint) has an associated access policy, it is documented in the description of each environment(endpoint).
|
||||
|
||||
Different access policies are available:
|
||||
|
||||
@@ -27,27 +27,27 @@ Different access policies are available:
|
||||
|
||||
### Public access
|
||||
|
||||
No authentication is required to access the endpoints with this access policy.
|
||||
No authentication is required to access the environments(endpoints) with this access policy.
|
||||
|
||||
### Authenticated access
|
||||
|
||||
Authentication is required to access the endpoints with this access policy.
|
||||
Authentication is required to access the environments(endpoints) with this access policy.
|
||||
|
||||
### Restricted access
|
||||
|
||||
Authentication is required to access the endpoints with this access policy.
|
||||
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 endpoints with this access policy.
|
||||
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 endpoints to manage your Docker resources (create a container, remove a volume, etc...).
|
||||
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 endpoint (which is not documented below due to Swagger limitations). This endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker 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/).
|
||||
|
||||
29
api/apikey/apikey.go
Normal file
29
api/apikey/apikey.go
Normal file
@@ -0,0 +1,29 @@
|
||||
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)
|
||||
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
|
||||
}
|
||||
50
api/apikey/apikey_test.go
Normal file
50
api/apikey/apikey_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
69
api/apikey/cache.go
Normal file
69
api/apikey/cache.go
Normal file
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
181
api/apikey/cache_test.go
Normal file
181
api/apikey/cache_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
121
api/apikey/service.go
Normal file
121
api/apikey/service.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const portainerAPIKeyPrefix = "ptr_"
|
||||
|
||||
var ErrInvalidAPIKey = errors.New("Invalid API key")
|
||||
|
||||
type apiKeyService struct {
|
||||
apiKeyRepository portainer.APIKeyRepository
|
||||
userRepository portainer.UserService
|
||||
cache *apiKeyCache
|
||||
}
|
||||
|
||||
func NewAPIKeyService(apiKeyRepository portainer.APIKeyRepository, userRepository portainer.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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
289
api/apikey/service_test.go
Normal file
289
api/apikey/service_test.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"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 := bolt.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_GetAPIKeys(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.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 := bolt.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 := bolt.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().CreateUser(&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 := bolt.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 := bolt.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)
|
||||
})
|
||||
}
|
||||
137
api/bolt/apikeyrepository/apikeyrepository.go
Normal file
137
api/bolt/apikeyrepository/apikeyrepository.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package apikeyrepository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"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 = "api_key"
|
||||
)
|
||||
|
||||
// Service represents a service for managing api-key data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAPIKeysByUserID returns a slice containing all the APIKeys a user has access to.
|
||||
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
|
||||
var result = make([]portainer.APIKey, 0)
|
||||
|
||||
err := service.connection.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 record portainer.APIKey
|
||||
err := internal.UnmarshalObject(v, &record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if record.UserID == userID {
|
||||
result = append(result, record)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetAPIKeyByDigest returns the API key for the associated digest.
|
||||
// Note: there is a 1-to-1 mapping of api-key and digest
|
||||
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
|
||||
var result portainer.APIKey
|
||||
|
||||
err := service.connection.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 record portainer.APIKey
|
||||
err := internal.UnmarshalObject(v, &record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bytes.Equal(record.Digest, digest) {
|
||||
result = record
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// CreateAPIKey creates a new APIKey object.
|
||||
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
record.ID = portainer.APIKeyID(id)
|
||||
|
||||
data, err := internal.MarshalObject(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(record.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// GetAPIKey retrieves an existing APIKey object by api key ID.
|
||||
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
||||
var apiKey *portainer.APIKey
|
||||
|
||||
err := service.connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
item := bucket.Get(internal.Itob(int(keyID)))
|
||||
if item == nil {
|
||||
return errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
err := internal.UnmarshalObject(item, &apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return apiKey, err
|
||||
}
|
||||
|
||||
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
|
||||
identifier := internal.Itob(int(key.ID))
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, key)
|
||||
}
|
||||
|
||||
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
return bucket.Delete(internal.Itob(int(ID)))
|
||||
})
|
||||
}
|
||||
142
api/bolt/backup.go
Normal file
142
api/bolt/backup.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
)
|
||||
|
||||
var backupDefaults = struct {
|
||||
backupDir string
|
||||
commonDir string
|
||||
databaseFileName string
|
||||
}{
|
||||
"backups",
|
||||
"common",
|
||||
databaseFileName,
|
||||
}
|
||||
|
||||
var backupLog = plog.NewScopedLog("bolt, backup")
|
||||
|
||||
//
|
||||
// Backup Helpers
|
||||
//
|
||||
|
||||
// createBackupFolders create initial folders for backups
|
||||
func (store *Store) createBackupFolders() {
|
||||
// create common dir
|
||||
commonDir := store.commonBackupDir()
|
||||
if exists, _ := store.fileService.FileExists(commonDir); !exists {
|
||||
if err := os.MkdirAll(commonDir, 0700); err != nil {
|
||||
backupLog.Error("Error while creating common backup folder", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) databasePath() string {
|
||||
return path.Join(store.path, databaseFileName)
|
||||
}
|
||||
|
||||
func (store *Store) commonBackupDir() string {
|
||||
return path.Join(store.path, backupDefaults.backupDir, backupDefaults.commonDir)
|
||||
}
|
||||
|
||||
func (store *Store) copyDBFile(from string, to string) error {
|
||||
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
|
||||
err := store.fileService.Copy(from, to, true)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BackupOptions provide a helper to inject backup options
|
||||
type BackupOptions struct {
|
||||
Version int
|
||||
BackupDir string
|
||||
BackupFileName string
|
||||
BackupPath string
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
}
|
||||
if options.Version == 0 {
|
||||
options.Version, _ = store.version()
|
||||
}
|
||||
if options.BackupDir == "" {
|
||||
options.BackupDir = store.commonBackupDir()
|
||||
}
|
||||
if options.BackupFileName == "" {
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
|
||||
}
|
||||
if options.BackupPath == "" {
|
||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// BackupWithOptions backup current database with options
|
||||
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
|
||||
backupLog.Info("creating db backup")
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
// Restore strategies:
|
||||
// - default: restore latest from current edition
|
||||
// - restore a specific
|
||||
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
|
||||
options = store.setupOptions(options)
|
||||
|
||||
// Check if backup file exist before restoring
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
backupLog.Error("Error while closing store before restore", err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info("Restoring db backup")
|
||||
err = store.copyDBFile(options.BackupPath, store.databasePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Open()
|
||||
}
|
||||
|
||||
// RemoveWithOptions removes backup database based on supplied options
|
||||
func (store *Store) RemoveWithOptions(options *BackupOptions) error {
|
||||
backupLog.Info("Removing db backup")
|
||||
|
||||
options = store.setupOptions(options)
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
|
||||
err = os.Remove(options.BackupPath)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
116
api/bolt/backup_test.go
Normal file
116
api/bolt/backup_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// isFileExist is helper function to check for file existence
|
||||
func isFileExist(path string) bool {
|
||||
matches, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(matches) > 0
|
||||
}
|
||||
|
||||
func TestCreateBackupFolders(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
backupPath := path.Join(store.path, backupDefaults.backupDir)
|
||||
|
||||
if isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to not exist")
|
||||
}
|
||||
|
||||
store.createBackupFolders()
|
||||
if !isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
if store == nil {
|
||||
t.Error("Expect to create a store")
|
||||
}
|
||||
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
t.Error("Expect to get CE Edition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("Backup should create default db backup", func(t *testing.T) {
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
store.BackupWithOptions(nil)
|
||||
|
||||
backupFileName := path.Join(store.path, "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
|
||||
store.BackupWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
BackupDir: store.commonBackupDir(),
|
||||
})
|
||||
backupFileName := path.Join(store.path, "backups", "common", beforePortainerVersionUpgradeBackup)
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveWithOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("successfully removes file if existent", func(t *testing.T) {
|
||||
store.createBackupFolders()
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
filePath := path.Join(options.BackupDir, options.BackupFileName)
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("file should be created; err=%s", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
err = store.RemoveWithOptions(options)
|
||||
if err != nil {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
|
||||
}
|
||||
|
||||
if isFileExist(f.Name()) {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails to removes file if non-existent", func(t *testing.T) {
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
err := store.RemoveWithOptions(options)
|
||||
if err == nil {
|
||||
t.Error("RemoveWithOptions should fail for non-existent file")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,10 +2,10 @@ package bolt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/apikeyrepository"
|
||||
"github.com/portainer/portainer/api/bolt/helmuserrepository"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"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"
|
||||
@@ -36,7 +35,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
"github.com/portainer/portainer/api/bolt/webhook"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,6 +61,7 @@ type Store struct {
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
APIKeyRepositoryService *apikeyrepository.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
SSLSettingsService *ssl.Service
|
||||
@@ -76,6 +75,14 @@ type Store struct {
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) version() (int, error) {
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
@@ -85,25 +92,13 @@ func (store *Store) edition() portainer.SoftwareEdition {
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
|
||||
store := &Store{
|
||||
func NewStore(storePath string, fileService portainer.FileService) *Store {
|
||||
return &Store{
|
||||
path: storePath,
|
||||
fileService: fileService,
|
||||
isNew: true,
|
||||
connection: &internal.DbConnection{},
|
||||
}
|
||||
|
||||
databasePath := path.Join(storePath, databaseFileName)
|
||||
databaseFileExists, err := fileService.FileExists(databasePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if databaseFileExists {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
@@ -115,7 +110,17 @@ func (store *Store) Open() error {
|
||||
}
|
||||
store.connection.DB = db
|
||||
|
||||
return store.initServices()
|
||||
err = store.initServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we have DBVersion in the database then ensure we flag this as NOT a new store
|
||||
if _, err := store.VersionService.DBVersion(); err == nil {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
@@ -133,64 +138,6 @@ func (store *Store) IsNew() bool {
|
||||
return store.isNew
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
log.Printf("An error occurred during database migration: %s\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (store *Store) BackupTo(w io.Writer) error {
|
||||
@@ -199,3 +146,11 @@ func (store *Store) BackupTo(w io.Writer) error {
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an endpoint.
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an endpoint.
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const (
|
||||
BucketName = "endpoints"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Endpoint returns an endpoint by ID.
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint portainer.Endpoint
|
||||
identifier := internal.Itob(int(ID))
|
||||
@@ -41,19 +41,19 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an endpoint.
|
||||
// UpdateEndpoint updates an environment(endpoint).
|
||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, endpoint)
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an endpoint.
|
||||
// DeleteEndpoint deletes an environment(endpoint).
|
||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the endpoints.
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
@@ -76,12 +76,12 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
return endpoints, err
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new endpoint and saves it.
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
// We manually manage sequences for endpoints
|
||||
// We manually manage sequences for environments(endpoints)
|
||||
err := bucket.SetSequence(uint64(endpoint.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,12 +96,12 @@ func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
|
||||
})
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an endpoint.
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
||||
// Synchronize creates, updates and deletes endpoints inside a single transaction.
|
||||
// Synchronize creates, updates and deletes environments(endpoints) inside a single transaction.
|
||||
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "endpoint_groups"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EndpointGroup returns an endpoint group by ID.
|
||||
// EndpointGroup returns an environment(endpoint) group by ID.
|
||||
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
identifier := internal.Itob(int(ID))
|
||||
@@ -42,19 +42,19 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
|
||||
return &endpointGroup, nil
|
||||
}
|
||||
|
||||
// UpdateEndpointGroup updates an endpoint group.
|
||||
// UpdateEndpointGroup updates an environment(endpoint) group.
|
||||
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, endpointGroup)
|
||||
}
|
||||
|
||||
// DeleteEndpointGroup deletes an endpoint group.
|
||||
// DeleteEndpointGroup deletes an environment(endpoint) group.
|
||||
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
||||
// EndpointGroups return an array containing all the endpoint groups.
|
||||
// EndpointGroups return an array containing all the environment(endpoint) groups.
|
||||
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||
var endpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
@@ -77,7 +77,7 @@ func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||
return endpointGroups, err
|
||||
}
|
||||
|
||||
// CreateEndpointGroup assign an ID to a new endpoint group and saves it.
|
||||
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
@@ -11,7 +11,7 @@ const (
|
||||
BucketName = "endpoint_relations"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint relation data.
|
||||
// Service represents a service for managing environment(endpoint) relation data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EndpointRelation returns a Endpoint relation object by EndpointID
|
||||
// EndpointRelation returns a Environment(Endpoint) relation object by EndpointID
|
||||
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := internal.Itob(int(endpointID))
|
||||
@@ -55,13 +55,13 @@ func (service *Service) CreateEndpointRelation(endpointRelation *portainer.Endpo
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Endpoint relation object
|
||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
identifier := internal.Itob(int(EndpointID))
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, endpointRelation)
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Endpoint relation object
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
||||
identifier := internal.Itob(int(EndpointID))
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "extension"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "helm_user_repository"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func (store *Store) Init() error {
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
KubectlShellImage: portainer.DefaultKubectlShellImage,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
@@ -17,7 +17,7 @@ func UnmarshalObject(data []byte, object interface{}) error {
|
||||
}
|
||||
|
||||
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||
// using the jsoniter library. It is mainly used to accelerate endpoint
|
||||
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
|
||||
// decoding at the moment.
|
||||
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
151
api/bolt/migrate_data.go
Normal file
151
api/bolt/migrate_data.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
store.Rollback(true)
|
||||
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
|
||||
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
// !Important: we must use a named return value in the function definition and not a local
|
||||
// !variable referenced from the closure or else the return value will be incorrectly set
|
||||
return migrator.Migrate()
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
migrator, err := store.newMigrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// backup db file before upgrading DB to support rollback
|
||||
isUpdating, err := store.VersionService.IsUpdating()
|
||||
if err != nil && err != errors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isUpdating && migrator.Version() != portainer.DBVersion {
|
||||
err = store.backupVersion(migrator)
|
||||
if err != nil {
|
||||
return werrors.Wrapf(err, "failed to backup database")
|
||||
}
|
||||
}
|
||||
|
||||
if migrator.Version() < portainer.DBVersion {
|
||||
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
|
||||
err = store.FailSafeMigrate(migrator)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database migration", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) newMigrator() (*migrator.Migrator, error) {
|
||||
version, err := store.version()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
return migrator.NewMigrator(migratorParams), nil
|
||||
}
|
||||
|
||||
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
|
||||
// - db backup prior to version upgrade
|
||||
// - db rollback
|
||||
func getBackupRestoreOptions(store *Store) *BackupOptions {
|
||||
return &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
}
|
||||
}
|
||||
|
||||
// backupVersion will backup the database or panic if any errors occur
|
||||
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
|
||||
migrateLog.Info("Backing up database prior to version upgrade...")
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
_, err := store.BackupWithOptions(options)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database backup", err)
|
||||
removalErr := store.RemoveWithOptions(options)
|
||||
if removalErr != nil {
|
||||
migrateLog.Error("An error occurred during store removal prior to backup", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
|
||||
if !force {
|
||||
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
|
||||
if err != nil || !confirmed {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
err := store.RestoreWithOptions(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Close()
|
||||
}
|
||||
172
api/bolt/migrate_data_test.go
Normal file
172
api/bolt/migrate_data_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
if v, _ := store.version(); v != versionWant {
|
||||
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
if !store.IsNew() {
|
||||
t.Error("Expect a new DB")
|
||||
}
|
||||
|
||||
store.MigrateData(false)
|
||||
|
||||
testVersion(store, portainer.DBVersion, t)
|
||||
store.Close()
|
||||
|
||||
store.Open()
|
||||
if store.IsNew() {
|
||||
t.Error("Expect store to NOT be new DB")
|
||||
}
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
version int
|
||||
expectedVersion int
|
||||
}{
|
||||
{version: 2, expectedVersion: portainer.DBVersion},
|
||||
{version: 21, expectedVersion: portainer.DBVersion},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
// Setup data
|
||||
store.VersionService.StoreDBVersion(tc.version)
|
||||
|
||||
// Required roles by migrations 22.2
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 1})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 2})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 3})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 4})
|
||||
|
||||
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.MigrateData(true)
|
||||
testVersion(store, tc.expectedVersion, t)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.Rollback(true)
|
||||
store.Open()
|
||||
testVersion(store, tc.version, t)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
version := 2
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(0)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if !isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.VersionService.StoreIsUpdating(true)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
wantDir := store.commonBackupDir()
|
||||
if !strings.HasSuffix(options.BackupDir, wantDir) {
|
||||
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
|
||||
}
|
||||
|
||||
wantFilename := "portainer.db.bak"
|
||||
if options.BackupFileName != wantFilename {
|
||||
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := 21
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
_, err := store.BackupWithOptions(getBackupRestoreOptions(store))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Change the current edition
|
||||
err = store.VersionService.StoreDBVersion(version + 10)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Rollback(true)
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
store.Open()
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
}
|
||||
343
api/bolt/migrator/migrate_ce.go
Normal file
343
api/bolt/migrator/migrate_ce.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return werrors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// set DB to updating status
|
||||
err := m.versionService.StoreIsUpdating(true)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreIsUpdating")
|
||||
}
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateAdminUserToDBVersion1")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion2")
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion2")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion3")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion4")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion5")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion6")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion7")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion8")
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion9")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion10")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion11")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToVersion12")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion13")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion14")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion15")
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTemplatesToVersion15")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion16")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateExtensionsToDBVersion17")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateRegistriesToDBVersion18")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion19")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSchedulesToDBVersion20")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion22")
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersAndRolesToDBVersion22")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
migrateLog.Info("Migrating to DB 23")
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTagsToDBVersion23")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
migrateLog.Info("Migrating to DB 24")
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
migrateLog.Info("Migrating to DB 25")
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB25")
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
migrateLog.Info("Migrating to DB 26")
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointSettingsToDB25")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
migrateLog.Info("Migrating to DB 27")
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStackResourceControlToDB27")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
migrateLog.Info("Migrating to DB 30")
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB30")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB32")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.1, 2.9.2
|
||||
if m.currentDBVersion < 33 {
|
||||
migrateLog.Info("Migrating to DB 33")
|
||||
err := m.migrateDBVersionToDB33()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB33")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.10
|
||||
if m.currentDBVersion < 34 {
|
||||
migrateLog.Info("Migrating to DB 34")
|
||||
if err := m.migrateDBVersionToDB34(); err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB34")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
|
||||
if m.currentDBVersion < 35 {
|
||||
migrateLog.Info("Migrating to DB 35")
|
||||
if err := m.migrateDBVersionToDB35(); err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB35")
|
||||
}
|
||||
}
|
||||
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
|
||||
|
||||
// reset DB updating status
|
||||
return m.versionService.StoreIsUpdating(false)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/portainer/portainer/api"
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
migrateLog.Info("Updating tags")
|
||||
tags, err := m.tagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -20,6 +21,7 @@ func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
|
||||
migrateLog.Info("Updating endpoints and endpoint groups")
|
||||
tags, err := m.tagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,8 @@ package migrator
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
migrateLog.Info("Updating Settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -16,6 +18,7 @@ func (m *Migrator) updateSettingsToDB24() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateStacksToDB24() error {
|
||||
migrateLog.Info("Updating stacks")
|
||||
stacks, err := m.stackService.Stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB25() error {
|
||||
migrateLog.Info("Updating settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
migrateLog.Info("Updating endpoint settings")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
migrateLog.Info("Updating stack resource controls")
|
||||
resourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB30() error {
|
||||
migrateLog.Info("Updating legacy settings")
|
||||
if err := m.migrateSettingsToDB30(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -13,6 +14,7 @@ func (m *Migrator) migrateSettingsToDB30() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.OAuthSettings.SSO = false
|
||||
legacySettings.OAuthSettings.LogoutURI = ""
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
|
||||
@@ -2,6 +2,7 @@ package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
@@ -10,24 +11,29 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB32() error {
|
||||
migrateLog.Info("Updating registries")
|
||||
err := m.updateRegistriesToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Updating dockerhub")
|
||||
err = m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Updating resource controls")
|
||||
if err := m.updateVolumeResourceControlToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Updating kubeconfig expiry")
|
||||
if err := m.kubeconfigExpiryToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Setting default helm repository url")
|
||||
if err := m.helmRepositoryURLToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,6 +105,32 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
// The following code will make this function idempotent.
|
||||
// i.e. if run again, it will not change the data. It will ensure that
|
||||
// we only have one migrated registry entry. Duplicates will be removed
|
||||
// if they exist and which has been happening due to earlier migration bugs
|
||||
migrated := false
|
||||
registries, _ := m.registryService.Registries()
|
||||
for _, r := range registries {
|
||||
if r.Type == registry.Type &&
|
||||
r.Name == registry.Name &&
|
||||
r.URL == registry.URL &&
|
||||
r.Authentication == registry.Authentication {
|
||||
|
||||
if !migrated {
|
||||
// keep this one entry
|
||||
migrated = true
|
||||
} else {
|
||||
// delete subsequent duplicates
|
||||
m.registryService.DeleteRegistry(portainer.RegistryID(r.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if migrated {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -167,6 +199,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
|
||||
totalSnapshots := len(endpoint.Snapshots)
|
||||
if totalSnapshots == 0 {
|
||||
log.Println("[DEBUG] [volume migration] [message: no snapshot found]")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -174,11 +207,13 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environment docker id: %w", err)
|
||||
log.Printf("[WARN] [bolt,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
|
||||
if volumesData["Volumes"] == nil {
|
||||
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -199,7 +234,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] [volume migration] [message: legacy resource control(%s) has been deleted]", resourceControl.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +245,16 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
volumes := volumesData["Volumes"].([]interface{})
|
||||
for _, volumeMeta := range volumes {
|
||||
volume := volumeMeta.(map[string]interface{})
|
||||
volumeName := volume["Name"].(string)
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
|
||||
volumeName, nameExist := volume["Name"].(string)
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
createTime, createTimeExist := volume["CreatedAt"].(string)
|
||||
if !createTimeExist {
|
||||
continue
|
||||
}
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
||||
if ok {
|
||||
|
||||
22
api/bolt/migrator/migrate_dbversion32.go
Normal file
22
api/bolt/migrator/migrate_dbversion32.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB33() error {
|
||||
if err := m.migrateSettingsToDB33(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateSettingsToDB33() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Setting default kubectl shell image")
|
||||
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionTo33() error {
|
||||
func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
migrateLog.Info("Migrating stacks")
|
||||
err := migrateStackEntryPoint(m.stackService)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-34.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
assert.NoError(t, err, "failed to init testing DB connection")
|
||||
defer dbConn.Close()
|
||||
|
||||
|
||||
12
api/bolt/migrator/migrate_dbversion34.go
Normal file
12
api/bolt/migrator/migrate_dbversion34.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB35() error {
|
||||
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
|
||||
// calling it again will now fix the issue as the function has been repaired.
|
||||
migrateLog.Info("Updating dockerhub registries")
|
||||
err := m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
108
api/bolt/migrator/migrate_dbversion34_test.go
Normal file
108
api/bolt/migrator/migrate_dbversion34_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "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/internal"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
db35TestFile = "portainer-mig-35.db"
|
||||
username = "portainer"
|
||||
password = "password"
|
||||
)
|
||||
|
||||
func setupDB35Test(t *testing.T) *Migrator {
|
||||
is := assert.New(t)
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
is.NoError(err, "failed to init testing DB connection")
|
||||
|
||||
// Create an old style dockerhub authenticated account
|
||||
dockerhubService, err := dockerhub.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init testing registry service")
|
||||
err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
|
||||
is.NoError(err, "failed to create dockerhub account")
|
||||
|
||||
registryService, err := registry.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init testing registry service")
|
||||
|
||||
endpointService, err := endpoint.NewService(&internal.DbConnection{DB: dbConn})
|
||||
is.NoError(err, "failed to init endpoint service")
|
||||
|
||||
m := &Migrator{
|
||||
db: dbConn,
|
||||
dockerhubService: dockerhubService,
|
||||
registryService: registryService,
|
||||
endpointService: endpointService,
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// TestUpdateDockerhubToDB32 tests a normal upgrade
|
||||
func TestUpdateDockerhubToDB32(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
m := setupDB35Test(t)
|
||||
defer m.db.Close()
|
||||
defer os.Remove(db35TestFile)
|
||||
|
||||
if err := m.updateDockerhubToDB32(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
|
||||
// Verify we have a single registry were created
|
||||
registries, err := m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
}
|
||||
|
||||
// TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
|
||||
// created a large number of duplicate "dockerhub migrated" registry entries.
|
||||
func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
m := setupDB35Test(t)
|
||||
defer m.db.Close()
|
||||
defer os.Remove(db35TestFile)
|
||||
|
||||
// Create lots of duplicate entries...
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
Name: "Dockerhub (authenticated - migrated)",
|
||||
URL: "docker.io",
|
||||
Authentication: true,
|
||||
Username: "portainer",
|
||||
Password: "password",
|
||||
RegistryAccesses: portainer.RegistryAccesses{},
|
||||
}
|
||||
|
||||
for i := 1; i < 150; i++ {
|
||||
err := m.registryService.CreateRegistry(registry)
|
||||
assert.NoError(t, err, "create registry failed")
|
||||
}
|
||||
|
||||
// Verify they were created
|
||||
registries, err := m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Condition(func() bool {
|
||||
return len(registries) > 1
|
||||
}, "expected multiple duplicate registry entries")
|
||||
|
||||
// Now run the migrator
|
||||
if err := m.updateDockerhubToDB32(); err != nil {
|
||||
t.Errorf("failed to update settings: %v", err)
|
||||
}
|
||||
|
||||
// Verify we have a single registry were created
|
||||
registries, err = m.registryService.Registries()
|
||||
is.NoError(err, "failed to read registries from the RegistryService")
|
||||
is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
}
|
||||
@@ -27,8 +27,9 @@ var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
db *bolt.DB
|
||||
currentDBVersion int
|
||||
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
@@ -97,295 +98,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 33 {
|
||||
if err := m.migrateDBVersionTo33(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
// Version exposes version of database
|
||||
func (migrator *Migrator) Version() int {
|
||||
return migrator.currentDBVersion
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "registries"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "resource_control"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "roles"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package bolt
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/apikeyrepository"
|
||||
"github.com/portainer/portainer/api/bolt/customtemplate"
|
||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/api/bolt/edgegroup"
|
||||
@@ -155,6 +156,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.UserService = userService
|
||||
|
||||
apiKeyService, err := apikeyrepository.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.APIKeyRepositoryService = apiKeyService
|
||||
|
||||
versionService, err := version.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -196,7 +203,7 @@ func (store *Store) EdgeStack() portainer.EdgeStackService {
|
||||
return store.EdgeStackService
|
||||
}
|
||||
|
||||
// Endpoint gives access to the Endpoint data management layer
|
||||
// Environment(Endpoint) gives access to the Environment(Endpoint) data management layer
|
||||
func (store *Store) Endpoint() portainer.EndpointService {
|
||||
return store.EndpointService
|
||||
}
|
||||
@@ -231,6 +238,11 @@ func (store *Store) Role() portainer.RoleService {
|
||||
return store.RoleService
|
||||
}
|
||||
|
||||
// APIKeyRepository gives access to the api-key data management layer
|
||||
func (store *Store) APIKeyRepository() portainer.APIKeyRepository {
|
||||
return store.APIKeyRepositoryService
|
||||
}
|
||||
|
||||
// Settings gives access to the Settings data management layer
|
||||
func (store *Store) Settings() portainer.SettingsService {
|
||||
return store.SettingsService
|
||||
|
||||
@@ -11,7 +11,7 @@ const (
|
||||
settingsKey = "SETTINGS"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
@@ -44,3 +44,17 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||
return internal.UpdateObject(service.connection, BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
|
||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
settings, err := service.Settings()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
featureFlagSetting, ok := settings.FeatureFlagSettings[feature]
|
||||
if ok {
|
||||
return featureFlagSetting
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
BucketName = "stacks"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
@@ -77,6 +77,31 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
||||
return stack, err
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks with same name
|
||||
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
|
||||
err := service.connection.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 {
|
||||
stacks = append(stacks, t)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks.
|
||||
func (service *Service) Stacks() ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
@@ -192,8 +217,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
var stack portainer.Stack
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
stack := portainer.Stack{}
|
||||
err := internal.UnmarshalObject(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,18 +4,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/bolttest"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newGuidString(t *testing.T) string {
|
||||
@@ -35,7 +29,7 @@ func TestService_StackByWebhookID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
b := stackBuilder{t: t, store: store}
|
||||
@@ -93,7 +87,7 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "tags"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
BucketName = "teams"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
BucketName = "team_membership"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package bolttest
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -6,13 +6,12 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
var errTempDir = errors.New("can't create a temp dir")
|
||||
|
||||
func MustNewTestStore(init bool) (*bolt.Store, func()) {
|
||||
func MustNewTestStore(init bool) (*Store, func()) {
|
||||
store, teardown, err := NewTestStore(init)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errTempDir) {
|
||||
@@ -24,7 +23,7 @@ func MustNewTestStore(init bool) (*bolt.Store, func()) {
|
||||
return store, teardown
|
||||
}
|
||||
|
||||
func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
func NewTestStore(init bool) (*Store, func(), error) {
|
||||
// Creates unique temp directory in a concurrency friendly manner.
|
||||
dataStorePath, err := ioutil.TempDir("", "boltdb")
|
||||
if err != nil {
|
||||
@@ -36,11 +35,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store := NewStore(dataStorePath, fileService)
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -60,7 +55,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
return store, teardown, nil
|
||||
}
|
||||
|
||||
func teardown(store *bolt.Store, dataStorePath string) {
|
||||
func teardown(store *Store, dataStorePath string) {
|
||||
err := store.Close()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
@@ -11,7 +11,7 @@ const (
|
||||
infoKey = "INFO"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
BucketName = "users"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
updatingKey = "DB_UPDATING"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -83,6 +84,21 @@ func (service *Service) StoreDBVersion(version int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// IsUpdating retrieves the database updating status.
|
||||
func (service *Service) IsUpdating() (bool, error) {
|
||||
isUpdating, err := service.getKey(updatingKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return strconv.ParseBool(string(isUpdating))
|
||||
}
|
||||
|
||||
// StoreIsUpdating store the database updating status.
|
||||
func (service *Service) StoreIsUpdating(isUpdating bool) error {
|
||||
return service.setKey(updatingKey, strconv.FormatBool(isUpdating))
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
var data []byte
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an endpoint.
|
||||
// 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)
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +33,7 @@ type Service struct {
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
@@ -42,6 +45,55 @@ func NewService(dataStore portainer.DataStore, shutdownCtx context.Context) *Ser
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -165,18 +217,13 @@ func (service *Service) checkTunnels() {
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnel.Jobs) > 0 {
|
||||
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))
|
||||
} else {
|
||||
service.tunnelDetailsMap.Remove(item.Key)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func randomInt(min, max int) int {
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
||||
|
||||
// GetTunnelDetails returns information about the tunnel associated to an endpoint.
|
||||
// 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))
|
||||
|
||||
@@ -56,7 +56,39 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
}
|
||||
}
|
||||
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||
// 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)
|
||||
@@ -68,7 +100,7 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified endpoint.
|
||||
// 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) {
|
||||
@@ -86,13 +118,15 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
|
||||
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 endpoint.
|
||||
// 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 endpoint.
|
||||
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
|
||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
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(),
|
||||
@@ -47,6 +48,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
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").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(),
|
||||
|
||||
24
api/cli/confirm.go
Normal file
24
api/cli/confirm.go
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"fmt"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type pairList []portainer.Pair
|
||||
|
||||
45
api/cli/pairlistbool.go
Normal file
45
api/cli/pairlistbool.go
Normal file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
||||
@@ -2,21 +2,24 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
@@ -56,17 +59,24 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||
return fileService
|
||||
}
|
||||
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store := bolt.NewStore(dataStorePath, fileService)
|
||||
err := store.Open()
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
}
|
||||
|
||||
if rollback {
|
||||
err := store.Rollback(false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed rolling back: %s", err)
|
||||
}
|
||||
|
||||
log.Println("Exiting rollback")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing data store: %v", err)
|
||||
@@ -99,14 +109,18 @@ func initSwarmStackManager(assetsPath string, configPath string, signatureServic
|
||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||
}
|
||||
|
||||
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
|
||||
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
|
||||
}
|
||||
|
||||
func initAPIKeyService(datastore portainer.DataStore) apikey.APIKeyService {
|
||||
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
|
||||
}
|
||||
|
||||
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
@@ -230,6 +244,49 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableFeaturesFromFlags turns on or off feature flags
|
||||
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
|
||||
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
|
||||
func enableFeaturesFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings == nil {
|
||||
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
}
|
||||
|
||||
// loop through feature flags to check if they are supported
|
||||
for _, feat := range *flags.FeatureFlags {
|
||||
var correspondingFeature *portainer.Feature
|
||||
for i, supportedFeat := range portainer.SupportedFeatureFlags {
|
||||
if strings.EqualFold(feat.Name, string(supportedFeat)) {
|
||||
correspondingFeature = &portainer.SupportedFeatureFlags[i]
|
||||
}
|
||||
}
|
||||
|
||||
if correspondingFeature == nil {
|
||||
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
|
||||
}
|
||||
|
||||
featureState, err := strconv.ParseBool(feat.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
|
||||
}
|
||||
|
||||
if featureState {
|
||||
log.Printf("Feature %v : on", *correspondingFeature)
|
||||
} else {
|
||||
log.Printf("Feature %v : off", *correspondingFeature)
|
||||
}
|
||||
|
||||
settings.FeatureFlagSettings[*correspondingFeature] = featureState
|
||||
}
|
||||
|
||||
return dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
private, public, err := fileService.LoadKeyPair()
|
||||
if err != nil {
|
||||
@@ -399,23 +456,32 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
|
||||
dataStore := initDataStore(*flags.Data, *flags.Rollback, fileService, shutdownCtx)
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
apiKeyService := initAPIKeyService(dataStore)
|
||||
|
||||
jwtService, err := initJWTService(dataStore)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing JWT service: %v", err)
|
||||
}
|
||||
|
||||
err = enableFeaturesFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatalf("failed enabling feature flag: %v", err)
|
||||
}
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
oauthService := initOAuthService()
|
||||
|
||||
gitService := initGitService()
|
||||
|
||||
openAMTService := openamt.NewService(dataStore)
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
@@ -460,6 +526,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
@@ -469,7 +537,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatalf("failed initializing swarm stack manager: %s", err)
|
||||
}
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
|
||||
|
||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||
if err != nil {
|
||||
@@ -497,7 +565,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
|
||||
if err != nil {
|
||||
log.Fatalf("failed getting admin password file: %v", err)
|
||||
}
|
||||
@@ -542,7 +610,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
|
||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager)
|
||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
||||
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||
|
||||
return &http.Server{
|
||||
@@ -559,11 +627,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
HelmPackageManager: helmPackageManager,
|
||||
CryptoService: cryptoService,
|
||||
APIKeyService: apiKeyService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
OAuthService: oauthService,
|
||||
GitService: gitService,
|
||||
OpenAMTService: openAMTService,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
KubeConfigService: kubeConfigService,
|
||||
|
||||
114
api/cmd/portainer/main_test.go
Normal file
114
api/cmd/portainer/main_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type mockKingpinSetting string
|
||||
|
||||
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
||||
value.Set(string(m))
|
||||
}
|
||||
|
||||
func Test_enableFeaturesFromFlags(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
tests := []struct {
|
||||
featureFlag string
|
||||
isSupported bool
|
||||
}{
|
||||
{"test", false},
|
||||
{"openamt", false},
|
||||
{"open-amt", true},
|
||||
{"oPeN-amT", true},
|
||||
{"fdo", true},
|
||||
{"FDO", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
if test.isSupported {
|
||||
is.NoError(err)
|
||||
} else {
|
||||
is.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("passes for all supported feature flags", func(t *testing.T) {
|
||||
for _, flag := range portainer.SupportedFeatureFlags {
|
||||
mockKingpinSetting := mockKingpinSetting(flag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const FeatTest portainer.Feature = "optional-test"
|
||||
|
||||
func optionalFunc(dataStore portainer.DataStore) string {
|
||||
|
||||
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
|
||||
// ideally, the `if` should look more like:
|
||||
// if featureflags.FlagEnabled(FeatTest) {}
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings[FeatTest] {
|
||||
return "enabled"
|
||||
}
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func Test_optionalFeature(t *testing.T) {
|
||||
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
|
||||
|
||||
is := assert.New(t)
|
||||
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
// Enable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so the feature flag should still be enabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// disable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so feature flag should still be disabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
// ECDSAService is a service used to create digital signatures when communicating with
|
||||
// an agent based environment. It will automatically generates a key pair using ECDSA or
|
||||
// an agent based environment(endpoint). It will automatically generates a key pair using ECDSA or
|
||||
// can also reuse an existing ECDSA key pair.
|
||||
type ECDSAService struct {
|
||||
privateKey *ecdsa.PrivateKey
|
||||
|
||||
@@ -34,8 +34,8 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
}
|
||||
|
||||
// createClient is a generic function to create a Docker client based on
|
||||
// a specific endpoint configuration. The nodeName parameter can be used
|
||||
// with an agent enabled endpoint to target a specific node in an agent cluster.
|
||||
// a specific environment(endpoint) configuration. The nodeName parameter can be used
|
||||
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, errUnsupportedEnvironmentType
|
||||
@@ -91,7 +91,11 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create endpoint snapshots
|
||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||
type Snapshotter struct {
|
||||
clientFactory *ClientFactory
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a snapshot of a specific Docker endpoint
|
||||
// CreateSnapshot creates a snapshot of a specific Docker environment(endpoint)
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
|
||||
5
api/exec/common.go
Normal file
5
api/exec/common.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package exec
|
||||
|
||||
import "regexp"
|
||||
|
||||
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -17,6 +15,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// ComposeStackManager is a wrapper for docker-compose binary
|
||||
@@ -47,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to featch environment proxy")
|
||||
return errors.Wrap(err, "failed to fetch environment proxy")
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
@@ -59,7 +58,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
@@ -74,15 +73,14 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||
func (w *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
@@ -90,7 +88,7 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
proxy, err := manager.proxyManager.CreateComposeProxyServer(endpoint)
|
||||
proxy, err := manager.proxyManager.CreateAgentProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -117,27 +115,3 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
// getStackFiles returns list of stack's confile file paths.
|
||||
// items in the list would be sanitized according to following criterias:
|
||||
// 1. no empty paths
|
||||
// 2. no "../xxx" paths that are trying to escape stack folder
|
||||
// 3. no dir paths
|
||||
// 4. root paths would be made relative
|
||||
func getStackFiles(stack *portainer.Stack) []string {
|
||||
paths := make([]string, 0, len(stack.AdditionalFiles)+1)
|
||||
|
||||
for _, p := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
p = `.` + p
|
||||
}
|
||||
|
||||
if p == `` || p == `.` || strings.HasPrefix(p, `..`) || strings.HasSuffix(p, string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -64,21 +64,3 @@ func Test_createEnvFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getStackFiles(t *testing.T) {
|
||||
stack := &portainer.Stack{
|
||||
EntryPoint: "./file", // picks entry point
|
||||
AdditionalFiles: []string{
|
||||
``, // ignores empty string
|
||||
`.`, // ignores .
|
||||
`..`, // ignores ..
|
||||
`./dir/`, // ignrores paths that end with trailing /
|
||||
`/with-root-prefix`, // replaces "root" based paths with relative
|
||||
`./relative`, // keeps relative paths
|
||||
`../escape`, // prevents dir escape
|
||||
},
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
assert.ElementsMatch(t, filePaths, []string{`./file`, `./with-root-prefix`, `./relative`})
|
||||
}
|
||||
|
||||
23
api/exec/exectest/kubernetes_mocks.go
Normal file
23
api/exec/exectest/kubernetes_mocks.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package exectest
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type kubernetesMockDeployer struct{}
|
||||
|
||||
func NewKubernetesDeployer() portainer.KubernetesDeployer {
|
||||
return &kubernetesMockDeployer{}
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2,27 +2,22 @@ package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
)
|
||||
|
||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
|
||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
|
||||
type KubernetesDeployer struct {
|
||||
binaryPath string
|
||||
dataStore portainer.DataStore
|
||||
@@ -30,10 +25,11 @@ type KubernetesDeployer struct {
|
||||
signatureService portainer.DigitalSignatureService
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
|
||||
return &KubernetesDeployer{
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
@@ -41,32 +37,33 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
|
||||
signatureService: signatureService,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
proxyManager: proxyManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
|
||||
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
|
||||
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
user, err := deployer.dataStore.User().User(userID)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to fetch the user")
|
||||
}
|
||||
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return tokenManager.GetAdminServiceAccountToken(), nil
|
||||
}
|
||||
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
|
||||
token, err := tokenManager.GetUserServiceAccountToken(int(user.ID), endpoint.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -77,156 +74,62 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
|
||||
// Otherwise it will use kubectl to deploy the manifest.
|
||||
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment {
|
||||
token, err := deployer.getToken(request, endpoint, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Deploy upserts Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
command := path.Join(deployer.binaryPath, "kubectl")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
// Remove deletes Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "--server", endpoint.URL)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
args = append(args, "--token", token)
|
||||
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed generating a user token")
|
||||
}
|
||||
|
||||
command := path.Join(deployer.binaryPath, "kubectl")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := []string{"--token", token}
|
||||
if namespace != "" {
|
||||
args = append(args, "--namespace", namespace)
|
||||
args = append(args, "apply", "-f", "-")
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdin = strings.NewReader(stackConfig)
|
||||
|
||||
output, err := cmd.Output()
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
url, proxy, err := deployer.getAgentURL(endpoint)
|
||||
if err != nil {
|
||||
return "", errors.New(stderr.String())
|
||||
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
defer proxy.Close()
|
||||
args = append(args, "--server", url)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
|
||||
// agent
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
|
||||
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
settings, err := deployer.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
|
||||
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
if operation == "delete" {
|
||||
args = append(args, "--ignore-not-found=true")
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
args = append(args, operation)
|
||||
for _, path := range manifestFiles {
|
||||
args = append(args, "-f", strings.TrimSpace(path))
|
||||
}
|
||||
|
||||
httpCli := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if !strings.HasPrefix(endpointURL, "http") {
|
||||
endpointURL = fmt.Sprintf("https://%s", endpointURL)
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
|
||||
}
|
||||
|
||||
reqPayload, err := json.Marshal(
|
||||
struct {
|
||||
StackConfig string
|
||||
Namespace string
|
||||
}{
|
||||
StackConfig: stackConfig,
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := deployer.getToken(request, endpoint, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
|
||||
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errorResponseData struct {
|
||||
Message string
|
||||
Details string
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
|
||||
if err != nil {
|
||||
output, parseStringErr := ioutil.ReadAll(resp.Body)
|
||||
if parseStringErr != nil {
|
||||
return "", parseStringErr
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
|
||||
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
|
||||
}
|
||||
|
||||
var responseData struct{ Output string }
|
||||
err = json.NewDecoder(resp.Body).Decode(&responseData)
|
||||
if err != nil {
|
||||
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
|
||||
if parseStringErr != nil {
|
||||
return "", parseStringErr
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
|
||||
}
|
||||
|
||||
return responseData.Output, nil
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
|
||||
@@ -251,3 +154,12 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -45,19 +44,26 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -65,7 +71,10 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
|
||||
@@ -85,7 +94,10 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -109,7 +121,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
@@ -122,7 +134,10 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
@@ -142,7 +157,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
}
|
||||
}
|
||||
|
||||
return command, args
|
||||
return command, args, nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
@@ -176,7 +191,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
||||
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
|
||||
var config map[string]interface{}
|
||||
|
||||
raw, err := manager.fileService.GetFileContent(path)
|
||||
raw, err := manager.fileService.GetFileContent(path, "")
|
||||
if err != nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
@@ -190,8 +205,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func configureFilePaths(args []string, filePaths []string) []string {
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -69,12 +69,31 @@ type Service struct {
|
||||
fileStorePath string
|
||||
}
|
||||
|
||||
// JoinPaths takes a trusted root path and a list of untrusted paths and joins
|
||||
// them together using directory separators while enforcing that the untrusted
|
||||
// paths cannot go higher up than the trusted root
|
||||
func JoinPaths(trustedRoot string, untrustedPaths ...string) string {
|
||||
if trustedRoot == "" {
|
||||
trustedRoot = "."
|
||||
}
|
||||
|
||||
p := filepath.Join(trustedRoot, filepath.Join(append([]string{"/"}, untrustedPaths...)...))
|
||||
|
||||
// avoid setting a volume name from the untrusted paths
|
||||
vnp := filepath.VolumeName(p)
|
||||
if filepath.VolumeName(trustedRoot) == "" && vnp != "" {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(p, vnp), `\`)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// NewService initializes a new service. It creates a data directory and a directory to store files
|
||||
// inside this directory if they don't exist.
|
||||
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
service := &Service{
|
||||
dataStorePath: dataStorePath,
|
||||
fileStorePath: path.Join(dataStorePath, fileStorePath),
|
||||
fileStorePath: JoinPaths(dataStorePath, fileStorePath),
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dataStorePath, 0755)
|
||||
@@ -112,12 +131,12 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
|
||||
// GetBinaryFolder returns the full path to the binary store on the filesystem
|
||||
func (service *Service) GetBinaryFolder() string {
|
||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||
return JoinPaths(service.fileStorePath, BinaryStorePath)
|
||||
}
|
||||
|
||||
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
|
||||
func (service *Service) GetDockerConfigPath() string {
|
||||
return path.Join(service.fileStorePath, DockerConfigPath)
|
||||
return JoinPaths(service.fileStorePath, DockerConfigPath)
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
@@ -128,7 +147,7 @@ func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
// GetStackProjectPath returns the absolute path on the FS for a stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
return JoinPaths(service.wrapFileStore(ComposeStorePath), stackIdentifier)
|
||||
}
|
||||
|
||||
// Copy copies the file on fromFilePath to toFilePath
|
||||
@@ -194,13 +213,13 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -208,25 +227,25 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
|
||||
return JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier)
|
||||
}
|
||||
|
||||
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
|
||||
stackStorePath := JoinPaths(EdgeStackStorePath, edgeStackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -234,20 +253,20 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
}
|
||||
|
||||
// StoreRegistryManagementFileFromBytes creates a subfolder in the
|
||||
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
|
||||
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
|
||||
extensionStorePath := JoinPaths(ExtensionRegistryManagementStorePath, folder)
|
||||
err := service.createDirectoryInStore(extensionStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
file := path.Join(extensionStorePath, fileName)
|
||||
file := JoinPaths(extensionStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(file, r)
|
||||
@@ -255,13 +274,13 @@ func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, file), nil
|
||||
return service.wrapFileStore(file), nil
|
||||
}
|
||||
|
||||
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||
// It returns the path to the newly created file.
|
||||
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
|
||||
storePath := path.Join(TLSStorePath, folder)
|
||||
storePath := JoinPaths(TLSStorePath, folder)
|
||||
err := service.createDirectoryInStore(storePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -279,16 +298,16 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
tlsFilePath := JoinPaths(storePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path.Join(service.fileStorePath, tlsFilePath), nil
|
||||
return service.wrapFileStore(tlsFilePath), nil
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an environment(endpoint).
|
||||
func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSFileType) (string, error) {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
@@ -301,17 +320,13 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
|
||||
default:
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||
return JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
storePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder)
|
||||
return os.RemoveAll(storePath)
|
||||
}
|
||||
|
||||
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||
@@ -328,20 +343,19 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
|
||||
return ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||
filePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName)
|
||||
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// GetFileContent returns the content of a file as bytes.
|
||||
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
|
||||
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if filePath == "" {
|
||||
filePath = trustedRoot
|
||||
}
|
||||
return nil, fmt.Errorf("could not get the contents of the file '%s'", filePath)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
@@ -359,7 +373,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, jsonContent, 0644)
|
||||
return os.WriteFile(path, jsonContent, 0644)
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
@@ -369,23 +383,17 @@ func (service *Service) FileExists(filePath string) (bool, error) {
|
||||
|
||||
// KeyPairFilesExist checks for the existence of the key files.
|
||||
func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
||||
privateKeyPath := JoinPaths(service.dataStorePath, PrivateKeyFile)
|
||||
exists, err := service.FileExists(privateKeyPath)
|
||||
if err != nil {
|
||||
if err != nil || !exists {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
||||
publicKeyPath := JoinPaths(service.dataStorePath, PublicKeyFile)
|
||||
exists, err = service.FileExists(publicKeyPath)
|
||||
if err != nil {
|
||||
if err != nil || !exists {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -397,12 +405,7 @@ func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, p
|
||||
return err
|
||||
}
|
||||
|
||||
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
}
|
||||
|
||||
// LoadKeyPair retrieve the content of both key files on disk.
|
||||
@@ -422,13 +425,13 @@ func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
|
||||
|
||||
// createDirectoryInStore creates a new directory in the file store
|
||||
func (service *Service) createDirectoryInStore(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
path := service.wrapFileStore(name)
|
||||
return os.MkdirAll(path, 0700)
|
||||
}
|
||||
|
||||
// createFile creates a new file in the file store with the content from r.
|
||||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
@@ -437,15 +440,11 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
block := &pem.Block{Type: fileType, Bytes: content}
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
@@ -454,18 +453,13 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
err = pem.Encode(out, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return pem.Encode(out, block)
|
||||
}
|
||||
|
||||
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
path := service.wrapFileStore(filePath)
|
||||
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -477,19 +471,19 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based
|
||||
// on its identifier.
|
||||
func (service *Service) GetCustomTemplateProjectPath(identifier string) string {
|
||||
return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier)
|
||||
return JoinPaths(service.wrapFileStore(CustomTemplateStorePath), identifier)
|
||||
}
|
||||
|
||||
// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) {
|
||||
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
|
||||
customTemplateStorePath := JoinPaths(CustomTemplateStorePath, identifier)
|
||||
err := service.createDirectoryInStore(customTemplateStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templateFilePath := path.Join(customTemplateStorePath, fileName)
|
||||
templateFilePath := JoinPaths(customTemplateStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(templateFilePath, r)
|
||||
@@ -497,32 +491,32 @@ func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, customTemplateStorePath), nil
|
||||
return service.wrapFileStore(customTemplateStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeJobFolder(identifier string) string {
|
||||
return path.Join(service.fileStorePath, EdgeJobStorePath, identifier)
|
||||
return JoinPaths(service.wrapFileStore(EdgeJobStorePath), identifier)
|
||||
}
|
||||
|
||||
// StoreEdgeJobFileFromBytes creates a subfolder in the EdgeJobStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeJobFileFromBytes(identifier string, data []byte) (string, error) {
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, identifier)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
filePath := JoinPaths(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, filePath), nil
|
||||
return service.wrapFileStore(filePath), nil
|
||||
}
|
||||
|
||||
func createEdgeJobFileName(identifier string) string {
|
||||
@@ -532,20 +526,14 @@ func createEdgeJobFileName(identifier string) string {
|
||||
// ClearEdgeJobTaskLogs clears the Edge job task logs
|
||||
func (service *Service) ClearEdgeJobTaskLogs(edgeJobID string, taskID string) error {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
|
||||
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -555,20 +543,15 @@ func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID st
|
||||
|
||||
// StoreEdgeJobTaskLogFileFromBytes stores the log file
|
||||
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, edgeJobID)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return service.createFileInStore(filePath, r)
|
||||
}
|
||||
|
||||
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
|
||||
@@ -582,7 +565,7 @@ func (service *Service) GetTemporaryPath() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
|
||||
return JoinPaths(service.wrapFileStore(TempPath), uid.String()), nil
|
||||
}
|
||||
|
||||
// GetDataStorePath returns path to data folder
|
||||
@@ -591,12 +574,12 @@ func (service *Service) GetDatastorePath() string {
|
||||
}
|
||||
|
||||
func (service *Service) wrapFileStore(filepath string) string {
|
||||
return path.Join(service.fileStorePath, filepath)
|
||||
return JoinPaths(service.fileStorePath, filepath)
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
|
||||
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
|
||||
70
api/filesystem/filesystem_linux_test.go
Normal file
70
api/filesystem/filesystem_linux_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", "d/e/f"},
|
||||
{"", "./d/e/f", "d/e/f"},
|
||||
{"", "../d/e/f", "d/e/f"},
|
||||
{"", "/d/e/f", "d/e/f"},
|
||||
{"", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", "d/e/f"},
|
||||
{".", "./d/e/f", "d/e/f"},
|
||||
{".", "../d/e/f", "d/e/f"},
|
||||
{".", "/d/e/f", "d/e/f"},
|
||||
{".", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", "d/e/f"},
|
||||
{"./", "./d/e/f", "d/e/f"},
|
||||
{"./", "../d/e/f", "d/e/f"},
|
||||
{"./", "/d/e/f", "d/e/f"},
|
||||
{"./", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"a/b/c", "", "a/b/c"},
|
||||
{"a/b/c", ".", "a/b/c"},
|
||||
{"a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"/a/b/c", "", "/a/b/c"},
|
||||
{"/a/b/c", ".", "/a/b/c"},
|
||||
{"/a/b/c", "d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "./d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../../../etc/shadow", "/a/b/c/etc/shadow"},
|
||||
|
||||
{"./a/b/c", "", "a/b/c"},
|
||||
{"./a/b/c", ".", "a/b/c"},
|
||||
{"./a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"../a/b/c", "", "../a/b/c"},
|
||||
{"../a/b/c", ".", "../a/b/c"},
|
||||
{"../a/b/c", "d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "./d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../../../etc/shadow", "../a/b/c/etc/shadow"},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
api/filesystem/filesystem_windows_test.go
Normal file
120
api/filesystem/filesystem_windows_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", `d\e\f`},
|
||||
{"", "./d/e/f", `d\e\f`},
|
||||
{"", "../d/e/f", `d\e\f`},
|
||||
{"", "/d/e/f", `d\e\f`},
|
||||
{"", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", `d\e\f`},
|
||||
{".", "./d/e/f", `d\e\f`},
|
||||
{".", "../d/e/f", `d\e\f`},
|
||||
{".", "/d/e/f", `d\e\f`},
|
||||
{".", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{".", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{".", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", `d\e\f`},
|
||||
{"./", "./d/e/f", `d\e\f`},
|
||||
{"./", "../d/e/f", `d\e\f`},
|
||||
{"./", "/d/e/f", `d\e\f`},
|
||||
{"./", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"./", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"./", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"a/b/c", "", `a\b\c`},
|
||||
{"a/b/c", ".", `a\b\c`},
|
||||
{"a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"/a/b/c", "", `\a\b\c`},
|
||||
{"/a/b/c", ".", `\a\b\c`},
|
||||
{"/a/b/c", "d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "./d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../../../windows/system.ini", `\a\b\c\windows\system.ini`},
|
||||
{"/a/b/c", `C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `..\..\..\..\C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
{"/a/b/c", `..\..\..\..\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
|
||||
{"./a/b/c", "", `a\b\c`},
|
||||
{"./a/b/c", ".", `a\b\c`},
|
||||
{"./a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"./a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"./a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"../a/b/c", "", `..\a\b\c`},
|
||||
{"../a/b/c", ".", `..\a\b\c`},
|
||||
{"../a/b/c", "d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "./d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../../../windows/system.ini", `..\a\b\c\windows\system.ini`},
|
||||
{"../a/b/c", `C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `..\..\..\..\C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
{"../a/b/c", `..\..\..\..\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
|
||||
{"C:/a/b/c", "", `C:\a\b\c`},
|
||||
{"C:/a/b/c", ".", `C:\a\b\c`},
|
||||
{"C:/a/b/c", "d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "./d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../../../windows/system.ini", `C:\a\b\c\windows\system.ini`},
|
||||
{"C:/a/b/c", `C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `..\..\..\..\C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
{"C:/a/b/c", `..\..\..\..\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
|
||||
{`\\server\a\b\c`, "", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, ".", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, "d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "./d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../../../windows/system.ini", `\\server\a\b\c\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `..\..\..\C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
{`\\server\a\b\c`, `..\..\..\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
api/filesystem/write.go
Normal file
23
api/filesystem/write.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WriteToFile(dst string, content []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
|
||||
}
|
||||
|
||||
file, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open a file %q", dst)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(content)
|
||||
return errors.Wrapf(err, "failed to write a file %q", dst)
|
||||
}
|
||||
48
api/filesystem/write_test.go
Normal file
48
api/filesystem/write_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||
|
||||
content := []byte("content")
|
||||
err := WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
|
||||
func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dummy")
|
||||
|
||||
err := WriteToFile(tmpFilePath, []byte("content"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
content := []byte("new content")
|
||||
err = WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
|
||||
func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFilePath := path.Join(tmpDir, "dir", "sub-dir", "dummy")
|
||||
|
||||
content := []byte("content")
|
||||
err := WriteToFile(tmpFilePath, content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := ioutil.ReadFile(tmpFilePath)
|
||||
assert.Equal(t, content, fileContent)
|
||||
}
|
||||
46
api/go.mod
46
api/go.mod
@@ -3,57 +3,49 @@ module github.com/portainer/portainer/api
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
|
||||
)
|
||||
|
||||
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
|
||||
|
||||
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456
|
||||
|
||||
956
api/go.sum
956
api/go.sum
File diff suppressed because it is too large
Load Diff
52
api/hostmanagement/openamt/authorization.go
Normal file
52
api/hostmanagement/openamt/authorization.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type authenticationResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (service *Service) executeAuthenticationRequest(configuration portainer.OpenAMTConfiguration) (*authenticationResponse, error) {
|
||||
loginURL := fmt.Sprintf("https://%s/mps/login/api/v1/authorize", configuration.MPSServer)
|
||||
|
||||
payload := map[string]string{
|
||||
"username": configuration.Credentials.MPSUser,
|
||||
"password": configuration.Credentials.MPSPassword,
|
||||
}
|
||||
jsonValue, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonValue))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
response, err := service.httpsClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseBody, readErr := ioutil.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
}
|
||||
|
||||
var token authenticationResponse
|
||||
err = json.Unmarshal(responseBody, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &token, nil
|
||||
}
|
||||
143
api/hostmanagement/openamt/configCIRA.go
Normal file
143
api/hostmanagement/openamt/configCIRA.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type CIRAConfig struct {
|
||||
ConfigName string `json:"configName"`
|
||||
MPSServerAddress string `json:"mpsServerAddress"`
|
||||
ServerAddressFormat int `json:"serverAddressFormat"`
|
||||
CommonName string `json:"commonName"`
|
||||
MPSPort int `json:"mpsPort"`
|
||||
Username string `json:"username"`
|
||||
MPSRootCertificate string `json:"mpsRootCertificate"`
|
||||
RegeneratePassword bool `json:"regeneratePassword"`
|
||||
AuthMethod int `json:"authMethod"`
|
||||
}
|
||||
|
||||
func (service *Service) createOrUpdateCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
|
||||
ciraConfig, err := service.getCIRAConfig(configuration, configName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
if ciraConfig != nil {
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
ciraConfig, err = service.saveCIRAConfig(method, configuration, configName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ciraConfig, nil
|
||||
}
|
||||
|
||||
func (service *Service) getCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/ciraconfigs/%s", configuration.MPSServer, configName)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result CIRAConfig
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveCIRAConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/ciraconfigs", configuration.MPSServer)
|
||||
|
||||
certificate, err := service.getCIRACertificate(configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addressFormat, err := addressFormat(configuration.MPSServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := CIRAConfig{
|
||||
ConfigName: configName,
|
||||
MPSServerAddress: configuration.MPSServer,
|
||||
CommonName: configuration.MPSServer,
|
||||
ServerAddressFormat: addressFormat,
|
||||
MPSPort: 4433,
|
||||
Username: "admin",
|
||||
MPSRootCertificate: certificate,
|
||||
RegeneratePassword: false,
|
||||
AuthMethod: 2,
|
||||
}
|
||||
payload, _ := json.Marshal(config)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result CIRAConfig
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func addressFormat(url string) (int, error) {
|
||||
ip := net.ParseIP(url)
|
||||
if ip == nil {
|
||||
return 201, nil // FQDN
|
||||
}
|
||||
if strings.Contains(url, ".") {
|
||||
return 3, nil // IPV4
|
||||
}
|
||||
if strings.Contains(url, ":") {
|
||||
return 4, nil // IPV6
|
||||
}
|
||||
return 0, fmt.Errorf("could not determine server address format for %s", url)
|
||||
}
|
||||
|
||||
func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfiguration) (string, error) {
|
||||
loginURL := fmt.Sprintf("https://%s/mps/api/v1/ciracert", configuration.MPSServer)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configuration.Credentials.MPSToken))
|
||||
|
||||
response, err := service.httpsClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
|
||||
}
|
||||
|
||||
certificate, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, _ := pem.Decode(certificate)
|
||||
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
||||
}
|
||||
81
api/hostmanagement/openamt/configDomain.go
Normal file
81
api/hostmanagement/openamt/configDomain.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
Domain struct {
|
||||
DomainName string `json:"profileName"`
|
||||
DomainSuffix string `json:"domainSuffix"`
|
||||
ProvisioningCert string `json:"provisioningCert"`
|
||||
ProvisioningCertPassword string `json:"provisioningCertPassword"`
|
||||
ProvisioningCertStorageFormat string `json:"provisioningCertStorageFormat"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *Service) createOrUpdateDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
|
||||
domain, err := service.getDomain(configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
if domain != nil {
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
domain, err = service.saveDomain(method, configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func (service *Service) getDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/domains/%s", configuration.MPSServer, configuration.DomainConfiguration.DomainName)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result Domain
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveDomain(method string, configuration portainer.OpenAMTConfiguration) (*Domain, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/domains", configuration.MPSServer)
|
||||
|
||||
profile := Domain{
|
||||
DomainName: configuration.DomainConfiguration.DomainName,
|
||||
DomainSuffix: configuration.DomainConfiguration.DomainName,
|
||||
ProvisioningCert: configuration.DomainConfiguration.CertFileText,
|
||||
ProvisioningCertPassword: configuration.DomainConfiguration.CertPassword,
|
||||
ProvisioningCertStorageFormat: "string",
|
||||
}
|
||||
payload, _ := json.Marshal(profile)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Domain
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
104
api/hostmanagement/openamt/configProfile.go
Normal file
104
api/hostmanagement/openamt/configProfile.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
Profile struct {
|
||||
ProfileName string `json:"profileName"`
|
||||
Activation string `json:"activation"`
|
||||
CIRAConfigName *string `json:"ciraConfigName"`
|
||||
GenerateRandomAMTPassword bool `json:"generateRandomPassword"`
|
||||
AMTPassword string `json:"amtPassword"`
|
||||
GenerateRandomMEBxPassword bool `json:"generateRandomMEBxPassword"`
|
||||
MEBXPassword string `json:"mebxPassword"`
|
||||
Tags []string `json:"tags"`
|
||||
DHCPEnabled bool `json:"dhcpEnabled"`
|
||||
TenantId string `json:"tenantId"`
|
||||
WIFIConfigs []ProfileWifiConfig `json:"wifiConfigs"`
|
||||
}
|
||||
|
||||
ProfileWifiConfig struct {
|
||||
Priority int `json:"priority"`
|
||||
ProfileName string `json:"profileName"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
|
||||
profile, err := service.getAMTProfile(configuration, profileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
if profile != nil {
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName, wirelessConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (service *Service) getAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string) (*Profile, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/profiles/%s", configuration.MPSServer, profileName)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result Profile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/profiles", configuration.MPSServer)
|
||||
|
||||
profile := Profile{
|
||||
ProfileName: profileName,
|
||||
Activation: "acmactivate",
|
||||
GenerateRandomAMTPassword: false,
|
||||
GenerateRandomMEBxPassword: false,
|
||||
AMTPassword: configuration.Credentials.MPSPassword,
|
||||
MEBXPassword: configuration.Credentials.MPSPassword,
|
||||
CIRAConfigName: &ciraConfigName,
|
||||
Tags: []string{},
|
||||
DHCPEnabled: true,
|
||||
}
|
||||
if wirelessConfig != "" {
|
||||
profile.WIFIConfigs = []ProfileWifiConfig{
|
||||
{
|
||||
Priority: 1,
|
||||
ProfileName: DefaultWirelessConfigName,
|
||||
},
|
||||
}
|
||||
}
|
||||
payload, _ := json.Marshal(profile)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Profile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
91
api/hostmanagement/openamt/configWireless.go
Normal file
91
api/hostmanagement/openamt/configWireless.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
WirelessProfile struct {
|
||||
ProfileName string `json:"profileName"`
|
||||
AuthenticationMethod int `json:"authenticationMethod"`
|
||||
EncryptionMethod int `json:"encryptionMethod"`
|
||||
SSID string `json:"ssid"`
|
||||
PSKPassphrase string `json:"pskPassphrase"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *Service) createOrUpdateWirelessConfig(configuration portainer.OpenAMTConfiguration, wirelessConfigName string) (*WirelessProfile, error) {
|
||||
wirelessConfig, err := service.getWirelessConfig(configuration, wirelessConfigName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
if wirelessConfig != nil {
|
||||
method = http.MethodPatch
|
||||
}
|
||||
|
||||
wirelessConfig, err = service.saveWirelessConfig(method, configuration, wirelessConfigName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wirelessConfig, nil
|
||||
}
|
||||
|
||||
func (service *Service) getWirelessConfig(configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs/%s", configuration.MPSServer, configName)
|
||||
|
||||
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseBody == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result WirelessProfile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (service *Service) saveWirelessConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
|
||||
parsedAuthenticationMethod, err := strconv.Atoi(configuration.WirelessConfiguration.AuthenticationMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing wireless authentication method: %s", err.Error())
|
||||
}
|
||||
parsedEncryptionMethod, err := strconv.Atoi(configuration.WirelessConfiguration.EncryptionMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing wireless encryption method: %s", err.Error())
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs", configuration.MPSServer)
|
||||
|
||||
config := WirelessProfile{
|
||||
ProfileName: configName,
|
||||
AuthenticationMethod: parsedAuthenticationMethod,
|
||||
EncryptionMethod: parsedEncryptionMethod,
|
||||
SSID: configuration.WirelessConfiguration.SSID,
|
||||
PSKPassphrase: configuration.WirelessConfiguration.PskPass,
|
||||
}
|
||||
payload, _ := json.Marshal(config)
|
||||
|
||||
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result WirelessProfile
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
157
api/hostmanagement/openamt/openamt.go
Normal file
157
api/hostmanagement/openamt/openamt.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package openamt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCIRAConfigName = "ciraConfigDefault"
|
||||
DefaultWirelessConfigName = "wirelessProfileDefault"
|
||||
DefaultProfileName = "profileAMTDefault"
|
||||
)
|
||||
|
||||
// Service represents a service for managing an OpenAMT server.
|
||||
type Service struct {
|
||||
httpsClient *http.Client
|
||||
}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(dataStore portainer.DataStore) *Service {
|
||||
if !dataStore.Settings().IsFeatureFlagEnabled(portainer.FeatOpenAMT) {
|
||||
return nil
|
||||
}
|
||||
return &Service{
|
||||
httpsClient: &http.Client{
|
||||
Timeout: time.Second * time.Duration(5),
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type openAMTError struct {
|
||||
ErrorMsg string `json:"message"`
|
||||
Errors []struct {
|
||||
ErrorMsg string `json:"msg"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func parseError(responseBody []byte) error {
|
||||
var errorResponse openAMTError
|
||||
err := json.Unmarshal(responseBody, &errorResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(errorResponse.Errors) > 0 {
|
||||
return errors.New(errorResponse.Errors[0].ErrorMsg)
|
||||
}
|
||||
if errorResponse.ErrorMsg != "" {
|
||||
return errors.New(errorResponse.ErrorMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) ConfigureDefault(configuration portainer.OpenAMTConfiguration) error {
|
||||
token, err := service.executeAuthenticationRequest(configuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuration.Credentials.MPSToken = token.Token
|
||||
|
||||
ciraConfig, err := service.createOrUpdateCIRAConfig(configuration, DefaultCIRAConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wirelessConfigName := ""
|
||||
if configuration.WirelessConfiguration != nil {
|
||||
wirelessConfig, err := service.createOrUpdateWirelessConfig(configuration, DefaultWirelessConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wirelessConfigName = wirelessConfig.ProfileName
|
||||
}
|
||||
|
||||
_, err = service.createOrUpdateAMTProfile(configuration, DefaultProfileName, ciraConfig.ConfigName, wirelessConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = service.createOrUpdateDomain(configuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) executeSaveRequest(method string, url string, token string, payload []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
response, err := service.httpsClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseBody, readErr := ioutil.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
if response.StatusCode < 200 || response.StatusCode > 300 {
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
|
||||
}
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
|
||||
func (service *Service) executeGetRequest(url string, token string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
response, err := service.httpsClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseBody, readErr := ioutil.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
if response.StatusCode < 200 || response.StatusCode > 300 {
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
|
||||
}
|
||||
|
||||
return responseBody, nil
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func Get(url string, timeout int) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment
|
||||
// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment(endpoint)
|
||||
// using the specified host and optional TLS configuration.
|
||||
// It uses a new Http.Client for each operation.
|
||||
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package errors
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrEndpointAccessDenied Access denied to endpoint error
|
||||
// ErrEndpointAccessDenied Access denied to environment(endpoint) error
|
||||
ErrEndpointAccessDenied = errors.New("Access denied to environment")
|
||||
// ErrUnauthorized Unauthorized error
|
||||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
|
||||
@@ -39,7 +39,8 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id AuthenticateUser
|
||||
// @summary Authenticate
|
||||
// @description Use this endpoint to authenticate against Portainer using a username and password.
|
||||
// @description **Access policy**: public
|
||||
// @description Use this environment(endpoint) to authenticate against Portainer using a username and password.
|
||||
// @tags auth
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -44,6 +44,7 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
|
||||
|
||||
// @id ValidateOAuth
|
||||
// @summary Authenticate with OAuth
|
||||
// @description **Access policy**: public
|
||||
// @tags auth
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
// @success 204 "Success"
|
||||
|
||||
@@ -26,9 +26,11 @@ func (p *backupPayload) Validate(r *http.Request) error {
|
||||
// @description Creates an archive with a system data snapshot that could be used to restore the system.
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce octet-stream
|
||||
// @param Password body string false "Password to encrypt the backup with"
|
||||
// @param body body backupPayload false "An object contains the password to encrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user