Compare commits
323 Commits
feat/EE-25
...
feat/EE-35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05ac5ae577 | ||
|
|
17865fa1a5 | ||
|
|
1fcf523267 | ||
|
|
4a73d46acb | ||
|
|
1e78234f04 | ||
|
|
d0a9c046b3 | ||
|
|
c54bb255ba | ||
|
|
8843b7b0e8 | ||
|
|
a95d734c34 | ||
|
|
8262487401 | ||
|
|
57e53d1a21 | ||
|
|
e28a1491d4 | ||
|
|
9342ba9792 | ||
|
|
2552eb5e25 | ||
|
|
ddaf9dc885 | ||
|
|
11c778cfeb | ||
|
|
11dffdee9a | ||
|
|
d4d80ed8f7 | ||
|
|
0ba10b44ec | ||
|
|
0f617f7f87 | ||
|
|
423dd5e394 | ||
|
|
44737029a9 | ||
|
|
ce22544c60 | ||
|
|
9106e74e61 | ||
|
|
6c57ddb563 | ||
|
|
a2e1570162 | ||
|
|
ea60740d48 | ||
|
|
762c664948 | ||
|
|
d574a71cb1 | ||
|
|
bb066cd58c | ||
|
|
e779939ae1 | ||
|
|
aa830a0e58 | ||
|
|
52ac54f15c | ||
|
|
cc0ab75aca | ||
|
|
7e3347da2b | ||
|
|
87e9d7f8d4 | ||
|
|
6d3a33635d | ||
|
|
090268d7b6 | ||
|
|
698a91596e | ||
|
|
bb447bb02a | ||
|
|
5ffcbe8677 | ||
|
|
ac6296b86d | ||
|
|
3239a61bda | ||
|
|
2a43285593 | ||
|
|
36071837cb | ||
|
|
1ef713d80b | ||
|
|
82b848af0c | ||
|
|
b059641c80 | ||
|
|
728e885b9d | ||
|
|
3acefba069 | ||
|
|
9205f67791 | ||
|
|
6d95643a68 | ||
|
|
149c414d08 | ||
|
|
f8b4663e0a | ||
|
|
7b774c702d | ||
|
|
8045a15a50 | ||
|
|
9a18dd8162 | ||
|
|
70a7eefa22 | ||
|
|
3356d1abe2 | ||
|
|
7ee8dac832 | ||
|
|
5b3f099f4e | ||
|
|
5f5cb36df1 | ||
|
|
3645ff7459 | ||
|
|
9a92b97b7e | ||
|
|
005c48b1ad | ||
|
|
4fb1880ddc | ||
|
|
54145ce949 | ||
|
|
b040aa1e78 | ||
|
|
985eef6987 | ||
|
|
a5c3116b0c | ||
|
|
df381b6a33 | ||
|
|
9223c0226a | ||
|
|
314fdc850e | ||
|
|
43bbeed141 | ||
|
|
e07253bcef | ||
|
|
23b9baa059 | ||
|
|
05357ecce5 | ||
|
|
1a8fe82821 | ||
|
|
95f4db4f48 | ||
|
|
43600083a7 | ||
|
|
e6477b0b97 | ||
|
|
6aa7fdb4f2 | ||
|
|
4997e9c7be | ||
|
|
f0456cbf5f | ||
|
|
a0d349e0b3 | ||
|
|
f5e774c89d | ||
|
|
552d3f8a3e | ||
|
|
39f9173956 | ||
|
|
e4fc41fc94 | ||
|
|
ce7d234cba | ||
|
|
35701f5899 | ||
|
|
3d4d2b50ae | ||
|
|
0da4e3ae63 | ||
|
|
ad7055ee01 | ||
|
|
8076455423 | ||
|
|
23eca3ce80 | ||
|
|
4cc672f902 | ||
|
|
82fb5f7ac1 | ||
|
|
de59ea030a | ||
|
|
d9be6d1724 | ||
|
|
958a8e97e9 | ||
|
|
5fd202d629 | ||
|
|
768f1aa663 | ||
|
|
69caa1179f | ||
|
|
9a2cdc4a93 | ||
|
|
14a8b1d897 | ||
|
|
712207e69f | ||
|
|
8d46692d66 | ||
|
|
3241738775 | ||
|
|
ce840997bf | ||
|
|
88c4a43a19 | ||
|
|
b4acbfc9e1 | ||
|
|
8bf1c91bc9 | ||
|
|
a66fd78dc1 | ||
|
|
b004b33935 | ||
|
|
d32793e84e | ||
|
|
fd4b515350 | ||
|
|
0cd2a4558b | ||
|
|
89359a21ce | ||
|
|
69baa279d4 | ||
|
|
33861a834b | ||
|
|
dd4d126934 | ||
|
|
7275d23e4b | ||
|
|
d7306fb22e | ||
|
|
ebc0a8c772 | ||
|
|
f26e1fa21b | ||
|
|
6b27ba9121 | ||
|
|
975dc9c1da | ||
|
|
6fe26a52dd | ||
|
|
cd66e32912 | ||
|
|
81f8b88541 | ||
|
|
882051cc30 | ||
|
|
ed8f9b5931 | ||
|
|
e5e57978af | ||
|
|
75fef397d3 | ||
|
|
624490716e | ||
|
|
8eff32ebc7 | ||
|
|
cd19eb036b | ||
|
|
95f706aabe | ||
|
|
1551b02fde | ||
|
|
557f4773cf | ||
|
|
b84e1c8550 | ||
|
|
46e1a01625 | ||
|
|
7238372d8d | ||
|
|
00126cd08a | ||
|
|
58c44ad1ea | ||
|
|
84611a90a1 | ||
|
|
f78a6568a6 | ||
|
|
825269c119 | ||
|
|
60cd7b5527 | ||
|
|
767fabe0ce | ||
|
|
f86ba7b176 | ||
|
|
912250732a | ||
|
|
ae731b5496 | ||
|
|
92eaa02156 | ||
|
|
18252ab854 | ||
|
|
212400c283 | ||
|
|
8ed41de815 | ||
|
|
97a880e6c1 | ||
|
|
f39775752d | ||
|
|
6d6c70a98b | ||
|
|
461fc91446 | ||
|
|
8059cae8e7 | ||
|
|
41107191c3 | ||
|
|
cb6a5fa41d | ||
|
|
66799a53f4 | ||
|
|
892fdbf60d | ||
|
|
b6309682ef | ||
|
|
be11dfc231 | ||
|
|
12527aa820 | ||
|
|
0d0f9499eb | ||
|
|
60eab3e263 | ||
|
|
eb547162e9 | ||
|
|
0864c371e8 | ||
|
|
b90b1701e9 | ||
|
|
eb4ff12744 | ||
|
|
0522032515 | ||
|
|
bca1c6b9cf | ||
|
|
4195d93a16 | ||
|
|
e8a8b71daa | ||
|
|
aea62723c0 | ||
|
|
9b58c2e466 | ||
|
|
c41f7f8270 | ||
|
|
ac096dda46 | ||
|
|
e686d64011 | ||
|
|
1ccdb64938 | ||
|
|
71c0e8e661 | ||
|
|
c162e180e0 | ||
|
|
e806f74652 | ||
|
|
d52417c14f | ||
|
|
75d854e6ad | ||
|
|
0b2217a916 | ||
|
|
ca30efeca7 | ||
|
|
dc98850489 | ||
|
|
01dc9066b7 | ||
|
|
3aacaa7caf | ||
|
|
b031a30f62 | ||
|
|
12cddbd896 | ||
|
|
3791b7a16f | ||
|
|
d754532ab1 | ||
|
|
9a48ceaec1 | ||
|
|
1132c9ce87 | ||
|
|
668d526604 | ||
|
|
0e257c200f | ||
|
|
df05914fac | ||
|
|
0ffb84aaa6 | ||
|
|
b01180bb29 | ||
|
|
16f8b737f1 | ||
|
|
d9d1d6bfaa | ||
|
|
45b300eaff | ||
|
|
ad7545f009 | ||
|
|
5df30b9eb0 | ||
|
|
2e0555dbca | ||
|
|
9650aa56c7 | ||
|
|
0beb0d95c1 | ||
|
|
3de585fe17 | ||
|
|
c732ca2d2f | ||
|
|
d4c2ad4a57 | ||
|
|
bf59ef50a3 | ||
|
|
840a3ce732 | ||
|
|
f7780cecb3 | ||
|
|
24c61034c1 | ||
|
|
95b3fff917 | ||
|
|
0f52188261 | ||
|
|
b1b0a76465 | ||
|
|
8a6024ce9b | ||
|
|
61a3bfe994 | ||
|
|
842044e759 | ||
|
|
b3e035d353 | ||
|
|
33f433ce45 | ||
|
|
abb79ccbeb | ||
|
|
c340b62f43 | ||
|
|
bbb096412d | ||
|
|
141a530e28 | ||
|
|
d08b498cb9 | ||
|
|
bebee78152 | ||
|
|
5b77edb76d | ||
|
|
bcec6a8915 | ||
|
|
3496d5f00b | ||
|
|
4ee5ae90e7 | ||
|
|
4180e41fa1 | ||
|
|
5289e4d66b | ||
|
|
ace162ec1c | ||
|
|
a9887d4a31 | ||
|
|
8ce3e7581b | ||
|
|
9de0704775 | ||
|
|
e20c34e12a | ||
|
|
e217ac7121 | ||
|
|
76d1b70644 | ||
|
|
360701e256 | ||
|
|
7efdae5eee | ||
|
|
da9ef7dfcf | ||
|
|
69c34cdf0c | ||
|
|
030b3d7c4d | ||
|
|
355674cf22 | ||
|
|
85a7b7e0fc | ||
|
|
328ce2f995 | ||
|
|
e4241207cb | ||
|
|
85ad4e334a | ||
|
|
9ebc963082 | ||
|
|
3178787bc1 | ||
|
|
b08e0b0235 | ||
|
|
aac2aca912 | ||
|
|
f707c90cd3 | ||
|
|
3eea3e88bc | ||
|
|
13faa75a2d | ||
|
|
287107e8da | ||
|
|
2535887984 | ||
|
|
f12c3968f1 | ||
|
|
6419e7740a | ||
|
|
298e3d263e | ||
|
|
9ffaf47741 | ||
|
|
dff74f0823 | ||
|
|
f9f937f844 | ||
|
|
77e48bfb74 | ||
|
|
f4ac6f8320 | ||
|
|
bf8b44834a | ||
|
|
3c98bf9a79 | ||
|
|
e1df46b92b | ||
|
|
7e28b3ca3f | ||
|
|
2059a9e064 | ||
|
|
167825ff3f | ||
|
|
f154e6e0f1 | ||
|
|
311129e746 | ||
|
|
f59459f936 | ||
|
|
ee90fffce1 | ||
|
|
4ddd6663f5 | ||
|
|
ec3d7026d4 | ||
|
|
fb7f24df9c | ||
|
|
8860d72f70 | ||
|
|
b846c8e6d2 | ||
|
|
379f9e2822 | ||
|
|
3579b11a8b | ||
|
|
4377aec72b | ||
|
|
c486130a9f | ||
|
|
cf7746082b | ||
|
|
1ab65a4b4f | ||
|
|
a66e863646 | ||
|
|
d962c300f9 | ||
|
|
9aeedf1bfa | ||
|
|
98d8cd99fb | ||
|
|
226ffdcd20 | ||
|
|
78150a738f | ||
|
|
ecf5e90783 | ||
|
|
f63b07bbb9 | ||
|
|
07294c19bb | ||
|
|
f8cbb54ba5 | ||
|
|
f8fd28bb61 | ||
|
|
78f7cd0d6c | ||
|
|
9a42d4c506 | ||
|
|
f2c48409e0 | ||
|
|
5188ead870 | ||
|
|
f1ea2b5c02 | ||
|
|
b7d18ef50f | ||
|
|
20405e9803 | ||
|
|
0f3c7b1424 | ||
|
|
c442d936d3 | ||
|
|
0cd164bada | ||
|
|
ee42e44246 | ||
|
|
6695d75468 | ||
|
|
eb6cdf1229 | ||
|
|
a3b1466b96 | ||
|
|
8b7dcf20bf |
@@ -1,3 +1,5 @@
|
||||
*
|
||||
!dist
|
||||
!build
|
||||
!metadata.json
|
||||
!docker-extension/build
|
||||
|
||||
@@ -31,7 +31,12 @@ rules:
|
||||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
|
||||
pathGroups:
|
||||
[
|
||||
{ pattern: '@@/**', group: 'internal', position: 'after' },
|
||||
{ pattern: '@/**', group: 'internal' },
|
||||
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
|
||||
],
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroupsExcludedImportTypes: ['internal'],
|
||||
},
|
||||
@@ -41,6 +46,7 @@ settings:
|
||||
'import/resolver':
|
||||
alias:
|
||||
map:
|
||||
- ['@@', './app/react/components']
|
||||
- ['@', './app']
|
||||
extensions: ['.js', '.ts', '.tsx']
|
||||
|
||||
@@ -52,6 +58,7 @@ overrides:
|
||||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
- 'regex'
|
||||
extends:
|
||||
- airbnb
|
||||
- airbnb-typescript
|
||||
@@ -68,7 +75,14 @@ overrides:
|
||||
version: 'detect'
|
||||
rules:
|
||||
import/order:
|
||||
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
|
||||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { 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 }]
|
||||
@@ -90,6 +104,7 @@ overrides:
|
||||
'react/jsx-no-bind': off
|
||||
'no-await-in-loop': 'off'
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
||||
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
|
||||
17
.github/workflows/lint.yml
vendored
17
.github/workflows/lint.yml
vendored
@@ -18,17 +18,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
# ESLint and Prettier must be in `package.json`
|
||||
- name: Install Node.js dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
@@ -39,3 +34,5 @@ jobs:
|
||||
prettier_dir: app/
|
||||
gofmt: true
|
||||
gofmt_dir: api/
|
||||
- name: Typecheck
|
||||
uses: icrawl/action-tsc@v1
|
||||
|
||||
230
.github/workflows/nightly-security-scan.yml
vendored
Normal file
230
.github/workflows/nightly-security-scan.yml
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=js_result::${result}"
|
||||
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/golang@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=go_result::${result}"
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the trivy result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
||||
echo "::set-output name=image_result::${result}"
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||
steps:
|
||||
- name: Display the results of js, go and image
|
||||
run: |
|
||||
echo ${{ matrix.js.status }}
|
||||
echo ${{ matrix.go.status }}
|
||||
echo ${{ matrix.image.status }}
|
||||
echo ${{ matrix.js.summary }}
|
||||
echo ${{ matrix.go.summary }}
|
||||
echo ${{ matrix.image.summary }}
|
||||
|
||||
- name: Send Slack message
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.18.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"attachments": [
|
||||
{
|
||||
"color": "#FF0000",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*Image vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
233
.github/workflows/pr-security.yml
vendored
Normal file
233
.github/workflows/pr-security.yml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
name: PR Code Security Scan
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
- edited
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'api/go.mod'
|
||||
- 'gruntfile.js'
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-feat-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mv ./snyk.json ./js-snyk-feature.json
|
||||
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
|
||||
if [[ -e ./snyk.json ]]; then
|
||||
mv ./snyk.json ./js-snyk-develop.json
|
||||
else
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-compare-to-develop-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=js_diff_result::${result}"
|
||||
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/golang@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-feature-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mv ./snyk.json ./go-snyk-feature.json
|
||||
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
|
||||
if [[ -e ./snyk.json ]]; then
|
||||
mv ./snyk.json ./go-snyk-develop.json
|
||||
else
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-compare-to-develop-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=go_diff_result::${result}"
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-feature-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mv ./image-trivy.json ./image-trivy-feature.json
|
||||
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
|
||||
if [[ -e ./image-trivy.json ]]; then
|
||||
mv ./image-trivy.json ./image-trivy-develop.json
|
||||
else
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
|
||||
- name: Upload image result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the image diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=image_diff_result::${result}"
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result compared to develop
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
strategy:
|
||||
matrix:
|
||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
||||
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
||||
steps:
|
||||
|
||||
- name: Check job status of diff result
|
||||
if: >-
|
||||
matrix.jsdiff.status == 'failure' ||
|
||||
matrix.godiff.status == 'failure' ||
|
||||
matrix.imagediff.status == 'failure'
|
||||
run: |
|
||||
echo ${{ matrix.jsdiff.status }}
|
||||
echo ${{ matrix.godiff.status }}
|
||||
echo ${{ matrix.imagediff.status }}
|
||||
echo ${{ matrix.jsdiff.summary }}
|
||||
echo ${{ matrix.godiff.summary }}
|
||||
echo ${{ matrix.imagediff.summary }}
|
||||
exit 1
|
||||
11
.github/workflows/test-client.yaml
vendored
11
.github/workflows/test-client.yaml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Test Frontend
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
29
.github/workflows/test.yaml
vendored
Normal file
29
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Test
|
||||
on: push
|
||||
jobs:
|
||||
test-client:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
# test-server:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# GOPRIVATE: "github.com/portainer"
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - uses: actions/setup-go@v3
|
||||
# with:
|
||||
# go-version: '1.18'
|
||||
# - name: Run tests
|
||||
# run: |
|
||||
# cd api
|
||||
# go test ./...
|
||||
@@ -1 +1,2 @@
|
||||
dist
|
||||
dist
|
||||
api/datastore/test_data
|
||||
@@ -16,6 +16,9 @@ module.exports = {
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import '../app/assets/css';
|
||||
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
|
||||
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
|
||||
import { handlers } from '@/setup-tests/server-handlers';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
// Initialize MSW
|
||||
initMSW({
|
||||
@@ -31,11 +32,17 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
const testQueryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||
<Story />
|
||||
</UIRouter>
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||
<Story />
|
||||
</UIRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
||||
@@ -22,7 +22,7 @@ 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.9.x**. Portainer is on version 2, the second number denotes the month of release.
|
||||
**The latest version of Portainer is 2.13.x**.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -3,32 +3,48 @@ package adminmonitor
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
var logFatalf = log.Fatalf
|
||||
|
||||
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
|
||||
|
||||
type Monitor struct {
|
||||
timeout time.Duration
|
||||
datastore dataservices.DataStore
|
||||
shutdownCtx context.Context
|
||||
cancellationFunc context.CancelFunc
|
||||
timeout time.Duration
|
||||
datastore dataservices.DataStore
|
||||
shutdownCtx context.Context
|
||||
cancellationFunc context.CancelFunc
|
||||
mu sync.Mutex
|
||||
adminInitDisabled bool
|
||||
}
|
||||
|
||||
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
|
||||
// New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application
|
||||
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
|
||||
return &Monitor{
|
||||
timeout: timeout,
|
||||
datastore: datastore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
timeout: timeout,
|
||||
datastore: datastore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
adminInitDisabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
|
||||
func (m *Monitor) Start() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.cancellationFunc != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
|
||||
m.cancellationFunc = cancellationFunc
|
||||
|
||||
@@ -41,7 +57,11 @@ func (m *Monitor) Start() {
|
||||
logFatalf("%s", err)
|
||||
}
|
||||
if !initialized {
|
||||
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
|
||||
log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer")
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.adminInitDisabled = true
|
||||
return
|
||||
}
|
||||
case <-cancellationCtx.Done():
|
||||
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
|
||||
@@ -53,6 +73,9 @@ func (m *Monitor) Start() {
|
||||
|
||||
// Stop stops monitor. Safe to call even if monitor wasn't started.
|
||||
func (m *Monitor) Stop() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.cancellationFunc == nil {
|
||||
return
|
||||
}
|
||||
@@ -68,3 +91,25 @@ func (m *Monitor) WasInitialized() (bool, error) {
|
||||
}
|
||||
return len(users) > 0, nil
|
||||
}
|
||||
|
||||
func (m *Monitor) WasInstanceDisabled() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.adminInitDisabled
|
||||
}
|
||||
|
||||
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
|
||||
// Otherwise, it will pass through the request to next
|
||||
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if m.WasInstanceDisabled() {
|
||||
if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
|
||||
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
|
||||
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
|
||||
monitor.Stop()
|
||||
}
|
||||
|
||||
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
|
||||
monitor := New(1*time.Minute, nil, context.Background())
|
||||
|
||||
go monitor.Start()
|
||||
monitor.Start()
|
||||
|
||||
go monitor.Stop()
|
||||
monitor.Stop()
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
func Test_canStopStartedMonitor(t *testing.T) {
|
||||
monitor := New(1*time.Minute, nil, context.Background())
|
||||
monitor.Start()
|
||||
@@ -30,21 +42,13 @@ func Test_canStopStartedMonitor(t *testing.T) {
|
||||
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
|
||||
}
|
||||
|
||||
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
|
||||
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
|
||||
timeout := 10 * time.Millisecond
|
||||
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
|
||||
|
||||
var fataled bool
|
||||
origLogFatalf := logFatalf
|
||||
logFatalf = func(s string, v ...interface{}) { fataled = true }
|
||||
defer func() {
|
||||
logFatalf = origLogFatalf
|
||||
}()
|
||||
|
||||
monitor := New(timeout, datastore, context.Background())
|
||||
monitor.Start()
|
||||
<-time.After(2 * timeout)
|
||||
|
||||
assert.True(t, fataled, "monitor should been timeout and fatal")
|
||||
<-time.After(20 * timeout)
|
||||
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
|
||||
}
|
||||
|
||||
@@ -50,4 +50,15 @@ Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you
|
||||
|
||||
To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(endpoint) (which is not documented below due to Swagger limitations). This environment(endpoint) has a restricted access policy so you still need to be authenticated to be able to query this environment(endpoint). Any query on this environment(endpoint) will be proxied to the Docker API of the associated environment(endpoint) (requests and responses objects are the same as documented in the Docker API).
|
||||
|
||||
# Private Registry
|
||||
|
||||
Using private registry, you will need to pass a based64 encoded JSON string ‘{"registryId":\<registryID value\>}’ inside the Request Header. The parameter name is "X-Registry-Auth".
|
||||
\<registryID value\> - The registry ID where the repository was created.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
eyJyZWdpc3RyeUlkIjoxfQ==
|
||||
```
|
||||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).
|
||||
|
||||
@@ -20,7 +20,7 @@ func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
|
||||
func Test_GenerateApiKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -74,7 +74,7 @@ func Test_GenerateApiKey(t *testing.T) {
|
||||
func Test_GetAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -94,7 +94,7 @@ func Test_GetAPIKey(t *testing.T) {
|
||||
func Test_GetAPIKeys(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -115,7 +115,7 @@ func Test_GetAPIKeys(t *testing.T) {
|
||||
func Test_GetDigestUserAndKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -151,7 +151,7 @@ func Test_GetDigestUserAndKey(t *testing.T) {
|
||||
func Test_UpdateAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -199,7 +199,7 @@ func Test_UpdateAPIKey(t *testing.T) {
|
||||
func Test_DeleteAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
@@ -240,7 +240,7 @@ func Test_DeleteAPIKey(t *testing.T) {
|
||||
func Test_InvalidateUserKeyCache(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
9
api/build/variables.go
Normal file
9
api/build/variables.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package build
|
||||
|
||||
// Variables to be set during the build time
|
||||
var BuildNumber string
|
||||
var ImageTag string
|
||||
var NodejsVersion string
|
||||
var YarnVersion string
|
||||
var WebpackVersion string
|
||||
var GoVersion string
|
||||
@@ -1,14 +1,13 @@
|
||||
package chisel
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
existingJobIndex := -1
|
||||
for idx, existingJob := range tunnel.Jobs {
|
||||
@@ -24,24 +23,25 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
||||
}
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
|
||||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnelDetails := item.Val.(*portainer.TunnelDetails)
|
||||
service.mu.Lock()
|
||||
|
||||
updatedJobs := make([]portainer.EdgeJob, 0)
|
||||
for _, edgeJob := range tunnelDetails.Jobs {
|
||||
if edgeJob.ID == edgeJobID {
|
||||
continue
|
||||
for _, tunnel := range service.tunnelDetailsMap {
|
||||
// Filter in-place
|
||||
n := 0
|
||||
for _, edgeJob := range tunnel.Jobs {
|
||||
if edgeJob.ID != edgeJobID {
|
||||
tunnel.Jobs[n] = edgeJob
|
||||
n++
|
||||
}
|
||||
updatedJobs = append(updatedJobs, edgeJob)
|
||||
}
|
||||
|
||||
tunnelDetails.Jobs = updatedJobs
|
||||
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
|
||||
tunnel.Jobs = tunnel.Jobs[:n]
|
||||
}
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
chserver "github.com/jpillora/chisel/server"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,18 +27,19 @@ const (
|
||||
type Service struct {
|
||||
serverFingerprint string
|
||||
serverPort string
|
||||
tunnelDetailsMap cmap.ConcurrentMap
|
||||
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
|
||||
dataStore dataservices.DataStore
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
|
||||
return &Service{
|
||||
tunnelDetailsMap: cmap.New(),
|
||||
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
||||
dataStore: dataStore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
}
|
||||
@@ -58,11 +58,7 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
_, err = httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
|
||||
@@ -185,42 +181,37 @@ func (service *Service) startTunnelVerificationLoop() {
|
||||
}
|
||||
|
||||
func (service *Service) checkTunnels() {
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
|
||||
|
||||
service.mu.Lock()
|
||||
for key, tunnel := range service.tunnelDetailsMap {
|
||||
tunnels[key] = *tunnel
|
||||
}
|
||||
service.mu.Unlock()
|
||||
|
||||
for endpointID, tunnel := range tunnels {
|
||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||
continue
|
||||
}
|
||||
|
||||
elapsed := time.Since(tunnel.LastActivity)
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
||||
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
err := service.snapshotEnvironment(endpointID, tunnel.Port)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
|
||||
}
|
||||
|
||||
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
|
||||
@@ -4,13 +4,11 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/portainer/libcrypto"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
@@ -19,13 +17,13 @@ const (
|
||||
maxAvailablePort = 65535
|
||||
)
|
||||
|
||||
// NOTE: it needs to be called with the lock acquired
|
||||
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
||||
// Dynamic ports (also called private ports) are 49152 to 65535.
|
||||
func (service *Service) getUnusedPort() int {
|
||||
port := randomInt(minAvailablePort, maxAvailablePort)
|
||||
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||
for _, tunnel := range service.tunnelDetailsMap {
|
||||
if tunnel.Port == port {
|
||||
return service.getUnusedPort()
|
||||
}
|
||||
@@ -38,26 +36,32 @@ func randomInt(min, max int) int {
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
||||
|
||||
// NOTE: it needs to be called with the lock acquired
|
||||
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
|
||||
|
||||
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
|
||||
return tunnel
|
||||
}
|
||||
|
||||
tunnel := &portainer.TunnelDetails{
|
||||
Status: portainer.EdgeAgentIdle,
|
||||
}
|
||||
|
||||
service.tunnelDetailsMap[endpointID] = tunnel
|
||||
|
||||
return tunnel
|
||||
}
|
||||
|
||||
// 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))
|
||||
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
if item, ok := service.tunnelDetailsMap.Get(key); ok {
|
||||
tunnelDetails := item.(*portainer.TunnelDetails)
|
||||
return tunnelDetails
|
||||
}
|
||||
|
||||
jobs := make([]portainer.EdgeJob, 0)
|
||||
return &portainer.TunnelDetails{
|
||||
Status: portainer.EdgeAgentIdle,
|
||||
Port: 0,
|
||||
Jobs: jobs,
|
||||
Credentials: "",
|
||||
}
|
||||
return *service.getTunnelDetails(endpointID)
|
||||
}
|
||||
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
@@ -68,13 +72,13 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
|
||||
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)
|
||||
return portainer.TunnelDetails{}, 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)
|
||||
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
|
||||
}
|
||||
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
@@ -83,29 +87,27 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
|
||||
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
|
||||
}
|
||||
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
return tunnel, nil
|
||||
return service.GetTunnelDetails(endpoint.ID), 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)
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
tunnel.Status = portainer.EdgeAgentActive
|
||||
tunnel.Credentials = ""
|
||||
tunnel.LastActivity = time.Now()
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
|
||||
// It sets the status to IDLE.
|
||||
// It removes any existing credentials associated to the tunnel.
|
||||
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
service.mu.Lock()
|
||||
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
tunnel.Status = portainer.EdgeAgentIdle
|
||||
tunnel.Port = 0
|
||||
tunnel.LastActivity = time.Now()
|
||||
@@ -116,10 +118,9 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||
}
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
@@ -128,7 +129,10 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
||||
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
|
||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
if tunnel.Port == 0 {
|
||||
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
|
||||
@@ -152,9 +156,6 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
|
||||
return err
|
||||
}
|
||||
tunnel.Credentials = credentials
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -35,6 +35,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
|
||||
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(),
|
||||
@@ -51,7 +52,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
|
||||
@@ -18,8 +18,6 @@ const (
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
)
|
||||
|
||||
@@ -15,8 +15,6 @@ const (
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
|
||||
@@ -1,41 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type portainerFormatter struct {
|
||||
logrus.TextFormatter
|
||||
}
|
||||
|
||||
func (f *portainerFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
var levelColor int
|
||||
switch entry.Level {
|
||||
case logrus.DebugLevel, logrus.TraceLevel:
|
||||
levelColor = 31 // gray
|
||||
case logrus.WarnLevel:
|
||||
levelColor = 33 // yellow
|
||||
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
|
||||
levelColor = 31 // red
|
||||
default:
|
||||
levelColor = 36 // blue
|
||||
}
|
||||
return []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m %s %s\n", levelColor, strings.ToUpper(entry.Level.String()), entry.Time.Format(f.TimestampFormat), entry.Message)), nil
|
||||
}
|
||||
|
||||
func configureLogger() {
|
||||
logger := logrus.New() // logger is to implicitly substitute stdlib's log
|
||||
log.SetOutput(logger.Writer())
|
||||
|
||||
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
|
||||
formatterLogrus := &portainerFormatter{logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true, TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true}}
|
||||
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
|
||||
|
||||
logger.SetFormatter(formatter)
|
||||
logrus.SetFormatter(formatterLogrus)
|
||||
logrus.SetFormatter(formatter)
|
||||
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
@@ -208,7 +210,7 @@ func initGitService() portainer.GitService {
|
||||
return git.NewService()
|
||||
}
|
||||
|
||||
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
||||
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
||||
slices := strings.Split(addr, ":")
|
||||
host := slices[0]
|
||||
if host == "" {
|
||||
@@ -278,6 +280,12 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
}
|
||||
|
||||
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
|
||||
settings.AgentSecret = agentKey
|
||||
} else {
|
||||
settings.AgentSecret = ""
|
||||
}
|
||||
|
||||
err = dataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -566,9 +574,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
openAMTService := openamt.NewService()
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@@ -599,9 +608,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
|
||||
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
@@ -628,6 +637,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
applicationStatus := initStatus(instanceID)
|
||||
|
||||
demoService := demo.NewService()
|
||||
if *flags.DemoEnvironment {
|
||||
err := demoService.Init(dataStore, cryptoService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing demo environment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed initializing environment: %v", err)
|
||||
@@ -706,7 +723,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
OpenAMTService: openAMTService,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
KubeConfigService: kubeConfigService,
|
||||
KubeClusterAccessService: kubeClusterAccessService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSLService: sslService,
|
||||
@@ -716,7 +733,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
BaseURL: *flags.BaseURL,
|
||||
DemoService: demoService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +744,15 @@ func main() {
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"Version": portainer.APIVersion,
|
||||
"BuildNumber": build.BuildNumber,
|
||||
"ImageTag": build.ImageTag,
|
||||
"NodejsVersion": build.NodejsVersion,
|
||||
"YarnVersion": build.YarnVersion,
|
||||
"WebpackVersion": build.WebpackVersion,
|
||||
"GoVersion": build.GoVersion},
|
||||
).Print("[INFO] [cmd,main] Starting Portainer")
|
||||
err := server.Start()
|
||||
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
||||
func Test_enableFeaturesFromFlags(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
tests := []struct {
|
||||
@@ -76,7 +76,7 @@ func Test_optionalFeature(t *testing.T) {
|
||||
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
// Enable the test feature
|
||||
|
||||
@@ -29,6 +29,7 @@ type Connection interface {
|
||||
GetNextIdentifier(bucketName string) int
|
||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
||||
CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error
|
||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
|
||||
@@ -9,11 +9,11 @@ type Service struct{}
|
||||
|
||||
// Hash hashes a string using the bcrypt algorithm
|
||||
func (*Service) Hash(data string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
|
||||
|
||||
53
api/crypto/hash_test.go
Normal file
53
api/crypto/hash_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService_Hash(t *testing.T) {
|
||||
var s = &Service{}
|
||||
|
||||
type args struct {
|
||||
hash string
|
||||
data string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
hash: "",
|
||||
data: "",
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "Matching",
|
||||
args: args{
|
||||
hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy",
|
||||
data: "Passw0rd!",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "Not matching",
|
||||
args: args{
|
||||
hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a",
|
||||
data: "Passw0rd!",
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
err := s.CompareHashAndData(tt.args.hash, tt.args.data)
|
||||
if (err != nil) == tt.expect {
|
||||
t.Errorf("Service.CompareHashAndData() = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -161,7 +161,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
|
||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
||||
}
|
||||
|
||||
b, err := connection.exportJson(databasePath)
|
||||
b, err := connection.ExportJson(databasePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,10 +181,7 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
|
||||
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -314,6 +311,19 @@ func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, ob
|
||||
})
|
||||
}
|
||||
|
||||
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
|
||||
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
data, err := connection.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateObjectWithSetSequence creates a new object in the bucket, using the specified id, and sets the bucket sequence
|
||||
// avoid this :)
|
||||
func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error {
|
||||
@@ -406,9 +416,9 @@ func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error
|
||||
}
|
||||
|
||||
err = connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.SetSequence(uint64(id))
|
||||
})
|
||||
|
||||
@@ -4,13 +4,34 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
|
||||
buckets := map[string]interface{}{}
|
||||
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
bucket = tx.Bucket([]byte(bucketName))
|
||||
seqId := bucket.Sequence()
|
||||
buckets[bucketName] = int(seqId)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return buckets, err
|
||||
}
|
||||
|
||||
// ExportJSON creates a JSON representation from a DbConnection. You can include
|
||||
// the database's metadata or ignore it. Ensure the database is closed before
|
||||
// using this function
|
||||
// inspired by github.com/konoui/boltdb-exporter (which has no license)
|
||||
// but very much simplified, based on how we use boltdb
|
||||
func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
|
||||
func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, error) {
|
||||
logrus.WithField("databasePath", databasePath).Infof("exportJson")
|
||||
|
||||
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
|
||||
@@ -20,6 +41,13 @@ func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
|
||||
defer connection.Close()
|
||||
|
||||
backup := make(map[string]interface{})
|
||||
if metadata {
|
||||
meta, err := backupMetadata(connection)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Errorf("Failed exporting metadata: %v", err)
|
||||
}
|
||||
backup["__metadata"] = meta
|
||||
}
|
||||
|
||||
err = connection.View(func(tx *bolt.Tx) error {
|
||||
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
@@ -45,15 +73,20 @@ func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
|
||||
}
|
||||
if bucketName == "version" {
|
||||
backup[bucketName] = version
|
||||
return nil
|
||||
}
|
||||
if len(list) > 0 {
|
||||
if bucketName == "ssl" ||
|
||||
bucketName == "settings" ||
|
||||
bucketName == "tunnel_server" {
|
||||
backup[bucketName] = list[0]
|
||||
backup[bucketName] = nil
|
||||
if len(list) > 0 {
|
||||
backup[bucketName] = list[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
backup[bucketName] = list
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
|
||||
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
|
||||
passphrase = "my secret key"
|
||||
)
|
||||
|
||||
|
||||
@@ -67,14 +67,14 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
|
||||
return &edgeJob, nil
|
||||
}
|
||||
|
||||
// CreateEdgeJob creates a new Edge job
|
||||
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
|
||||
return service.connection.CreateObject(
|
||||
// Create creates a new EdgeJob
|
||||
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
edgeJob.ID = ID
|
||||
|
||||
return service.connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
edgeJob.ID = portainer.EdgeJobID(id)
|
||||
return int(edgeJob.ID), edgeJob
|
||||
},
|
||||
int(edgeJob.ID),
|
||||
edgeJob,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ type (
|
||||
EdgeJobService interface {
|
||||
EdgeJobs() ([]portainer.EdgeJob, error)
|
||||
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
|
||||
Create(edgeJob *portainer.EdgeJob) error
|
||||
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
DeleteEdgeJob(ID portainer.EdgeJobID) error
|
||||
GetNextIdentifier() int
|
||||
|
||||
@@ -29,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 := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
b := stackBuilder{t: t, store: store}
|
||||
@@ -87,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 := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func Test_teamByName(t *testing.T) {
|
||||
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
_, err := store.Team().TeamByName("name")
|
||||
@@ -19,7 +19,7 @@ func Test_teamByName(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
@@ -35,7 +35,7 @@ func Test_teamByName(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
|
||||
@@ -69,6 +69,11 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// Backup current database with default options
|
||||
func (store *Store) Backup() (string, error) {
|
||||
return store.backupWithOptions(nil)
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
@@ -98,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
dbPath := store.databasePath()
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
if err := store.Close(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error closing datastore before creating backup: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
|
||||
return options.BackupPath, err
|
||||
}
|
||||
|
||||
if _, err := store.Open(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error opening datastore after creating backup: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
return options.BackupPath, nil
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateBackupFolders(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
connection := store.GetConnection()
|
||||
@@ -27,7 +27,7 @@ func TestCreateBackupFolders(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(true)
|
||||
_, store, teardown := MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
if store == nil {
|
||||
@@ -40,7 +40,7 @@ func TestStoreCreation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(true)
|
||||
_, store, teardown := MustNewTestStore(true, true)
|
||||
connection := store.GetConnection()
|
||||
defer teardown()
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestBackup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveWithOptions(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(true)
|
||||
_, store, teardown := MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("successfully removes file if existent", func(t *testing.T) {
|
||||
@@ -86,7 +86,7 @@ func TestRemoveWithOptions(t *testing.T) {
|
||||
|
||||
err = store.removeWithOptions(options)
|
||||
if err != nil {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
|
||||
}
|
||||
|
||||
if isFileExist(f.Name()) {
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
// TestStoreFull an eventually comprehensive set of tests for the Store.
|
||||
// The idea is what we write to the store, we should read back.
|
||||
func TestStoreFull(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(true)
|
||||
_, store, teardown := MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
testCases := map[string]func(t *testing.T){
|
||||
|
||||
@@ -22,23 +22,18 @@ func (store *Store) Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.checkOrCreateDefaultData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return store.checkOrCreateDefaultData()
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateInstanceID() error {
|
||||
instanceID, err := store.VersionService.InstanceID()
|
||||
_, err := store.VersionService.InstanceID()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
uid, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instanceID = uid.String()
|
||||
instanceID := uid.String()
|
||||
return store.VersionService.StoreInstanceID(instanceID)
|
||||
}
|
||||
return err
|
||||
@@ -52,6 +47,9 @@ func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
EnableTelemetry: true,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
BlackListedLabels: make([]portainer.Pair, 0),
|
||||
InternalAuthSettings: portainer.InternalAuthSettings{
|
||||
RequiredPasswordLength: 12,
|
||||
},
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
|
||||
@@ -28,10 +28,20 @@ func (slog *ScopedLog) Debug(message string) {
|
||||
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Debugf(message string, vars ...interface{}) {
|
||||
message = fmt.Sprintf(message, vars...)
|
||||
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Info(message string) {
|
||||
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Infof(message string, vars ...interface{}) {
|
||||
message = fmt.Sprintf(message, vars...)
|
||||
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
|
||||
}
|
||||
|
||||
func (slog *ScopedLog) Error(message string, err error) {
|
||||
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
plog "github.com/portainer/portainer/api/datastore/log"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -24,6 +25,12 @@ func (store *Store) MigrateData() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup Database
|
||||
backupPath, err := store.Backup()
|
||||
if err != nil {
|
||||
return werrors.Wrap(err, "while backing up db before migration")
|
||||
}
|
||||
|
||||
migratorParams := &migrator.MigratorParameters{
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
@@ -46,7 +53,27 @@ func (store *Store) MigrateData() error {
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
|
||||
return store.connectionMigrateData(migratorParams)
|
||||
// restore on error
|
||||
err = store.connectionMigrateData(migratorParams)
|
||||
if err != nil {
|
||||
logrus.Errorf("While DB migration %v. Restoring DB", err)
|
||||
// Restore options
|
||||
options := BackupOptions{
|
||||
BackupPath: backupPath,
|
||||
}
|
||||
err := store.restoreWithOptions(&options)
|
||||
if err != nil {
|
||||
logrus.Fatalf(
|
||||
"Failed restoring the backup. portainer database file needs to restored manually by "+
|
||||
"replacing %s database file with recent backup %s. Error %v",
|
||||
store.databasePath(),
|
||||
options.BackupPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
@@ -22,8 +28,32 @@ func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
snapshotTests := []struct {
|
||||
testName string
|
||||
srcPath string
|
||||
wantPath string
|
||||
}{
|
||||
{
|
||||
testName: "migrate version 24 to latest",
|
||||
srcPath: "test_data/input_24.json",
|
||||
wantPath: "test_data/output_24_to_latest.json",
|
||||
},
|
||||
}
|
||||
for _, test := range snapshotTests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath)
|
||||
if err != nil {
|
||||
t.Errorf(
|
||||
"Failed migrating mock database %v: %v",
|
||||
test.srcPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
newStore, store, teardown := MustNewTestStore(false)
|
||||
newStore, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
if !newStore {
|
||||
@@ -50,7 +80,7 @@ func TestMigrateData(t *testing.T) {
|
||||
{version: 21, expectedVersion: portainer.DBVersion},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
_, store, teardown := MustNewTestStore(true)
|
||||
_, store, teardown := MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
// Setup data
|
||||
@@ -75,7 +105,7 @@ func TestMigrateData(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
version := 17
|
||||
@@ -87,7 +117,7 @@ func TestMigrateData(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(0)
|
||||
|
||||
@@ -101,7 +131,7 @@ func TestMigrateData(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
store.VersionService.StoreIsUpdating(true)
|
||||
@@ -116,7 +146,7 @@ func TestMigrateData(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
store.MigrateData()
|
||||
@@ -131,7 +161,7 @@ func TestMigrateData(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
options := getBackupRestoreOptions(store.commonBackupDir())
|
||||
@@ -150,7 +180,7 @@ func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := 21
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
@@ -185,3 +215,250 @@ func isFileExist(path string) bool {
|
||||
}
|
||||
return len(matches) > 0
|
||||
}
|
||||
|
||||
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
||||
// parses it into a database, runs a migration on that database, and then
|
||||
// compares it with an expected output database.
|
||||
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
|
||||
srcJSON, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
|
||||
}
|
||||
|
||||
// Parse source json to db.
|
||||
_, store, teardown := MustNewTestStore(true, false)
|
||||
defer teardown()
|
||||
err = importJSON(t, bytes.NewReader(srcJSON), store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run the actual migrations on our input database.
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assert that our database connection is using bolt so we can call
|
||||
// exportJson rather than ExportRaw. The exportJson function allows us to
|
||||
// strip out the metadata which we don't want for our tests.
|
||||
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
|
||||
err = store.connection.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("err closing bolt connection: %v", err)
|
||||
}
|
||||
con, ok := store.connection.(*boltdb.DbConnection)
|
||||
if !ok {
|
||||
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
|
||||
}
|
||||
|
||||
// Convert database back to json.
|
||||
databasePath := con.GetDatabaseFilePath()
|
||||
if _, err := os.Stat(databasePath); err != nil {
|
||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
||||
}
|
||||
|
||||
gotJSON, err := con.ExportJson(databasePath, false)
|
||||
if err != nil {
|
||||
t.Logf(
|
||||
"failed re-exporting database %s to JSON: %v",
|
||||
databasePath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
wantJSON, err := os.ReadFile(wantPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
|
||||
}
|
||||
|
||||
// Compare the result we got with the one we wanted.
|
||||
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
|
||||
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
|
||||
os.WriteFile(
|
||||
gotPath,
|
||||
gotJSON,
|
||||
0600,
|
||||
)
|
||||
t.Errorf(
|
||||
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
|
||||
srcPath,
|
||||
wantPath,
|
||||
gotPath,
|
||||
diff,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// importJSON reads input JSON and commits it to a portainer datastore.Store.
|
||||
// Errors are logged with the testing package.
|
||||
func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
||||
objects := make(map[string]interface{})
|
||||
|
||||
// Parse json into map of objects.
|
||||
d := json.NewDecoder(r)
|
||||
d.UseNumber()
|
||||
err := d.Decode(&objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get database connection from store.
|
||||
con := store.connection
|
||||
|
||||
for k, v := range objects {
|
||||
switch k {
|
||||
case "version":
|
||||
versions, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed casting %s to map[string]interface{}", k)
|
||||
}
|
||||
|
||||
dbVersion, ok := versions["DB_VERSION"]
|
||||
if !ok {
|
||||
t.Logf("failed getting DB_VERSION from %s", k)
|
||||
}
|
||||
|
||||
numDBVersion, ok := dbVersion.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed parsing DB_VERSION as json number from %s", k)
|
||||
}
|
||||
|
||||
intDBVersion, err := numDBVersion.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
|
||||
}
|
||||
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DB_VERSION"),
|
||||
int(intDBVersion),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
|
||||
}
|
||||
|
||||
instanceID, ok := versions["INSTANCE_ID"]
|
||||
if !ok {
|
||||
t.Logf("failed getting INSTANCE_ID from %s", k)
|
||||
}
|
||||
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INSTANCE_ID"),
|
||||
instanceID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "dockerhub":
|
||||
obj, ok := v.([]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to cast %s to []interface{}", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DOCKERHUB"),
|
||||
obj[0],
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "ssl":
|
||||
obj, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("SSL"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing SSL in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "settings":
|
||||
obj, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("SETTINGS"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing SETTINGS in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "tunnel_server":
|
||||
obj, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INFO"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INFO in %s: %v", k, err)
|
||||
}
|
||||
case "templates":
|
||||
continue
|
||||
|
||||
default:
|
||||
objlist, ok := v.([]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to cast %s to []interface{}", k)
|
||||
}
|
||||
|
||||
for _, obj := range objlist {
|
||||
value, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Logf("failed to cast %v to map[string]interface{}", obj)
|
||||
} else {
|
||||
var ok bool
|
||||
var id interface{}
|
||||
switch k {
|
||||
case "endpoint_relations":
|
||||
// TODO: need to make into an int, then do that weird
|
||||
// stringification
|
||||
id, ok = value["EndpointID"]
|
||||
default:
|
||||
id, ok = value["Id"]
|
||||
}
|
||||
if !ok {
|
||||
// endpoint_relations: EndpointID
|
||||
t.Logf("missing Id field: %s", k)
|
||||
id = "error"
|
||||
}
|
||||
n, ok := id.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed to cast %v to json.Number in %s", id, k)
|
||||
} else {
|
||||
key, err := n.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed to cast %v to int in %s", n, k)
|
||||
} else {
|
||||
err := con.CreateObjectWithId(
|
||||
k,
|
||||
int(key),
|
||||
value,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing %v in %s: %v", key, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func setup(store *Store) error {
|
||||
}
|
||||
|
||||
func TestMigrateSettings(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
err := setup(store)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
defer teardown()
|
||||
|
||||
stackService := store.Stack()
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
dbversion int
|
||||
migrate func() error
|
||||
}
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return werrors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
func newMigration(dbversion int, migrate func() error) migration {
|
||||
return migration{
|
||||
dbversion: dbversion,
|
||||
migrate: migrate,
|
||||
}
|
||||
}
|
||||
|
||||
func dbTooOldError() error {
|
||||
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
|
||||
}
|
||||
|
||||
func GetFunctionName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -19,175 +41,96 @@ func (m *Migrator) Migrate() error {
|
||||
return migrationError(err, "StoreIsUpdating")
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 17 {
|
||||
return migrationError(err, "migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
|
||||
migrations := []migration{
|
||||
// Portainer < 1.21.0
|
||||
newMigration(17, dbTooOldError),
|
||||
|
||||
// Portainer 1.21.0
|
||||
newMigration(18, m.updateUsersToDBVersion18),
|
||||
newMigration(18, m.updateEndpointsToDBVersion18),
|
||||
newMigration(18, m.updateEndpointGroupsToDBVersion18),
|
||||
newMigration(18, m.updateRegistriesToDBVersion18),
|
||||
|
||||
// 1.22.0
|
||||
newMigration(19, m.updateSettingsToDBVersion19),
|
||||
|
||||
// 1.22.1
|
||||
newMigration(20, m.updateUsersToDBVersion20),
|
||||
newMigration(20, m.updateSettingsToDBVersion20),
|
||||
newMigration(20, m.updateSchedulesToDBVersion20),
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
newMigration(22, m.updateResourceControlsToDBVersion22),
|
||||
newMigration(22, m.updateUsersAndRolesToDBVersion22),
|
||||
|
||||
// Portainer 1.24.0
|
||||
newMigration(23, m.updateTagsToDBVersion23),
|
||||
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
|
||||
|
||||
// Portainer 1.24.1
|
||||
newMigration(24, m.updateSettingsToDB24),
|
||||
|
||||
// Portainer 2.0.0
|
||||
newMigration(25, m.updateSettingsToDB25),
|
||||
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
|
||||
|
||||
// Portainer 2.1.0
|
||||
newMigration(26, m.updateEndpointSettingsToDB25),
|
||||
|
||||
// Portainer 2.2.0
|
||||
newMigration(27, m.updateStackResourceControlToDB27),
|
||||
|
||||
// Portainer 2.6.0
|
||||
newMigration(30, m.migrateDBVersionToDB30),
|
||||
|
||||
// Portainer 2.9.0
|
||||
newMigration(32, m.migrateDBVersionToDB32),
|
||||
|
||||
// Portainer 2.9.1, 2.9.2
|
||||
newMigration(33, m.migrateDBVersionToDB33),
|
||||
|
||||
// Portainer 2.10
|
||||
newMigration(34, m.migrateDBVersionToDB34),
|
||||
|
||||
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
|
||||
newMigration(35, m.migrateDBVersionToDB35),
|
||||
|
||||
newMigration(36, m.migrateDBVersionToDB36),
|
||||
|
||||
// Portainer 2.13
|
||||
newMigration(40, m.migrateDBVersionToDB40),
|
||||
|
||||
// Portainer 2.14
|
||||
newMigration(50, m.migrateDBVersionToDB50),
|
||||
|
||||
// Portainer 2.15
|
||||
newMigration(60, m.migrateDBVersionToDB60),
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion18")
|
||||
}
|
||||
var lastDbVersion int
|
||||
for _, migration := range migrations {
|
||||
if m.currentDBVersion < migration.dbversion {
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion18")
|
||||
}
|
||||
// Print the next line only when the version changes
|
||||
if migration.dbversion > lastDbVersion {
|
||||
migrateLog.Infof("Migrating DB to version %d", migration.dbversion)
|
||||
}
|
||||
|
||||
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 := migration.migrate()
|
||||
if err != nil {
|
||||
return migrationError(err, GetFunctionName(migration.migrate))
|
||||
}
|
||||
}
|
||||
lastDbVersion = migration.dbversion
|
||||
}
|
||||
|
||||
migrateLog.Infof("Setting DB version to %d", portainer.DBVersion)
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
|
||||
migrateLog.Infof("Updated DB version to %d", portainer.DBVersion)
|
||||
|
||||
// reset DB updating status
|
||||
return m.versionService.StoreIsUpdating(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateUsersToDBVersion18() error {
|
||||
migrateLog.Info("- updating users")
|
||||
legacyUsers, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -39,6 +40,7 @@ func (m *Migrator) updateUsersToDBVersion18() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsToDBVersion18() error {
|
||||
migrateLog.Info("- updating endpoints")
|
||||
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -69,6 +71,7 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
|
||||
migrateLog.Info("- updating endpoint groups")
|
||||
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,6 +102,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRegistriesToDBVersion18() error {
|
||||
migrateLog.Info("- updating registries")
|
||||
legacyRegistries, err := m.registryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ package migrator
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion19() error {
|
||||
migrateLog.Info("- updating settings")
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
const scheduleScriptExecutionJobType = 1
|
||||
|
||||
func (m *Migrator) updateUsersToDBVersion20() error {
|
||||
migrateLog.Info("- updating user authentication")
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSchedulesToDBVersion20() error {
|
||||
migrateLog.Info("- updating schedules")
|
||||
legacySchedules, err := m.scheduleService.Schedules()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
migrateLog.Info("- updating resource controls")
|
||||
legacyResourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -24,6 +25,7 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
migrateLog.Info("- updating users and roles")
|
||||
legacyUsers, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,7 +3,7 @@ package migrator
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
migrateLog.Info("Updating tags")
|
||||
migrateLog.Info("- Updating tags")
|
||||
tags, err := m.tagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -21,7 +21,7 @@ func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
|
||||
migrateLog.Info("Updating endpoints and endpoint groups")
|
||||
migrateLog.Info("- updating endpoints and endpoint groups")
|
||||
tags, err := m.tagService.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,7 +3,7 @@ package migrator
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
migrateLog.Info("Updating Settings")
|
||||
migrateLog.Info("- updating Settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
@@ -18,7 +18,7 @@ func (m *Migrator) updateSettingsToDB24() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateStacksToDB24() error {
|
||||
migrateLog.Info("Updating stacks")
|
||||
migrateLog.Info("- updating stacks")
|
||||
stacks, err := m.stackService.Stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB25() error {
|
||||
migrateLog.Info("Updating settings")
|
||||
migrateLog.Info("- updating settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
migrateLog.Info("Updating endpoint settings")
|
||||
migrateLog.Info("- updating endpoint settings")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
migrateLog.Info("Updating stack resource controls")
|
||||
migrateLog.Info("- updating stack resource controls")
|
||||
resourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB30() error {
|
||||
migrateLog.Info("Updating legacy settings")
|
||||
migrateLog.Info("- updating legacy settings")
|
||||
if err := m.MigrateSettingsToDB30(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,38 +2,35 @@ package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"log"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -42,6 +39,7 @@ func (m *Migrator) migrateDBVersionToDB32() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRegistriesToDB32() error {
|
||||
migrateLog.Info("- updating registries")
|
||||
registries, err := m.registryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -84,6 +82,7 @@ func (m *Migrator) updateRegistriesToDB32() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateDockerhubToDB32() error {
|
||||
migrateLog.Info("- updating dockerhub")
|
||||
dockerhub, err := m.dockerhubService.DockerHub()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return nil
|
||||
@@ -172,6 +171,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
migrateLog.Info("- updating resource controls")
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environments: %w", err)
|
||||
@@ -211,14 +211,14 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
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
|
||||
}
|
||||
|
||||
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
|
||||
volumesData := snapshot.SnapshotRaw.Volumes
|
||||
if volumesData.Volumes == nil {
|
||||
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
|
||||
continue
|
||||
}
|
||||
|
||||
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
|
||||
|
||||
}
|
||||
|
||||
for _, resourceControl := range volumeResourceControls {
|
||||
@@ -241,18 +241,11 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
volumes := volumesData["Volumes"].([]interface{})
|
||||
for _, volumeMeta := range volumes {
|
||||
volume := volumeMeta.(map[string]interface{})
|
||||
volumeName, nameExist := volume["Name"].(string)
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
createTime, createTimeExist := volume["CreatedAt"].(string)
|
||||
if !createTimeExist {
|
||||
continue
|
||||
}
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.VolumeListOKBody, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
volumes := volumesData.Volumes
|
||||
for _, volume := range volumes {
|
||||
volumeName := volume.Name
|
||||
createTime := volume.CreatedAt
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
@@ -264,6 +257,7 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
}
|
||||
|
||||
func (m *Migrator) kubeconfigExpiryToDB32() error {
|
||||
migrateLog.Info("- updating kubeconfig expiry")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,6 +267,7 @@ func (m *Migrator) kubeconfigExpiryToDB32() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) helmRepositoryURLToDB32() error {
|
||||
migrateLog.Info("- setting default helm repository URL")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB33() error {
|
||||
migrateLog.Info("- updating settings")
|
||||
if err := m.migrateSettingsToDB33(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -16,7 +19,7 @@ func (m *Migrator) migrateSettingsToDB33() error {
|
||||
return err
|
||||
}
|
||||
|
||||
migrateLog.Info("Setting default kubectl shell image")
|
||||
migrateLog.Info("- setting default kubectl shell image")
|
||||
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/portainer/portainer/api/dataservices"
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
migrateLog.Info("Migrating stacks")
|
||||
migrateLog.Info("- updating stacks")
|
||||
err := MigrateStackEntryPoint(m.stackService)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,7 +3,7 @@ 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")
|
||||
migrateLog.Info("- updating dockerhub registries")
|
||||
err := m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
36
api/datastore/migrator/migrate_dbversion35.go
Normal file
36
api/datastore/migrator/migrate_dbversion35.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB36() error {
|
||||
migrateLog.Info("Updating user authorizations")
|
||||
if err := m.migrateUsersToDB36(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateUsersToDB36() error {
|
||||
users, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
currentAuthorizations := authorization.DefaultPortainerAuthorizations()
|
||||
currentAuthorizations[portainer.OperationPortainerUserListToken] = true
|
||||
currentAuthorizations[portainer.OperationPortainerUserCreateToken] = true
|
||||
currentAuthorizations[portainer.OperationPortainerUserRevokeToken] = true
|
||||
user.PortainerAuthorizations = currentAuthorizations
|
||||
err = m.userService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
31
api/datastore/migrator/migrate_dbversion40.go
Normal file
31
api/datastore/migrator/migrate_dbversion40.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB40() error {
|
||||
if err := m.trustCurrentEdgeEndpointsDB40(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) trustCurrentEdgeEndpointsDB40() error {
|
||||
migrateLog.Info("- trusting current edge endpoints")
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
endpoint.UserTrusted = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
20
api/datastore/migrator/migrate_dbversion50.go
Normal file
20
api/datastore/migrator/migrate_dbversion50.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB50() error {
|
||||
return m.migratePasswordLengthSettings()
|
||||
}
|
||||
|
||||
func (m *Migrator) migratePasswordLengthSettings() error {
|
||||
migrateLog.Info("Updating required password length")
|
||||
s, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve settings")
|
||||
}
|
||||
|
||||
s.InternalAuthSettings.RequiredPasswordLength = 12
|
||||
return m.settingsService.UpdateSettings(s)
|
||||
}
|
||||
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB60() error {
|
||||
if err := m.addGpuInputFieldDB60(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) addGpuInputFieldDB60() error {
|
||||
migrateLog.Info("- add gpu input field")
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Gpus = []portainer.Pair{}
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
2804
api/datastore/test_data/input_24.json
Normal file
2804
api/datastore/test_data/input_24.json
Normal file
File diff suppressed because it is too large
Load Diff
922
api/datastore/test_data/output_24_to_latest.json
Normal file
922
api/datastore/test_data/output_24_to_latest.json
Normal file
@@ -0,0 +1,922 @@
|
||||
{
|
||||
"dockerhub": [
|
||||
{
|
||||
"Authentication": false,
|
||||
"Username": ""
|
||||
}
|
||||
],
|
||||
"endpoint_groups": [
|
||||
{
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"Description": "Unassigned endpoints",
|
||||
"Id": 1,
|
||||
"Labels": [],
|
||||
"Name": "Unassigned",
|
||||
"TagIds": [],
|
||||
"Tags": null,
|
||||
"TeamAccessPolicies": {},
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
],
|
||||
"endpoint_relations": [
|
||||
{
|
||||
"EdgeStacks": {},
|
||||
"EndpointID": 1
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"AzureCredentials": {
|
||||
"ApplicationID": "",
|
||||
"AuthenticationKey": "",
|
||||
"TenantID": ""
|
||||
},
|
||||
"ComposeSyntaxMaxVersion": "",
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeCheckinInterval": 0,
|
||||
"EdgeKey": "",
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
"Kubernetes": {
|
||||
"Configuration": {
|
||||
"IngressClasses": null,
|
||||
"RestrictDefaultNamespace": false,
|
||||
"StorageClasses": null,
|
||||
"UseLoadBalancer": false,
|
||||
"UseServerMetrics": false
|
||||
},
|
||||
"Snapshots": null
|
||||
},
|
||||
"LastCheckInDate": 0,
|
||||
"Name": "local",
|
||||
"PublicURL": "",
|
||||
"QueryDate": 0,
|
||||
"SecuritySettings": {
|
||||
"allowBindMountsForRegularUsers": true,
|
||||
"allowContainerCapabilitiesForRegularUsers": true,
|
||||
"allowDeviceMappingForRegularUsers": true,
|
||||
"allowHostNamespaceForRegularUsers": true,
|
||||
"allowPrivilegedModeForRegularUsers": true,
|
||||
"allowStackManagementForRegularUsers": true,
|
||||
"allowSysctlSettingForRegularUsers": false,
|
||||
"allowVolumeBrowserForRegularUsers": false,
|
||||
"enableHostManagementFeatures": false
|
||||
},
|
||||
"Snapshots": [
|
||||
{
|
||||
"DockerSnapshotRaw": {
|
||||
"Containers": null,
|
||||
"Images": null,
|
||||
"Info": {
|
||||
"Architecture": "",
|
||||
"BridgeNfIp6tables": false,
|
||||
"BridgeNfIptables": false,
|
||||
"CPUSet": false,
|
||||
"CPUShares": false,
|
||||
"CgroupDriver": "",
|
||||
"ContainerdCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Containers": 0,
|
||||
"ContainersPaused": 0,
|
||||
"ContainersRunning": 0,
|
||||
"ContainersStopped": 0,
|
||||
"CpuCfsPeriod": false,
|
||||
"CpuCfsQuota": false,
|
||||
"Debug": false,
|
||||
"DefaultRuntime": "",
|
||||
"DockerRootDir": "",
|
||||
"Driver": "",
|
||||
"DriverStatus": null,
|
||||
"ExperimentalBuild": false,
|
||||
"GenericResources": null,
|
||||
"HttpProxy": "",
|
||||
"HttpsProxy": "",
|
||||
"ID": "",
|
||||
"IPv4Forwarding": false,
|
||||
"Images": 0,
|
||||
"IndexServerAddress": "",
|
||||
"InitBinary": "",
|
||||
"InitCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Isolation": "",
|
||||
"KernelMemory": false,
|
||||
"KernelMemoryTCP": false,
|
||||
"KernelVersion": "",
|
||||
"Labels": null,
|
||||
"LiveRestoreEnabled": false,
|
||||
"LoggingDriver": "",
|
||||
"MemTotal": 0,
|
||||
"MemoryLimit": false,
|
||||
"NCPU": 0,
|
||||
"NEventsListener": 0,
|
||||
"NFd": 0,
|
||||
"NGoroutines": 0,
|
||||
"Name": "",
|
||||
"NoProxy": "",
|
||||
"OSType": "",
|
||||
"OSVersion": "",
|
||||
"OomKillDisable": false,
|
||||
"OperatingSystem": "",
|
||||
"PidsLimit": false,
|
||||
"Plugins": {
|
||||
"Authorization": null,
|
||||
"Log": null,
|
||||
"Network": null,
|
||||
"Volume": null
|
||||
},
|
||||
"RegistryConfig": null,
|
||||
"RuncCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Runtimes": null,
|
||||
"SecurityOptions": null,
|
||||
"ServerVersion": "",
|
||||
"SwapLimit": false,
|
||||
"Swarm": {
|
||||
"ControlAvailable": false,
|
||||
"Error": "",
|
||||
"LocalNodeState": "",
|
||||
"NodeAddr": "",
|
||||
"NodeID": "",
|
||||
"RemoteManagers": null
|
||||
},
|
||||
"SystemTime": "",
|
||||
"Warnings": null
|
||||
},
|
||||
"Networks": null,
|
||||
"Version": {
|
||||
"ApiVersion": "",
|
||||
"Arch": "",
|
||||
"GitCommit": "",
|
||||
"GoVersion": "",
|
||||
"Os": "",
|
||||
"Platform": {
|
||||
"Name": ""
|
||||
},
|
||||
"Version": ""
|
||||
},
|
||||
"Volumes": {
|
||||
"Volumes": null,
|
||||
"Warnings": null
|
||||
}
|
||||
},
|
||||
"DockerVersion": "20.10.13",
|
||||
"GpuUseAll": false,
|
||||
"GpuUseList": null,
|
||||
"HealthyContainerCount": 0,
|
||||
"ImageCount": 9,
|
||||
"NodeCount": 0,
|
||||
"RunningContainerCount": 5,
|
||||
"ServiceCount": 0,
|
||||
"StackCount": 2,
|
||||
"StoppedContainerCount": 0,
|
||||
"Swarm": false,
|
||||
"Time": 1648610112,
|
||||
"TotalCPU": 8,
|
||||
"TotalMemory": 25098706944,
|
||||
"UnhealthyContainerCount": 0,
|
||||
"VolumeCount": 10
|
||||
}
|
||||
],
|
||||
"Status": 1,
|
||||
"TLSConfig": {
|
||||
"TLS": false,
|
||||
"TLSSkipVerify": false
|
||||
},
|
||||
"TagIds": [],
|
||||
"Tags": null,
|
||||
"TeamAccessPolicies": {},
|
||||
"Type": 1,
|
||||
"URL": "unix:///var/run/docker.sock",
|
||||
"UserAccessPolicies": {},
|
||||
"UserTrusted": false
|
||||
}
|
||||
],
|
||||
"registries": [
|
||||
{
|
||||
"Authentication": true,
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"BaseURL": "",
|
||||
"Ecr": {
|
||||
"Region": ""
|
||||
},
|
||||
"Gitlab": {
|
||||
"InstanceURL": "",
|
||||
"ProjectId": 0,
|
||||
"ProjectPath": ""
|
||||
},
|
||||
"Id": 1,
|
||||
"ManagementConfiguration": null,
|
||||
"Name": "canister.io",
|
||||
"Password": "MjWbx8A6YK7cw7",
|
||||
"Quay": {
|
||||
"OrganisationName": "",
|
||||
"UseOrganisation": false
|
||||
},
|
||||
"RegistryAccesses": {
|
||||
"1": {
|
||||
"Namespaces": [],
|
||||
"TeamAccessPolicies": {},
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
},
|
||||
"TeamAccessPolicies": {},
|
||||
"Type": 3,
|
||||
"URL": "cloud.canister.io:5000",
|
||||
"UserAccessPolicies": {},
|
||||
"Username": "prabhatkhera"
|
||||
}
|
||||
],
|
||||
"resource_control": [
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 2,
|
||||
"Public": true,
|
||||
"ResourceId": "762gbwaj8r4gcsdy8ld1u4why",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 5,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 3,
|
||||
"Public": true,
|
||||
"ResourceId": "1_alpine",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 4,
|
||||
"Public": true,
|
||||
"ResourceId": "1_redis",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 5,
|
||||
"Public": false,
|
||||
"ResourceId": "1_nginx",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [
|
||||
{
|
||||
"AccessLevel": 1,
|
||||
"TeamId": 1
|
||||
}
|
||||
],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentBrowseDelete": true,
|
||||
"DockerAgentBrowseGet": true,
|
||||
"DockerAgentBrowseList": true,
|
||||
"DockerAgentBrowsePut": true,
|
||||
"DockerAgentBrowseRename": true,
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerAgentUndefined": true,
|
||||
"DockerBuildCancel": true,
|
||||
"DockerBuildPrune": true,
|
||||
"DockerConfigCreate": true,
|
||||
"DockerConfigDelete": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerConfigUpdate": true,
|
||||
"DockerContainerArchive": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerAttach": true,
|
||||
"DockerContainerAttachWebsocket": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerCreate": true,
|
||||
"DockerContainerDelete": true,
|
||||
"DockerContainerExec": true,
|
||||
"DockerContainerExport": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerKill": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerPause": true,
|
||||
"DockerContainerPrune": true,
|
||||
"DockerContainerPutContainerArchive": true,
|
||||
"DockerContainerRename": true,
|
||||
"DockerContainerResize": true,
|
||||
"DockerContainerRestart": true,
|
||||
"DockerContainerStart": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerStop": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerContainerUnpause": true,
|
||||
"DockerContainerUpdate": true,
|
||||
"DockerContainerWait": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerExecInspect": true,
|
||||
"DockerExecResize": true,
|
||||
"DockerExecStart": true,
|
||||
"DockerImageBuild": true,
|
||||
"DockerImageCommit": true,
|
||||
"DockerImageCreate": true,
|
||||
"DockerImageDelete": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageLoad": true,
|
||||
"DockerImagePrune": true,
|
||||
"DockerImagePush": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerImageTag": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkConnect": true,
|
||||
"DockerNetworkCreate": true,
|
||||
"DockerNetworkDelete": true,
|
||||
"DockerNetworkDisconnect": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNetworkPrune": true,
|
||||
"DockerNodeDelete": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerNodeUpdate": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginCreate": true,
|
||||
"DockerPluginDelete": true,
|
||||
"DockerPluginDisable": true,
|
||||
"DockerPluginEnable": true,
|
||||
"DockerPluginInspect": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerPluginPrivileges": true,
|
||||
"DockerPluginPull": true,
|
||||
"DockerPluginPush": true,
|
||||
"DockerPluginSet": true,
|
||||
"DockerPluginUpgrade": true,
|
||||
"DockerSecretCreate": true,
|
||||
"DockerSecretDelete": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerSecretUpdate": true,
|
||||
"DockerServiceCreate": true,
|
||||
"DockerServiceDelete": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerServiceUpdate": true,
|
||||
"DockerSessionStart": true,
|
||||
"DockerSwarmInit": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSwarmJoin": true,
|
||||
"DockerSwarmLeave": true,
|
||||
"DockerSwarmUnlock": true,
|
||||
"DockerSwarmUnlockKey": true,
|
||||
"DockerSwarmUpdate": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerUndefined": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeCreate": true,
|
||||
"DockerVolumeDelete": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"DockerVolumePrune": true,
|
||||
"EndpointResourcesAccess": true,
|
||||
"IntegrationStoridgeAdmin": true,
|
||||
"PortainerResourceControlCreate": true,
|
||||
"PortainerResourceControlUpdate": true,
|
||||
"PortainerStackCreate": true,
|
||||
"PortainerStackDelete": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerStackMigrate": true,
|
||||
"PortainerStackUpdate": true,
|
||||
"PortainerWebhookCreate": true,
|
||||
"PortainerWebhookDelete": true,
|
||||
"PortainerWebhookList": true,
|
||||
"PortainerWebsocketExec": true
|
||||
},
|
||||
"Description": "Full control of all resources in an endpoint",
|
||||
"Id": 1,
|
||||
"Name": "Endpoint administrator",
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"EndpointResourcesAccess": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerWebhookList": true
|
||||
},
|
||||
"Description": "Read-only access of all resources in an endpoint",
|
||||
"Id": 2,
|
||||
"Name": "Helpdesk",
|
||||
"Priority": 2
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerAgentUndefined": true,
|
||||
"DockerBuildCancel": true,
|
||||
"DockerBuildPrune": true,
|
||||
"DockerConfigCreate": true,
|
||||
"DockerConfigDelete": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerConfigUpdate": true,
|
||||
"DockerContainerArchive": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerAttach": true,
|
||||
"DockerContainerAttachWebsocket": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerCreate": true,
|
||||
"DockerContainerDelete": true,
|
||||
"DockerContainerExec": true,
|
||||
"DockerContainerExport": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerKill": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerPause": true,
|
||||
"DockerContainerPutContainerArchive": true,
|
||||
"DockerContainerRename": true,
|
||||
"DockerContainerResize": true,
|
||||
"DockerContainerRestart": true,
|
||||
"DockerContainerStart": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerStop": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerContainerUnpause": true,
|
||||
"DockerContainerUpdate": true,
|
||||
"DockerContainerWait": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerExecInspect": true,
|
||||
"DockerExecResize": true,
|
||||
"DockerExecStart": true,
|
||||
"DockerImageBuild": true,
|
||||
"DockerImageCommit": true,
|
||||
"DockerImageCreate": true,
|
||||
"DockerImageDelete": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageLoad": true,
|
||||
"DockerImagePush": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerImageTag": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkConnect": true,
|
||||
"DockerNetworkCreate": true,
|
||||
"DockerNetworkDelete": true,
|
||||
"DockerNetworkDisconnect": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeDelete": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerNodeUpdate": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginCreate": true,
|
||||
"DockerPluginDelete": true,
|
||||
"DockerPluginDisable": true,
|
||||
"DockerPluginEnable": true,
|
||||
"DockerPluginInspect": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerPluginPrivileges": true,
|
||||
"DockerPluginPull": true,
|
||||
"DockerPluginPush": true,
|
||||
"DockerPluginSet": true,
|
||||
"DockerPluginUpgrade": true,
|
||||
"DockerSecretCreate": true,
|
||||
"DockerSecretDelete": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerSecretUpdate": true,
|
||||
"DockerServiceCreate": true,
|
||||
"DockerServiceDelete": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerServiceUpdate": true,
|
||||
"DockerSessionStart": true,
|
||||
"DockerSwarmInit": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSwarmJoin": true,
|
||||
"DockerSwarmLeave": true,
|
||||
"DockerSwarmUnlock": true,
|
||||
"DockerSwarmUnlockKey": true,
|
||||
"DockerSwarmUpdate": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerUndefined": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeCreate": true,
|
||||
"DockerVolumeDelete": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"PortainerResourceControlUpdate": true,
|
||||
"PortainerStackCreate": true,
|
||||
"PortainerStackDelete": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerStackMigrate": true,
|
||||
"PortainerStackUpdate": true,
|
||||
"PortainerWebhookCreate": true,
|
||||
"PortainerWebhookList": true,
|
||||
"PortainerWebsocketExec": true
|
||||
},
|
||||
"Description": "Full control of assigned resources in an endpoint",
|
||||
"Id": 3,
|
||||
"Name": "Standard user",
|
||||
"Priority": 3
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerWebhookList": true
|
||||
},
|
||||
"Description": "Read-only access of assigned resources in an endpoint",
|
||||
"Id": 4,
|
||||
"Name": "Read-only user",
|
||||
"Priority": 4
|
||||
}
|
||||
],
|
||||
"schedules": [
|
||||
{
|
||||
"Created": 1648608136,
|
||||
"CronExpression": "@every 5m",
|
||||
"EdgeSchedule": null,
|
||||
"EndpointSyncJob": null,
|
||||
"Id": 1,
|
||||
"JobType": 2,
|
||||
"Name": "system_snapshot",
|
||||
"Recurring": true,
|
||||
"ScriptExecutionJob": null,
|
||||
"SnapshotJob": {}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"AgentSecret": "",
|
||||
"AllowBindMountsForRegularUsers": true,
|
||||
"AllowContainerCapabilitiesForRegularUsers": true,
|
||||
"AllowDeviceMappingForRegularUsers": true,
|
||||
"AllowHostNamespaceForRegularUsers": true,
|
||||
"AllowPrivilegedModeForRegularUsers": true,
|
||||
"AllowStackManagementForRegularUsers": true,
|
||||
"AllowVolumeBrowserForRegularUsers": false,
|
||||
"AuthenticationMethod": 1,
|
||||
"BlackListedLabels": [],
|
||||
"DisplayDonationHeader": false,
|
||||
"DisplayExternalContributors": false,
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeAgentCheckinInterval": 5,
|
||||
"EdgePortainerUrl": "",
|
||||
"EnableEdgeComputeFeatures": false,
|
||||
"EnableHostManagementFeatures": false,
|
||||
"EnableTelemetry": true,
|
||||
"EnforceEdgeID": false,
|
||||
"FeatureFlagSettings": null,
|
||||
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
||||
"InternalAuthSettings": {
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
"GroupSearchSettings": [
|
||||
{
|
||||
"GroupAttribute": "",
|
||||
"GroupBaseDN": "",
|
||||
"GroupFilter": ""
|
||||
}
|
||||
],
|
||||
"ReaderDN": "",
|
||||
"SearchSettings": [
|
||||
{
|
||||
"BaseDN": "",
|
||||
"Filter": "",
|
||||
"UserNameAttribute": ""
|
||||
}
|
||||
],
|
||||
"StartTLS": false,
|
||||
"TLSConfig": {
|
||||
"TLS": false,
|
||||
"TLSSkipVerify": false
|
||||
},
|
||||
"URL": ""
|
||||
},
|
||||
"LogoURL": "",
|
||||
"OAuthSettings": {
|
||||
"AccessTokenURI": "",
|
||||
"AuthorizationURI": "",
|
||||
"ClientID": "",
|
||||
"DefaultTeamID": 0,
|
||||
"KubeSecretKey": null,
|
||||
"LogoutURI": "",
|
||||
"OAuthAutoCreateUsers": false,
|
||||
"RedirectURI": "",
|
||||
"ResourceURI": "",
|
||||
"SSO": false,
|
||||
"Scopes": "",
|
||||
"UserIdentifier": ""
|
||||
},
|
||||
"SnapshotInterval": "5m",
|
||||
"TemplatesURL": "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json",
|
||||
"TrustOnFirstConnect": false,
|
||||
"UserSessionTimeout": "8h",
|
||||
"fdoConfiguration": {
|
||||
"enabled": false,
|
||||
"ownerPassword": "",
|
||||
"ownerURL": "",
|
||||
"ownerUsername": ""
|
||||
},
|
||||
"openAMTConfiguration": {
|
||||
"certFileContent": "",
|
||||
"certFileName": "",
|
||||
"certFilePassword": "",
|
||||
"domainName": "",
|
||||
"enabled": false,
|
||||
"mpsPassword": "",
|
||||
"mpsServer": "",
|
||||
"mpsToken": "",
|
||||
"mpsUser": ""
|
||||
}
|
||||
},
|
||||
"ssl": {
|
||||
"certPath": "",
|
||||
"httpEnabled": true,
|
||||
"keyPath": "",
|
||||
"selfSigned": false
|
||||
},
|
||||
"stacks": [
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker/alpine37-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 2,
|
||||
"IsComposeFormat": false,
|
||||
"Name": "alpine",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "s3fd604zdba7z13tbq2x6lyue",
|
||||
"Type": 1,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
},
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 5,
|
||||
"IsComposeFormat": false,
|
||||
"Name": "redis",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "",
|
||||
"Type": 2,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
},
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 6,
|
||||
"IsComposeFormat": false,
|
||||
"Name": "nginx",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "",
|
||||
"Type": 2,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
}
|
||||
],
|
||||
"teams": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "hello"
|
||||
}
|
||||
],
|
||||
"tunnel_server": {
|
||||
"PrivateKeySeed": "IvX6ZPRuWtLS5zyg"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"EndpointAuthorizations": null,
|
||||
"Id": 1,
|
||||
"Password": "$2a$10$siRDprr/5uUFAU8iom3Sr./WXQkN2dhSNjAC471pkJaALkghS762a",
|
||||
"PortainerAuthorizations": {
|
||||
"PortainerDockerHubInspect": true,
|
||||
"PortainerEndpointGroupList": true,
|
||||
"PortainerEndpointInspect": true,
|
||||
"PortainerEndpointList": true,
|
||||
"PortainerMOTD": true,
|
||||
"PortainerRegistryInspect": true,
|
||||
"PortainerRegistryList": true,
|
||||
"PortainerTeamList": true,
|
||||
"PortainerTemplateInspect": true,
|
||||
"PortainerTemplateList": true,
|
||||
"PortainerUserCreateToken": true,
|
||||
"PortainerUserInspect": true,
|
||||
"PortainerUserList": true,
|
||||
"PortainerUserListToken": true,
|
||||
"PortainerUserMemberships": true,
|
||||
"PortainerUserRevokeToken": true
|
||||
},
|
||||
"Role": 1,
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
"Username": "admin"
|
||||
},
|
||||
{
|
||||
"EndpointAuthorizations": null,
|
||||
"Id": 2,
|
||||
"Password": "$2a$10$WpCAW8mSt6FRRp1GkynbFOGSZnHR6E5j9cETZ8HiMlw06hVlDW/Li",
|
||||
"PortainerAuthorizations": {
|
||||
"PortainerDockerHubInspect": true,
|
||||
"PortainerEndpointGroupList": true,
|
||||
"PortainerEndpointInspect": true,
|
||||
"PortainerEndpointList": true,
|
||||
"PortainerMOTD": true,
|
||||
"PortainerRegistryInspect": true,
|
||||
"PortainerRegistryList": true,
|
||||
"PortainerTeamList": true,
|
||||
"PortainerTemplateInspect": true,
|
||||
"PortainerTemplateList": true,
|
||||
"PortainerUserCreateToken": true,
|
||||
"PortainerUserInspect": true,
|
||||
"PortainerUserList": true,
|
||||
"PortainerUserListToken": true,
|
||||
"PortainerUserMemberships": true,
|
||||
"PortainerUserRevokeToken": true
|
||||
},
|
||||
"Role": 1,
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
"Username": "prabhat"
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"DB_UPDATING": "false",
|
||||
"DB_VERSION": "60",
|
||||
"INSTANCE_ID": "null"
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ func (store *Store) GetConnection() portainer.Connection {
|
||||
return store.connection
|
||||
}
|
||||
|
||||
func MustNewTestStore(init bool) (bool, *Store, func()) {
|
||||
newStore, store, teardown, err := NewTestStore(init)
|
||||
func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
|
||||
newStore, store, teardown, err := NewTestStore(init, secure)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errTempDir) {
|
||||
teardown()
|
||||
@@ -30,7 +30,7 @@ func MustNewTestStore(init bool) (bool, *Store, func()) {
|
||||
return newStore, store, teardown
|
||||
}
|
||||
|
||||
func NewTestStore(init bool) (bool, *Store, func(), error) {
|
||||
func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
|
||||
// Creates unique temp directory in a concurrency friendly manner.
|
||||
storePath, err := ioutil.TempDir("", "test-store")
|
||||
if err != nil {
|
||||
@@ -42,7 +42,12 @@ func NewTestStore(init bool) (bool, *Store, func(), error) {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
connection, err := database.NewDatabase("boltdb", storePath, []byte("apassphrasewhichneedstobe32bytes"))
|
||||
secretKey := []byte("apassphrasewhichneedstobe32bytes")
|
||||
if !secure {
|
||||
secretKey = nil
|
||||
}
|
||||
|
||||
connection, err := database.NewDatabase("boltdb", storePath, secretKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
15
api/datastore/validate/validate.go
Normal file
15
api/datastore/validate/validate.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
func ValidateLDAPSettings(ldp *portainer.LDAPSettings) error {
|
||||
validate = validator.New()
|
||||
registerValidationMethods(validate)
|
||||
|
||||
return validate.Struct(ldp)
|
||||
}
|
||||
61
api/datastore/validate/validate_test.go
Normal file
61
api/datastore/validate/validate_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func TestValidateLDAPSettings(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ldap portainer.LDAPSettings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Empty LDAP Settings",
|
||||
ldap: portainer.LDAPSettings{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "With URL",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
URL: "192.168.0.1:323",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Validate URL and URLs",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
URL: "192.168.0.1:323",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "validate client ldap",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: false,
|
||||
ReaderDN: "CN=LDAP API Service Account",
|
||||
Password: "Qu**dfUUU**",
|
||||
URL: "aukdc15.pgc.co:389",
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateLDAPSettings(&tt.ldap)
|
||||
if (err == nil) == tt.wantErr {
|
||||
t.Errorf("No error expected but got %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
api/datastore/validate/validationMethods.go
Normal file
17
api/datastore/validate/validationMethods.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
func registerValidationMethods(v *validator.Validate) {
|
||||
v.RegisterValidation("validate_bool", ValidateBool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation methods below are being used for custom validation
|
||||
*/
|
||||
func ValidateBool(fl validator.FieldLevel) bool {
|
||||
_, ok := fl.Field().Interface().(bool)
|
||||
return ok
|
||||
}
|
||||
118
api/demo/demo.go
Normal file
118
api/demo/demo.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type EnvironmentDetails struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Users []portainer.UserID `json:"users"`
|
||||
Environments []portainer.EndpointID `json:"environments"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
details EnvironmentDetails
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (service *Service) Details() EnvironmentDetails {
|
||||
return service.details
|
||||
}
|
||||
|
||||
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
|
||||
log.Print("[INFO] [main] Starting demo environment")
|
||||
|
||||
isClean, err := isCleanStore(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed checking if store is clean")
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return errors.New(" Demo environment can only be initialized on a clean database")
|
||||
}
|
||||
|
||||
id, err := initDemoUser(store, cryptoService)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo user")
|
||||
}
|
||||
|
||||
endpointIds, err := initDemoEndpoints(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo endpoint")
|
||||
}
|
||||
|
||||
err = initDemoSettings(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed updating demo settings")
|
||||
}
|
||||
|
||||
service.details = EnvironmentDetails{
|
||||
Enabled: true,
|
||||
Users: []portainer.UserID{id},
|
||||
// endpoints 2,3 are created after deployment of portainer
|
||||
Environments: endpointIds,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCleanStore(store dataservices.DataStore) (bool, error) {
|
||||
endpoints, err := store.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
users, err := store.User().Users()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (service *Service) IsDemo() bool {
|
||||
return service.details.Enabled
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoEndpointID := range service.details.Environments {
|
||||
if environmentID == demoEndpointID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoUserID := range service.details.Users {
|
||||
if userID == demoUserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
79
api/demo/init.go
Normal file
79
api/demo/init.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package demo
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func initDemoUser(
|
||||
store dataservices.DataStore,
|
||||
cryptoService portainer.CryptoService,
|
||||
) (portainer.UserID, error) {
|
||||
|
||||
password, err := cryptoService.Hash("tryportainer")
|
||||
if err != nil {
|
||||
return 0, errors.WithMessage(err, "failed creating password hash")
|
||||
}
|
||||
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
err = store.User().Create(admin)
|
||||
return admin.ID, errors.WithMessage(err, "failed creating user")
|
||||
}
|
||||
|
||||
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
|
||||
localEndpointId, err := initDemoLocalEndpoint(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
|
||||
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
|
||||
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
|
||||
}
|
||||
|
||||
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
|
||||
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
|
||||
localEndpoint := &portainer.Endpoint{
|
||||
ID: id,
|
||||
Name: "local",
|
||||
URL: "unix:///var/run/docker.sock",
|
||||
PublicURL: "demo.portainer.io",
|
||||
Type: portainer.DockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(localEndpoint)
|
||||
return id, errors.WithMessage(err, "failed creating local endpoint")
|
||||
}
|
||||
|
||||
func initDemoSettings(
|
||||
store dataservices.DataStore,
|
||||
) error {
|
||||
settings, err := store.Settings().Settings()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed fetching settings")
|
||||
}
|
||||
|
||||
settings.EnableTelemetry = false
|
||||
settings.LogoURL = ""
|
||||
|
||||
err = store.Settings().UpdateSettings(settings)
|
||||
return errors.WithMessage(err, "failed updating settings")
|
||||
}
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||
@@ -154,11 +155,35 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
healthyContainers := 0
|
||||
unhealthyContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
gpuUseSet := make(map[string]struct{})
|
||||
gpuUseAll := false
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" {
|
||||
stoppedContainers++
|
||||
} else if container.State == "running" {
|
||||
runningContainers++
|
||||
|
||||
// snapshot GPUs
|
||||
response, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gpuOptions *_container.DeviceRequest = nil
|
||||
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
|
||||
gpuOptions = &deviceRequest
|
||||
}
|
||||
}
|
||||
|
||||
if gpuOptions != nil {
|
||||
if gpuOptions.Count == -1 {
|
||||
gpuUseAll = true
|
||||
}
|
||||
for _, id := range gpuOptions.DeviceIDs {
|
||||
gpuUseSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(container.Status, "(healthy)") {
|
||||
@@ -174,6 +199,14 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
}
|
||||
}
|
||||
|
||||
gpuUseList := make([]string, 0, len(gpuUseSet))
|
||||
for gpuUse := range gpuUseSet {
|
||||
gpuUseList = append(gpuUseList, gpuUse)
|
||||
}
|
||||
|
||||
snapshot.GpuUseAll = gpuUseAll
|
||||
snapshot.GpuUseList = gpuUseList
|
||||
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.HealthyContainerCount = healthyContainers
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
@@ -56,13 +54,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
|
||||
@@ -76,12 +74,14 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
if err := updateNetworkEnvFile(stack); err != nil {
|
||||
return err
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
@@ -103,200 +103,42 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
|
||||
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
|
||||
}
|
||||
|
||||
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
|
||||
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
|
||||
func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
// workaround for EE-1862. It will have to be removed when
|
||||
// docker/compose upgraded to v2.x.
|
||||
if err := createNetworkEnvFile(stack); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create network env file")
|
||||
}
|
||||
|
||||
if stack.Env == nil || len(stack.Env) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
envFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
|
||||
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer envfile.Close()
|
||||
|
||||
copyDefaultEnvFile(stack, envfile)
|
||||
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
envfile.Close()
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
func fileNotExist(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func updateNetworkEnvFile(stack *portainer.Stack) error {
|
||||
envFilePath := path.Join(stack.ProjectPath, ".env")
|
||||
stackFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
if fileNotExist(envFilePath) {
|
||||
if fileNotExist(stackFilePath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := os.O_WRONLY | os.O_SYNC | os.O_CREATE
|
||||
envFile, err := os.OpenFile(envFilePath, flags, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer envFile.Close()
|
||||
|
||||
stackFile, err := os.Open(stackFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stackFile.Close()
|
||||
|
||||
_, err = io.Copy(envFile, stackFile)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNetworkEnvFile(stack *portainer.Stack) error {
|
||||
networkNameSet := NewStringSet()
|
||||
|
||||
for _, filePath := range stackutils.GetStackFilePaths(stack) {
|
||||
networkNames, err := extractNetworkNames(filePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to extract network name")
|
||||
}
|
||||
|
||||
if networkNames == nil || networkNames.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
networkNameSet.Union(networkNames)
|
||||
}
|
||||
|
||||
for _, s := range networkNameSet.List() {
|
||||
if _, ok := os.LookupEnv(s); ok {
|
||||
networkNameSet.Remove(s)
|
||||
}
|
||||
}
|
||||
|
||||
if networkNameSet.Len() == 0 && stack.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
envfile, err := os.OpenFile(path.Join(stack.ProjectPath, ".env"),
|
||||
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
|
||||
func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) {
|
||||
defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open env file")
|
||||
// If cannot open a default file, then don't need to copy it.
|
||||
// We could as well stat it and check if it exists, but this is more efficient.
|
||||
return
|
||||
}
|
||||
|
||||
defer envfile.Close()
|
||||
defer defaultEnvFile.Close()
|
||||
|
||||
var scanEnvSettingFunc = func(name string) (string, bool) {
|
||||
if stack.Env != nil {
|
||||
for _, v := range stack.Env {
|
||||
if name == v.Name {
|
||||
return v.Value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
if _, err = io.Copy(w, defaultEnvFile); err == nil {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
|
||||
for _, s := range networkNameSet.List() {
|
||||
if _, ok := scanEnvSettingFunc(s); !ok {
|
||||
stack.Env = append(stack.Env, portainer.Pair{
|
||||
Name: s,
|
||||
Value: "None",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if stack.Env != nil {
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(
|
||||
fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractNetworkNames(filePath string) (StringSet, error) {
|
||||
if info, err := os.Stat(filePath); errors.Is(err,
|
||||
os.ErrNotExist) || info.IsDir() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
stackFileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open yaml file")
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
// invalid stack file
|
||||
return nil, errors.Wrap(err, "invalid stack file")
|
||||
}
|
||||
|
||||
var version string
|
||||
if _, ok := config["version"]; ok {
|
||||
version, _ = config["version"].(string)
|
||||
}
|
||||
|
||||
var networks map[string]interface{}
|
||||
if value, ok := config["networks"]; ok {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if networks, ok = value.(map[string]interface{}); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
networkContent, err := loader.LoadNetworks(networks, version)
|
||||
if err != nil {
|
||||
return nil, nil // skip the error
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^\$\{?([^\}]+)\}?$`)
|
||||
networkNames := NewStringSet()
|
||||
|
||||
for _, v := range networkContent {
|
||||
matched := re.FindAllStringSubmatch(v.Name, -1)
|
||||
if matched != nil && matched[0] != nil {
|
||||
if strings.Contains(matched[0][1], ":-") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(matched[0][1], "?") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(matched[0][1], "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
networkNames.Add(matched[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
if networkNames.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return networkNames, nil
|
||||
// If couldn't copy the .env file, then ignore the error and try to continue
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
const composeFile = `version: "3.9"
|
||||
@@ -41,6 +42,8 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||
|
||||
func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
testhelpers.IntegrationTest(t)
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w, err := NewComposeStackManager("", "", nil)
|
||||
|
||||
@@ -65,56 +65,22 @@ func Test_createEnvFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createNetworkEnvFile(t *testing.T) {
|
||||
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
buf := []byte(`
|
||||
version: '3.6'
|
||||
services:
|
||||
nginx-example:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
default:
|
||||
name: ${test}
|
||||
driver: bridge
|
||||
`)
|
||||
if err := ioutil.WriteFile(path.Join(dir,
|
||||
"docker-compose.yml"), buf, 0644); err != nil {
|
||||
t.Fatalf("Failed to create yaml file: %s", err)
|
||||
}
|
||||
|
||||
stackWithoutEnv := &portainer.Stack{
|
||||
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
|
||||
stack := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: "docker-compose.yml",
|
||||
Env: []portainer.Pair{},
|
||||
}
|
||||
|
||||
if err := createNetworkEnvFile(stackWithoutEnv); err != nil {
|
||||
t.Fatalf("Failed to create network env file: %s", err)
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(dir, ".env"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read network env file: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test=None\n", string(content))
|
||||
|
||||
stackWithEnv := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: "docker-compose.yml",
|
||||
Env: []portainer.Pair{
|
||||
{Name: "test", Value: "test-value"},
|
||||
{Name: "VAR1", Value: "NEW_VAL1"},
|
||||
{Name: "VAR3", Value: "VAL3"},
|
||||
},
|
||||
}
|
||||
result, err := createEnvFile(stack)
|
||||
assert.Equal(t, "stack.env", result)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, path.Join(dir, "stack.env"))
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
content, _ := ioutil.ReadAll(f)
|
||||
|
||||
if err := createNetworkEnvFile(stackWithEnv); err != nil {
|
||||
t.Fatalf("Failed to create network env file: %s", err)
|
||||
}
|
||||
|
||||
content, err = ioutil.ReadFile(path.Join(dir, ".env"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read network env file: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test=test-value\n", string(content))
|
||||
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package exec
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -123,6 +124,8 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
output, err := cmd.Output()
|
||||
|
||||
@@ -56,10 +56,12 @@ const (
|
||||
TempPath = "tmp"
|
||||
// SSLCertPath represents the default ssl certificates path
|
||||
SSLCertPath = "certs"
|
||||
// DefaultSSLCertFilename represents the default ssl certificate file name
|
||||
DefaultSSLCertFilename = "cert.pem"
|
||||
// DefaultSSLKeyFilename represents the default ssl key file name
|
||||
DefaultSSLKeyFilename = "key.pem"
|
||||
// SSLCertFilename represents the ssl certificate file name
|
||||
SSLCertFilename = "cert.pem"
|
||||
// SSLKeyFilename represents the ssl key file name
|
||||
SSLKeyFilename = "key.pem"
|
||||
// SSLCACertFilename represents the CA ssl certificate file name for mTLS
|
||||
SSLCACertFilename = "ca-cert.pem"
|
||||
)
|
||||
|
||||
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
|
||||
@@ -161,7 +163,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.New("File doesn't exist")
|
||||
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
|
||||
}
|
||||
|
||||
finput, err := os.Open(fromFilePath)
|
||||
@@ -580,8 +582,8 @@ func (service *Service) wrapFileStore(filepath string) string {
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
|
||||
certPath := JoinPaths(SSLCertPath, SSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, SSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
@@ -627,6 +629,18 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
|
||||
return defCertPath, defKeyPath, nil
|
||||
}
|
||||
|
||||
// CopySSLCACert copies the specified caCert pem file
|
||||
func (service *Service) CopySSLCACert(caCertPath string) (string, error) {
|
||||
toFilePath := service.wrapFileStore(JoinPaths(SSLCertPath, SSLCACertFilename))
|
||||
|
||||
err := service.Copy(caCertPath, toFilePath, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return toFilePath, nil
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
func FileExists(filePath string) (bool, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WriteToFile creates a file in the filesystem storage
|
||||
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)
|
||||
|
||||
@@ -108,12 +108,12 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
|
||||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
refsUrl, err := a.buildRefsUrl(config, options.referenceName)
|
||||
rootItemUrl, err := a.buildRootItemUrl(config, options.referenceName)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build azure refs url")
|
||||
return "", errors.WithMessage(err, "failed to build azure root item url")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", refsUrl, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", rootItemUrl, nil)
|
||||
if options.username != "" || options.password != "" {
|
||||
req.SetBasicAuth(options.username, options.password)
|
||||
} else if config.username != "" || config.password != "" {
|
||||
@@ -131,26 +131,24 @@ func (a *azureDownloader) latestCommitID(ctx context.Context, options fetchOptio
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to get repository refs with a status \"%v\"", resp.Status)
|
||||
return "", fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status)
|
||||
}
|
||||
|
||||
var refs struct {
|
||||
var items struct {
|
||||
Value []struct {
|
||||
Name string `json:"name"`
|
||||
ObjectId string `json:"objectId"`
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&refs); err != nil {
|
||||
return "", errors.Wrap(err, "could not parse Azure Refs response")
|
||||
}
|
||||
|
||||
for _, ref := range refs.Value {
|
||||
if strings.EqualFold(ref.Name, options.referenceName) {
|
||||
return ref.ObjectId, nil
|
||||
CommitId string `json:"commitId"`
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.Errorf("could not find ref %q in the repository", options.referenceName)
|
||||
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
|
||||
return "", errors.Wrap(err, "could not parse Azure items response")
|
||||
}
|
||||
|
||||
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
|
||||
return "", errors.Errorf("failed to get latest commitID in the repository")
|
||||
}
|
||||
|
||||
return items.Value[0].CommitId, nil
|
||||
}
|
||||
|
||||
func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||
@@ -236,8 +234,10 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
|
||||
// scopePath=/&download=true&versionDescriptor.version=main&$format=zip&recursionLevel=full&api-version=6.0
|
||||
q.Set("scopePath", "/")
|
||||
q.Set("download", "true")
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
if referenceName != "" {
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
}
|
||||
q.Set("$format", "zip")
|
||||
q.Set("recursionLevel", "full")
|
||||
q.Set("api-version", "6.0")
|
||||
@@ -246,8 +246,8 @@ func (a *azureDownloader) buildDownloadUrl(config *azureOptions, referenceName s
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/refs",
|
||||
func (a *azureDownloader) buildRootItemUrl(config *azureOptions, referenceName string) (string, error) {
|
||||
rawUrl := fmt.Sprintf("%s/%s/%s/_apis/git/repositories/%s/items",
|
||||
a.baseUrl,
|
||||
url.PathEscape(config.organisation),
|
||||
url.PathEscape(config.project),
|
||||
@@ -255,12 +255,15 @@ func (a *azureDownloader) buildRefsUrl(config *azureOptions, referenceName strin
|
||||
u, err := url.Parse(rawUrl)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse refs url path %s", rawUrl)
|
||||
return "", errors.Wrapf(err, "failed to parse root item url path %s", rawUrl)
|
||||
}
|
||||
|
||||
// filterContains=main&api-version=6.0
|
||||
q := u.Query()
|
||||
q.Set("filterContains", formatReferenceName(referenceName))
|
||||
q.Set("scopePath", "/")
|
||||
if referenceName != "" {
|
||||
q.Set("versionDescriptor.versionType", getVersionType(referenceName))
|
||||
q.Set("versionDescriptor.version", formatReferenceName(referenceName))
|
||||
}
|
||||
q.Set("api-version", "6.0")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@ func Test_buildDownloadUrl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_buildRefsUrl(t *testing.T) {
|
||||
func Test_buildRootItemUrl(t *testing.T) {
|
||||
a := NewAzureDownloader(nil)
|
||||
u, err := a.buildRefsUrl(&azureOptions{
|
||||
u, err := a.buildRootItemUrl(&azureOptions{
|
||||
organisation: "organisation",
|
||||
project: "project",
|
||||
repository: "repository",
|
||||
}, "refs/heads/main")
|
||||
|
||||
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/refs?filterContains=main&api-version=6.0")
|
||||
expectedUrl, _ := url.Parse("https://dev.azure.com/organisation/project/_apis/git/repositories/repository/items?scopePath=/&api-version=6.0&versionDescriptor.version=main&versionDescriptor.versionType=branch")
|
||||
actualUrl, _ := url.Parse(u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedUrl.Host, actualUrl.Host)
|
||||
@@ -270,63 +270,17 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := `{
|
||||
"value": [
|
||||
{
|
||||
"name": "refs/heads/feature/calcApp",
|
||||
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2FcalcApp"
|
||||
},
|
||||
{
|
||||
"name": "refs/heads/feature/replacer",
|
||||
"objectId": "917131a709996c5cfe188c3b57e9a6ad90e8b85c",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Ffeature%2Freplacer"
|
||||
},
|
||||
{
|
||||
"name": "refs/heads/master",
|
||||
"objectId": "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
"creator": {
|
||||
"displayName": "Normal Paulk",
|
||||
"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"_links": {
|
||||
"avatar": {
|
||||
"href": "https://dev.azure.com/fabrikam/_apis/GraphProfile/MemberAvatars/aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
}
|
||||
},
|
||||
"id": "ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"uniqueName": "dev@mailserver.com",
|
||||
"imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=ac5aaba6-a66a-4e1d-b508-b060ec624fa9",
|
||||
"descriptor": "aad.YmFjMGYyZDctNDA3ZC03OGRhLTlhMjUtNmJhZjUwMWFjY2U5"
|
||||
},
|
||||
"url": "https://dev.azure.com/fabrikam/7484f783-66a3-4f27-b7cd-6b08b0b077ed/_apis/git/repositories/d3d1760b-311c-4175-a726-20dfc6a7f885/refs?filter=heads%2Fmaster"
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
"count": 1,
|
||||
"value": [
|
||||
{
|
||||
"objectId": "1a5630f017127db7de24d8771da0f536ff98fc9b",
|
||||
"gitObjectType": "tree",
|
||||
"commitId": "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
"path": "/",
|
||||
"isFolder": true,
|
||||
"url": "https://dev.azure.com/simonmeng0474/4b546a97-c481-4506-bdd5-976e9592f91a/_apis/git/repositories/a22247ad-053f-43bc-88a7-62ff4846bb97/items?path=%2F&versionType=Branch&versionOptions=None"
|
||||
}
|
||||
]
|
||||
}`
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(response))
|
||||
@@ -347,19 +301,11 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOptions{
|
||||
referenceName: "refs/heads/master",
|
||||
referenceName: "",
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
|
||||
want: "ffe9cba521f00d7f60e322845072238635edb451",
|
||||
want: "27104ad7549d9e66685e115a497533f18024be9c",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should be able to parse response",
|
||||
args: fetchOptions{
|
||||
referenceName: "refs/heads/unknown",
|
||||
repositoryUrl: "https://dev.azure.com/Organisation/Project/_git/Repository"},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -82,8 +82,17 @@ func (c gitClient) latestCommitID(ctx context.Context, opt fetchOptions) (string
|
||||
return "", errors.Wrap(err, "failed to list repository refs")
|
||||
}
|
||||
|
||||
referenceName := opt.referenceName
|
||||
if referenceName == "" {
|
||||
for _, ref := range refs {
|
||||
if strings.EqualFold(ref.Name().String(), "HEAD") {
|
||||
referenceName = ref.Target().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
if strings.EqualFold(ref.Name().String(), opt.referenceName) {
|
||||
if strings.EqualFold(ref.Name().String(), referenceName) {
|
||||
return ref.Hash().String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
79
api/go.mod
79
api/go.mod
@@ -1,51 +1,56 @@
|
||||
module github.com/portainer/portainer/api
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/Microsoft/go-winio v0.5.1
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
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 v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.16+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
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/go-playground/validator/v10 v10.10.1
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
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.11
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
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-20220225003350-cec58db3549e
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
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
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
k8s.io/api v0.22.5
|
||||
k8s.io/apimachinery v0.22.5
|
||||
k8s.io/client-go v0.22.5
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
|
||||
)
|
||||
|
||||
@@ -57,41 +62,44 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
|
||||
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
|
||||
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
|
||||
github.com/jpillora/ansi v1.0.2 // indirect
|
||||
github.com/jpillora/requestlog v1.0.0 // indirect
|
||||
github.com/jpillora/sizestr v1.0.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.15.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
@@ -101,21 +109,20 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
|
||||
google.golang.org/grpc v1.33.2 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.9.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
|
||||
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
718
api/go.sum
718
api/go.sum
File diff suppressed because it is too large
Load Diff
@@ -9,4 +9,6 @@ var (
|
||||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
// ErrResourceAccessDenied Access denied to resource error
|
||||
ErrResourceAccessDenied = errors.New("Access denied to resource")
|
||||
// ErrNotAvailableInDemo feature is not allowed in demo
|
||||
ErrNotAvailableInDemo = errors.New("This feature is not available in the demo version of Portainer")
|
||||
)
|
||||
|
||||
@@ -100,7 +100,8 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
forceChangePassword := !handler.passwordStrengthChecker.Check(password)
|
||||
return handler.writeToken(w, user, forceChangePassword)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
@@ -131,11 +132,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
return handler.writeToken(w, user, false)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user)
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, forceChangePassword bool) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user, forceChangePassword)
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
@@ -206,10 +207,11 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
|
||||
return false
|
||||
}
|
||||
|
||||
func composeTokenData(user *portainer.User) *portainer.TokenData {
|
||||
func composeTokenData(user *portainer.User, forceChangePassword bool) *portainer.TokenData {
|
||||
return &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ForceChangePassword: forceChangePassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
return handler.writeToken(w, user, false)
|
||||
}
|
||||
|
||||
@@ -22,12 +22,14 @@ type Handler struct {
|
||||
OAuthService portainer.OAuthService
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
passwordStrengthChecker security.PasswordStrengthChecker
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -49,7 +50,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
@@ -86,7 +87,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -25,7 +27,17 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates an new instance of backup handler
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
|
||||
func NewHandler(
|
||||
bouncer *security.RequestBouncer,
|
||||
dataStore dataservices.DataStore,
|
||||
gate *offlinegate.OfflineGate,
|
||||
filestorePath string,
|
||||
shutdownTrigger context.CancelFunc,
|
||||
adminMonitor *adminmonitor.Monitor,
|
||||
demoService *demo.Service,
|
||||
|
||||
) *Handler {
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
@@ -36,8 +48,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
adminMonitor: adminMonitor,
|
||||
}
|
||||
|
||||
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter := h.NewRoute().Subrouter()
|
||||
demoRestrictedRouter.Use(middlewares.RestrictDemoEnv(demoService.IsDemo))
|
||||
|
||||
demoRestrictedRouter.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -50,7 +65,7 @@ func adminAccess(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -51,7 +52,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, test.backupPassword)
|
||||
@@ -74,7 +75,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, "password")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
@@ -114,6 +116,8 @@ type customTemplateFromFileContentPayload struct {
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
}
|
||||
|
||||
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
@@ -135,6 +139,12 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
|
||||
err := validateVariablesDefinitions(payload.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -163,6 +173,7 @@ func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*p
|
||||
Platform: (payload.Platform),
|
||||
Type: (payload.Type),
|
||||
Logo: payload.Logo,
|
||||
Variables: payload.Variables,
|
||||
}
|
||||
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
@@ -203,6 +214,8 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||
RepositoryPassword string `example:"myGitPassword"`
|
||||
// Path to the Stack file inside the Git repository
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
}
|
||||
|
||||
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -235,6 +248,12 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
|
||||
err := validateVariablesDefinitions(payload.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -255,6 +274,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
Platform: payload.Platform,
|
||||
Type: payload.Type,
|
||||
Logo: payload.Logo,
|
||||
Variables: payload.Variables,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
|
||||
@@ -271,14 +291,19 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isValidProject := true
|
||||
defer func() {
|
||||
if !isValidProject {
|
||||
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
|
||||
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
|
||||
|
||||
exists, err := handler.FileService.FileExists(entryPath)
|
||||
if err != nil || !exists {
|
||||
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
|
||||
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
|
||||
}
|
||||
isValidProject = false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -289,6 +314,16 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
|
||||
}
|
||||
|
||||
info, err := os.Lstat(entryPath)
|
||||
if err != nil {
|
||||
isValidProject = false
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
|
||||
isValidProject = false
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file is not a symbolic link")
|
||||
}
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
@@ -300,6 +335,8 @@ type customTemplateFromFileUploadPayload struct {
|
||||
Platform portainer.CustomTemplatePlatform
|
||||
Type portainer.StackType
|
||||
FileContent []byte
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
}
|
||||
|
||||
func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
@@ -345,6 +382,17 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||
}
|
||||
payload.FileContent = composeFileContent
|
||||
|
||||
varsString, _ := request.RetrieveMultiPartFormValue(r, "Variables", true)
|
||||
err = json.Unmarshal([]byte(varsString), &payload.Variables)
|
||||
if err != nil {
|
||||
return errors.New("Invalid variables. Ensure that the variables are valid JSON")
|
||||
}
|
||||
|
||||
err = validateVariablesDefinitions(payload.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -365,6 +413,7 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
|
||||
Type: payload.Type,
|
||||
Logo: payload.Logo,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Variables: payload.Variables,
|
||||
}
|
||||
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
|
||||
@@ -31,6 +31,8 @@ type customTemplateUpdatePayload struct {
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
}
|
||||
|
||||
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -52,6 +54,12 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
|
||||
err := validateVariablesDefinitions(payload.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,6 +132,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
customTemplate.Note = payload.Note
|
||||
customTemplate.Platform = payload.Platform
|
||||
customTemplate.Type = payload.Type
|
||||
customTemplate.Variables = payload.Variables
|
||||
|
||||
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
|
||||
if err != nil {
|
||||
|
||||
19
api/http/handler/customtemplates/utils.go
Normal file
19
api/http/handler/customtemplates/utils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func validateVariablesDefinitions(variables []portainer.CustomTemplateVariableDefinition) error {
|
||||
for _, variable := range variables {
|
||||
if variable.Name == "" {
|
||||
return errors.New("variable name is required")
|
||||
}
|
||||
if variable.Label == "" {
|
||||
return errors.New("variable label is required")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Environments", &endpoints, false)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid environments")
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
}
|
||||
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob)
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeJobEndpointMeta {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
continue
|
||||
}
|
||||
|
||||
if meta, ok := edgeJob.Endpoints[endpointID]; ok {
|
||||
if meta, exists := edgeJob.Endpoints[endpointID]; exists {
|
||||
endpointsMap[endpointID] = meta
|
||||
} else {
|
||||
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
|
||||
@@ -103,13 +103,19 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
|
||||
updateVersion := false
|
||||
if payload.CronExpression != nil {
|
||||
if payload.CronExpression != nil && *payload.CronExpression != edgeJob.CronExpression {
|
||||
edgeJob.CronExpression = *payload.CronExpression
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if payload.FileContent != nil {
|
||||
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), []byte(*payload.FileContent))
|
||||
fileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.FileContent != nil && *payload.FileContent != string(fileContent) {
|
||||
fileContent = []byte(*payload.FileContent)
|
||||
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), fileContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -117,7 +123,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if payload.Recurring != nil {
|
||||
if payload.Recurring != nil && *payload.Recurring != edgeJob.Recurring {
|
||||
edgeJob.Recurring = *payload.Recurring
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
64
api/http/handler/edgestacks/edgestack_status_delete.go
Normal file
64
api/http/handler/edgestacks/edgestack_status_delete.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
|
||||
httpErr := &httperror.HandlerError{http.StatusInternalServerError, msg, err}
|
||||
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
httpErr.StatusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
return httpErr
|
||||
}
|
||||
|
||||
// @id EdgeStackStatusDelete
|
||||
// @summary Delete an EdgeStack status
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 404
|
||||
// @failure 403
|
||||
// @router /edge_stacks/{id}/status/{endpoint_id} [delete]
|
||||
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a valid endpoint from the handler context", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err != nil {
|
||||
return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
delete(stack.Status, endpoint.ID)
|
||||
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
924
api/http/handler/edgestacks/edgestack_test.go
Normal file
@@ -0,0 +1,924 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
jwtService, err := jwt.NewService("1h", store)
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := &portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole}
|
||||
err = store.User().Create(user)
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
rawAPIKey, _, err := apiKeyService.GenerateApiKey(*user, "test")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
)
|
||||
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewService(tmpDir, "")
|
||||
if err != nil {
|
||||
storeTeardown()
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler.FileService = fs
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.EnableEdgeComputeFeatures = true
|
||||
|
||||
err = handler.DataStore.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
|
||||
|
||||
return handler, rawAPIKey, storeTeardown
|
||||
}
|
||||
|
||||
func createEndpoint(t *testing.T, store dataservices.DataStore) portainer.Endpoint {
|
||||
t.Helper()
|
||||
|
||||
endpointID := portainer.EndpointID(5)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID portainer.EndpointID) portainer.EdgeStack {
|
||||
t.Helper()
|
||||
|
||||
edgeGroup := portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "EdgeGroup 1",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{endpointID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err := store.EdgeGroup().Create(&edgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(14)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpointID},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
|
||||
ProjectPath: "/project/path",
|
||||
EntryPoint: "entrypoint",
|
||||
Version: 237,
|
||||
ManifestPath: "/manifest/path",
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpointID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return edgeStack
|
||||
}
|
||||
|
||||
// Inspect
|
||||
func TestInspectInvalidEdgeID(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
EdgeStackID string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{"Invalid EdgeStackID", "x", 400},
|
||||
{"Non-existing EdgeStackID", "5", 404},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/edge_stacks/"+tc.EdgeStackID, nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create
|
||||
func TestCreateAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
// Create Endpoint, EdgeGroup and EndpointRelation
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeGroup := portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "EdgeGroup 1",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{endpoint.ID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
payload := swarmStackFromFileContentPayload{
|
||||
Name: "Test Stack",
|
||||
StackFileContent: "stack content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
|
||||
// Create EdgeStack
|
||||
req, err := http.NewRequest(http.MethodPost, "/edge_stacks?method=string", r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data = portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if payload.Name != data.Name {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStack Name %s, found %s", payload.Name, data.Name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateWithInvalidPayload(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload interface{}
|
||||
QueryString string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
Name: "Invalid query string parameter",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "invalid=query-string",
|
||||
ExpectedStatusCode: 400,
|
||||
},
|
||||
{
|
||||
Name: "Invalid creation method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=invalid-creation-method",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with string method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with repository method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=repository",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty swarmStackFromFileContentPayload with file method",
|
||||
Payload: swarmStackFromFileContentPayload{},
|
||||
QueryString: "method=file",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Duplicated EdgeStack Name",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: edgeStack.Name,
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: edgeStack.EdgeGroups,
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty EdgeStack Groups",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: edgeStack.Name,
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "EdgeStackDeploymentKubernetes with Docker endpoint",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: "Stack name",
|
||||
StackFileContent: "content",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Empty Stack File Content",
|
||||
Payload: swarmStackFromFileContentPayload{
|
||||
Name: "Stack name",
|
||||
StackFileContent: "",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
},
|
||||
QueryString: "method=string",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
{
|
||||
Name: "Clone Git respository error",
|
||||
Payload: swarmStackFromGitRepositoryPayload{
|
||||
Name: "Stack name",
|
||||
RepositoryURL: "github.com/portainer/portainer",
|
||||
RepositoryReferenceName: "ref name",
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: "",
|
||||
RepositoryPassword: "",
|
||||
FilePathInRepository: "/file/path",
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
},
|
||||
QueryString: "method=repository",
|
||||
ExpectedStatusCode: 500,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
|
||||
// Create EdgeStack
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks?%s", tc.QueryString), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete
|
||||
func TestDeleteAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
// Create
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Inspect
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.ID != edgeStack.ID {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID))
|
||||
}
|
||||
|
||||
// Delete
|
||||
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNoContent, rec.Code))
|
||||
}
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusNotFound, rec.Code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteInvalidEdgeStack(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
URL string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{Name: "Non-existing EdgeStackID", URL: "/edge_stacks/-1", ExpectedStatusCode: http.StatusNotFound},
|
||||
{Name: "Invalid EdgeStackID", URL: "/edge_stacks/aaaaaaa", ExpectedStatusCode: http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodDelete, tc.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update
|
||||
func TestUpdateAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack: create new Endpoint, EndpointRelation and EdgeGroup
|
||||
endpointID := portainer.EndpointID(6)
|
||||
newEndpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().Create(&newEndpoint)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpointID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newEdgeGroup := portainer.EdgeGroup{
|
||||
ID: 2,
|
||||
Name: "EdgeGroup 2",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{newEndpoint.ID},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newVersion := 238
|
||||
payload := updateEdgeStackPayload{
|
||||
StackFileContent: "update-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: append(edgeStack.EdgeGroups, newEdgeGroup.ID),
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.Version != *payload.Version {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackID %d, found %d", edgeStack.Version, data.Version))
|
||||
}
|
||||
|
||||
if data.DeploymentType != payload.DeploymentType {
|
||||
t.Fatalf(fmt.Sprintf("expected DeploymentType %d, found %d", edgeStack.DeploymentType, data.DeploymentType))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data.EdgeGroups, payload.EdgeGroups) {
|
||||
t.Fatalf("expected EdgeGroups to be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
//newEndpoint := createEndpoint(t, handler.DataStore)
|
||||
newEdgeGroup := portainer.EdgeGroup{
|
||||
ID: 2,
|
||||
Name: "EdgeGroup 2",
|
||||
Dynamic: false,
|
||||
TagIDs: nil,
|
||||
Endpoints: []portainer.EndpointID{8889},
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
|
||||
|
||||
newVersion := 238
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateEdgeStackPayload
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with non-existing EdgeGroupID",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{9999},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Update with invalid EdgeGroup (non-existing Endpoint)",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{2},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Update DeploymentType from Docker to Kubernetes",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{1},
|
||||
DeploymentType: portainer.EdgeStackDeploymentKubernetes,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateWithInvalidPayload(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
newVersion := 238
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateEdgeStackPayload
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with empty StackFileContent",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: edgeStack.EdgeGroups,
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
"Update with empty EdgeGroups",
|
||||
updateEdgeStackPayload{
|
||||
StackFileContent: "error-test",
|
||||
Version: &newVersion,
|
||||
EdgeGroups: []portainer.EdgeGroupID{},
|
||||
DeploymentType: edgeStack.DeploymentType,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Update Status
|
||||
func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
handler, rawAPIKey, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack status
|
||||
newStatus := portainer.StatusError
|
||||
payload := updateStatusPayload{
|
||||
Error: "test-error",
|
||||
Status: &newStatus,
|
||||
EndpointID: &endpoint.ID,
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].Type != *payload.Status {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusType %d, found %d", payload.Status, data.Status[endpoint.ID].Type))
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].Error != payload.Error {
|
||||
t.Fatalf(fmt.Sprintf("expected EdgeStackStatusError %s, found %s", payload.Error, data.Status[endpoint.ID].Error))
|
||||
}
|
||||
|
||||
if data.Status[endpoint.ID].EndpointID != *payload.EndpointID {
|
||||
t.Fatalf(fmt.Sprintf("expected EndpointID %d, found %d", payload.EndpointID, data.Status[endpoint.ID].EndpointID))
|
||||
}
|
||||
}
|
||||
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
||||
handler, _, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
// Update edge stack status
|
||||
statusError := portainer.StatusError
|
||||
statusOk := portainer.StatusOk
|
||||
cases := []struct {
|
||||
Name string
|
||||
Payload updateStatusPayload
|
||||
ExpectedErrorMessage string
|
||||
ExpectedStatusCode int
|
||||
}{
|
||||
{
|
||||
"Update with nil Status",
|
||||
updateStatusPayload{
|
||||
Error: "test-error",
|
||||
Status: nil,
|
||||
EndpointID: &endpoint.ID,
|
||||
},
|
||||
"Invalid status",
|
||||
400,
|
||||
},
|
||||
{
|
||||
"Update with error status and empty error message",
|
||||
updateStatusPayload{
|
||||
Error: "",
|
||||
Status: &statusError,
|
||||
EndpointID: &endpoint.ID,
|
||||
},
|
||||
"Error message is mandatory when status is error",
|
||||
400,
|
||||
},
|
||||
{
|
||||
"Update with nil EndpointID",
|
||||
updateStatusPayload{
|
||||
Error: "",
|
||||
Status: &statusOk,
|
||||
EndpointID: nil,
|
||||
},
|
||||
"Invalid EnvironmentID",
|
||||
400,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tc.ExpectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", tc.ExpectedStatusCode, rec.Code))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Status
|
||||
func TestDeleteStatus(t *testing.T) {
|
||||
handler, _, teardown := setupHandler(t)
|
||||
defer teardown()
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package edgestacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -80,8 +81,8 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
|
||||
}
|
||||
|
||||
oldRelatedSet := EndpointSet(relatedEndpointIds)
|
||||
newRelatedSet := EndpointSet(newRelated)
|
||||
oldRelatedSet := endpointutils.EndpointSet(relatedEndpointIds)
|
||||
newRelatedSet := endpointutils.EndpointSet(newRelated)
|
||||
|
||||
endpointsToRemove := map[portainer.EndpointID]bool{}
|
||||
for endpointID := range oldRelatedSet {
|
||||
@@ -189,13 +190,3 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
|
||||
set := map[portainer.EndpointID]bool{}
|
||||
|
||||
for _, endpointID := range endpointIDs {
|
||||
set[endpointID] = true
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -24,10 +25,11 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
DataStore: dataStore,
|
||||
}
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
@@ -43,6 +45,12 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
|
||||
edgeStackStatusRouter := h.NewRoute().Subrouter()
|
||||
edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id"))
|
||||
|
||||
edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user