Compare commits

..

3 Commits

Author SHA1 Message Date
Dmitry Salakhov
8c38c8d9b0 tmp 2022-02-11 11:53:23 +13:00
Dmitry Salakhov
5c37e6946b tmp 2022-01-12 17:52:55 +13:00
Dmitry Salakhov
4ab49b08f9 tmp 2022-01-10 10:57:17 +13:00
1536 changed files with 25269 additions and 47879 deletions

View File

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

View File

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

View File

@@ -79,7 +79,6 @@ overrides:
react/forbid-prop-types: off
react/require-default-props: off
react/no-array-index-key: off
no-underscore-dangle: off
react/jsx-filename-extension: [0]
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
'@typescript-eslint/explicit-module-boundary-types': off
@@ -88,8 +87,6 @@ overrides:
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
- files:
- app/**/*.test.*
extends:
@@ -97,9 +94,3 @@ overrides:
- 'plugin:jest/style'
env:
'jest/globals': true
rules:
'react/jsx-no-constructed-context-values': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off

View File

@@ -1,5 +0,0 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169

View File

@@ -2,7 +2,7 @@
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

View File

@@ -12,7 +12,7 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
@@ -47,7 +47,7 @@ You can see how [here](https://documentation.portainer.io/r/portainer-logs)
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**

View File

@@ -4,11 +4,11 @@ about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
---
Before you start, we need a little bit more information from you:
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
Have you reviewed our technical documentation and knowledge base? Yes/No
@@ -16,7 +16,7 @@ Have you reviewed our technical documentation and knowledge base? Yes/No
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->

View File

@@ -4,13 +4,14 @@ about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

View File

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

View File

@@ -1,38 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
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 ./...

View File

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

View File

@@ -4,16 +4,21 @@
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": ["*.html"],
"files": [
"*.html"
],
"options": {
"parser": "angular"
}
},
{
"files": ["*.{j,t}sx", "*.ts"],
"files": [
"*.{j,t}sx",
"*.ts"
],
"options": {
"printWidth": 80
}
}
]
}
}

View File

@@ -16,9 +16,6 @@ module.exports = {
exportLocalsConvention: 'camelCaseOnly',
},
},
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
@@ -34,5 +31,4 @@ module.exports = {
core: {
builder: 'webpack5',
},
staticDirs: ['./public'],
};

View File

@@ -1,22 +1,6 @@
import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
// Initialize MSW
initMSW({
onUnhandledRequest: ({ method, url }) => {
if (url.pathname.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
`);
}
},
});
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
@@ -26,9 +10,6 @@ export const parameters = {
date: /Date$/,
},
},
msw: {
handlers,
},
};
export const decorators = [
@@ -37,5 +18,4 @@ export const decorators = [
<Story />
</UIRouter>
),
mswDecorator,
];

View File

@@ -1,328 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.3).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
const bypassHeaderName = 'x-msw-bypass';
const activeClientIds = new Set();
self.addEventListener('install', function () {
return self.skipWaiting();
});
self.addEventListener('activate', async function (event) {
return self.clients.claim();
});
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
}
const client = await self.clients.get(clientId);
if (!client) {
return;
}
const allClients = await self.clients.matchAll();
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
});
break;
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
});
break;
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId);
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
});
break;
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}
break;
}
}
});
// Resolve the "main" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (client.frameType === 'top-level') {
return client;
}
const allClients = await self.clients.matchAll();
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible';
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
}
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const clonedResponse = response.clone();
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body: clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
}
return response;
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse();
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
return fetch(originalRequest);
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay);
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse();
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
// Rejecting a request Promise emulates a network error.
throw networkError;
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body);
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
return respondWithMock(clientMessage);
}
}
return getOriginalResponse();
}
self.addEventListener('fetch', function (event) {
const { request } = event;
const accept = request.headers.get('accept') || '';
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return;
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
const requestId = uuidv4();
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url);
return;
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`
);
})
);
});
function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value;
});
return reqHeaders;
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
}
resolve(event.data);
};
client.postMessage(JSON.stringify(message), [channel.port2]);
});
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,

View File

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

View File

@@ -42,10 +42,10 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.

View File

@@ -3,48 +3,32 @@ 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
mu sync.Mutex
adminInitDisabled bool
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
@@ -57,11 +41,7 @@ func (m *Monitor) Start() {
logFatalf("%s", err)
}
if !initialized {
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
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
@@ -73,9 +53,6 @@ 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
}
@@ -91,25 +68,3 @@ 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)
})
}

View File

@@ -21,18 +21,6 @@ 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()
@@ -42,13 +30,21 @@ func Test_canStopStartedMonitor(t *testing.T) {
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
assert.True(t, fataled, "monitor should been timeout and fatal")
}

View File

@@ -50,15 +50,4 @@ 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/).

View File

@@ -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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())

View File

@@ -10,7 +10,6 @@ import (
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
@@ -66,20 +65,5 @@ func restoreFiles(srcDir string, destinationDir string) error {
return err
}
}
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
return nil
}

View File

@@ -3,29 +3,22 @@ package chisel
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
)
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
// The key represents the following data in this particular format:
// http(s)://serverURL|http(s)://host:tunnelServerPort|tunnel_server_fingerprint|endpoint_ID
// Protocol used to connect to the tunnel server will match the one used by the Portainer server (http or https).
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
// The key returned by this function is a base64 encoded version of the data.
func (service *Service) GenerateEdgeKey(serverURL, host string, endpointIdentifier int) (string, error) {
u, err := url.Parse(serverURL)
if err != nil {
return "", err
}
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
keyInformation := []string{
serverURL,
fmt.Sprintf("%s://%s:%s", u.Scheme, host, service.serverPort),
url,
fmt.Sprintf("%s:%s", host, service.serverPort),
service.serverFingerprint,
strconv.Itoa(endpointIdentifier),
}
key := strings.Join(keyInformation, "|")
return base64.RawStdEncoding.EncodeToString([]byte(key)), nil
return base64.RawStdEncoding.EncodeToString([]byte(key))
}

View File

@@ -1,13 +1,14 @@
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) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel := service.GetTunnelDetails(endpointID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
@@ -23,25 +24,24 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
tunnel.Jobs[existingJobIndex] = *edgeJob
}
service.mu.Unlock()
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnelDetails := item.Val.(*portainer.TunnelDetails)
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 := make([]portainer.EdgeJob, 0)
for _, edgeJob := range tunnelDetails.Jobs {
if edgeJob.ID == edgeJobID {
continue
}
updatedJobs = append(updatedJobs, edgeJob)
}
tunnel.Jobs = tunnel.Jobs[:n]
tunnelDetails.Jobs = updatedJobs
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
}
service.mu.Unlock()
}

View File

@@ -3,16 +3,17 @@ package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"sync"
"strconv"
"time"
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
)
const (
@@ -27,19 +28,18 @@ const (
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
tunnelDetailsMap cmap.ConcurrentMap
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: make(map[portainer.EndpointID]*portainer.TunnelDetails),
tunnelDetailsMap: cmap.New(),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
@@ -58,7 +58,11 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
Timeout: 3 * time.Second,
}
_, err = httpClient.Do(req)
return err
if err != nil {
return err
}
return nil
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
@@ -95,7 +99,7 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
// be found inside the database, it will generate a new one randomly and persist it.
// It starts the tunnel status verification process in the background.
// The snapshotter is used in the tunnel status verification process.
func (service *Service) StartTunnelServer(addr, port, certPath, keyPath string, snapshotService portainer.SnapshotService) error {
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
keySeed, err := service.retrievePrivateKeySeed()
if err != nil {
return err
@@ -104,10 +108,6 @@ func (service *Service) StartTunnelServer(addr, port, certPath, keyPath string,
config := &chserver.Config{
Reverse: true,
KeySeed: keySeed,
TLS: chserver.TLSConfig{
Cert: certPath,
Key: keyPath,
},
}
chiselServer, err := chserver.NewServer(config)
@@ -185,37 +185,42 @@ func (service *Service) startTunnelVerificationLoop() {
}
func (service *Service) checkTunnels() {
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*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: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, 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())
err := service.snapshotEnvironment(endpointID, tunnel.Port)
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
}
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
}
}
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))

View File

@@ -4,11 +4,13 @@ import (
"encoding/base64"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
"github.com/dchest/uniuri"
portainer "github.com/portainer/portainer/api"
)
@@ -17,13 +19,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 _, tunnel := range service.tunnelDetailsMap {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
if tunnel.Port == port {
return service.getUnusedPort()
}
@@ -36,32 +38,26 @@ 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 {
service.mu.Lock()
defer service.mu.Unlock()
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
key := strconv.Itoa(int(endpointID))
return *service.getTunnelDetails(endpointID)
if item, ok := service.tunnelDetailsMap.Get(key); ok {
tunnelDetails := item.(*portainer.TunnelDetails)
return tunnelDetails
}
jobs := make([]portainer.EdgeJob, 0)
return &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
Port: 0,
Jobs: jobs,
Credentials: "",
}
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
@@ -72,13 +68,13 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
@@ -87,27 +83,29 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
return service.GetTunnelDetails(endpoint.ID), nil
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel := service.GetTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
service.mu.Unlock()
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.GetTunnelDetails(endpointID)
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
@@ -118,9 +116,10 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
service.ProxyManager.DeleteEndpointProxy(endpointID)
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.mu.Unlock()
service.ProxyManager.DeleteEndpointProxy(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
@@ -129,10 +128,7 @@ 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)
service.mu.Lock()
defer service.mu.Unlock()
tunnel := service.GetTunnelDetails(endpointID)
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
@@ -156,6 +152,9 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
return err
}
tunnel.Credentials = credentials
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
return nil

View File

@@ -35,7 +35,6 @@ 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,17 +50,13 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
InitialMmapSize: kingpin.Flag("initial-mmap-size", "Initial mmap size of the database in bytes").Int(),
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
}
kingpin.Parse()
@@ -130,7 +125,7 @@ func validateEndpointURL(endpointURL string) error {
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != "" {
if snapshotInterval != defaultSnapshotInterval {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval

View File

@@ -18,6 +18,8 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -15,7 +15,8 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -13,7 +13,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
importFile := "/data/import.json"
if exists, _ := fileService.FileExists(importFile); exists {
if err := store.Import(importFile); err != nil {
logrus.WithError(err).Debugf("Import %s failed", importFile)
logrus.WithError(err).Debugf("import %s failed", importFile)
// TODO: should really rollback on failure, but then we have nothing.
} else {
@@ -23,7 +23,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatalf("Failed initializing data store: %v", err)
log.Fatalf("failed initializing data store: %v", err)
}
}
}

View File

@@ -10,8 +10,7 @@ func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
@@ -20,10 +19,8 @@ import (
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database"
"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"
@@ -50,12 +47,12 @@ func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
logrus.Fatalf("Failed parsing flags: %v", err)
log.Fatalf("failed parsing flags: %v", err)
}
err = cliService.ValidateFlags(flags)
if err != nil {
logrus.Fatalf("Failed validating flags:%v", err)
log.Fatalf("failed validating flags:%v", err)
}
return flags
}
@@ -63,72 +60,62 @@ func initCLI() *portainer.CLIFlags {
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
logrus.Fatalf("Failed creating file service: %v", err)
log.Fatalf("failed creating file service: %v", err)
}
return fileService
}
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
func initDataStore(flags *portainer.CLIFlags, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data)
if err != nil {
logrus.Fatalf("failed creating database connection: %s", err)
panic(err)
}
if bconn, ok := connection.(*boltdb.DbConnection); ok {
bconn.MaxBatchSize = *flags.MaxBatchSize
bconn.MaxBatchDelay = *flags.MaxBatchDelay
bconn.InitialMmapSize = *flags.InitialMmapSize
} else {
logrus.Fatalf("failed creating database connection: expecting a boltdb database type but a different one was received")
}
store := datastore.NewStore(*flags.Data, fileService, connection)
isNew, err := store.Open()
if err != nil {
logrus.Fatalf("Failed opening store: %v", err)
log.Fatalf("failed opening store: %v", err)
}
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
logrus.Fatalf("Failed rolling back: %v", err)
log.Fatalf("failed rolling back: %s", err)
}
logrus.Println("Exiting rollback")
log.Println("Exiting rollback")
os.Exit(0)
return nil
}
// Init sets some defaults - it's basically a migration
// Init sets some defaults - its basically a migration
err = store.Init()
if err != nil {
logrus.Fatalf("Failed initializing data store: %v", err)
log.Fatalf("failed initializing data store: %v", err)
}
if isNew {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
// Disabled for now. Can't use feature flags due to the way that works
// EXPERIMENTAL, will only activate if `/data/import.json` exists
//importFromJson(fileService, store)
err := updateSettingsFromFlags(store, flags)
if err != nil {
logrus.Fatalf("Failed updating settings from flags: %v", err)
}
} else {
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
logrus.Fatalf("Something Failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
logrus.Fatalf("Failed migration: %v", err)
}
log.Fatalf("failed updating settings from flags: %v", err)
}
}
err = updateSettingsFromFlags(store, flags)
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
log.Fatalf("Failed updating settings from flags: %v", err)
log.Fatalf("Something failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
log.Fatalf("failed migration: %v", err)
}
}
// this is for the db restore functionality - needs more tests.
@@ -140,7 +127,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
err := store.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
} else {
logrus.Debugf("exported to %s", exportFilename)
}
@@ -152,7 +139,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
if err != nil {
logrus.Fatalf("Failed creating compose manager: %v", err)
log.Fatalf("failed creating compose manager: %s", err)
}
return composeWrapper
@@ -209,7 +196,7 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
@@ -234,11 +221,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
}
func initSnapshotService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
func initSnapshotService(snapshotInterval string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil {
return nil, err
}
@@ -259,17 +246,11 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err
}
if *flags.SnapshotInterval != "" {
settings.SnapshotInterval = *flags.SnapshotInterval
}
if *flags.Logo != "" {
settings.LogoURL = *flags.Logo
}
if *flags.EnableEdgeComputeFeatures {
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
}
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -279,12 +260,6 @@ 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
@@ -297,8 +272,8 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
if *flags.HTTPDisabled {
sslSettings.HTTPEnabled = false
} else if *flags.HTTPEnabled {
sslSettings.HTTPEnabled = true
} else {
sslSettings.HTTPEnabled = *flags.HTTPEnabled || sslSettings.HTTPEnabled
}
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
@@ -341,9 +316,9 @@ func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.
}
if featureState {
logrus.Printf("Feature %v : on", *correspondingFeature)
log.Printf("Feature %v : on", *correspondingFeature)
} else {
logrus.Printf("Feature %v : off", *correspondingFeature)
log.Printf("Feature %v : off", *correspondingFeature)
}
settings.FeatureFlagSettings[*correspondingFeature] = featureState
@@ -372,7 +347,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
logrus.Fatalf("Failed checking for existing key pair: %v", err)
log.Fatalf("failed checking for existing key pair: %v", err)
}
if existingKeyPair {
@@ -405,6 +380,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
TLSConfig: tlsConfiguration,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -442,7 +418,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -466,6 +442,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
TLSConfig: portainer.TLSConfiguration{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -487,7 +464,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -504,7 +481,7 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
}
if len(endpoints) > 0 {
logrus.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
log.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
return nil
}
@@ -514,81 +491,59 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func loadEncryptionSecretKey(keyfilename string) []byte {
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
if err != nil {
if os.IsNotExist(err) {
logrus.Printf("Encryption key file `%s` not present", keyfilename)
} else {
logrus.Printf("Error reading encryption key file: %v", err)
}
return nil
}
// return a 32 byte hash of the secret (required for AES)
hash := sha256.Sum256(content)
return hash[:]
}
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
logrus.Println("Proceeding without encryption key")
}
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
dataStore := initDataStore(flags, fileService, shutdownCtx)
if err := dataStore.CheckCurrentEdition(); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
logrus.Fatalf("Failed getting instance id: %v", err)
log.Fatalf("failed getting instance id: %v", err)
}
apiKeyService := initAPIKeyService(dataStore)
settings, err := dataStore.Settings().Settings()
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
if err != nil {
logrus.Fatalf("Failed initializing JWT service: %v", err)
log.Fatalf("failed initializing JWT service: %v", err)
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
logrus.Fatalf("Failed enabling feature flag: %v", err)
log.Fatalf("failed enabling feature flag: %v", err)
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
openAMTService := openamt.NewService()
openAMTService := openamt.NewService(dataStore)
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
sslSettings, err := sslService.GetSSLSettings()
if err != nil {
logrus.Fatalf("Failed to get ssl settings: %s", err)
log.Fatalf("failed to get ssl settings: %s", err)
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
logrus.Fatalf("Failed initializing key pair: %v", err)
log.Fatalf("failed initializing key pair: %v", err)
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
@@ -598,7 +553,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
logrus.Fatalf("Failed initializing snapshot service: %v", err)
log.Fatalf("failed initializing snapshot service: %v", err)
}
snapshotService.Start()
@@ -607,9 +562,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
reverseTunnelService.ProxyManager = proxyManager
@@ -619,45 +574,37 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
logrus.Fatalf("Failed initializing swarm stack manager: %v", err)
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
logrus.Fatalf("Failed initializing helm package manager: %v", err)
log.Fatalf("failed initializing helm package manager: %s", err)
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
logrus.Fatalf("Failed loading edge jobs from database: %v", err)
log.Fatalf("failed loading edge jobs from database: %v", err)
}
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)
log.Fatalf("failed initializing environment: %v", err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
logrus.Fatalf("Failed getting admin password file: %v", err)
log.Fatalf("failed getting admin password file: %v", err)
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
logrus.Fatalf("Failed hashing admin password: %v", err)
log.Fatalf("failed hashing admin password: %v", err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -666,11 +613,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
logrus.Fatalf("Failed getting admin user: %v", err)
log.Fatalf("failed getting admin user: %v", err)
}
if len(users) == 0 {
logrus.Println("Created admin user with the given password.")
log.Println("Created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
@@ -678,23 +625,21 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
err := dataStore.User().Create(user)
if err != nil {
logrus.Fatalf("Failed creating admin user: %v", err)
log.Fatalf("failed creating admin user: %v", err)
}
} else {
logrus.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
certPath, keyPath := fileService.GetDefaultSSLCertsPath()
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, certPath, keyPath, snapshotService)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
logrus.Fatalf("Failed starting tunnel server: %v", err)
log.Fatalf("failed starting tunnel server: %s", err)
}
sslDBSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
logrus.Fatalf("Failed to fetch ssl settings from DB")
log.Fatalf("failed to fetch ssl settings from DB")
}
scheduler := scheduler.NewScheduler(shutdownCtx)
@@ -724,7 +669,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeClusterAccessService: kubeClusterAccessService,
KubeConfigService: kubeConfigService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
@@ -734,7 +679,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
BaseURL: *flags.BaseURL,
}
}
@@ -745,8 +690,8 @@ func main() {
for {
server := buildServer(flags)
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
err := server.Start()
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
}
}

View File

@@ -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, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
tests := []struct {
@@ -29,6 +29,11 @@ func Test_enableFeaturesFromFlags(t *testing.T) {
isSupported bool
}{
{"test", false},
{"openamt", false},
{"open-amt", true},
{"oPeN-amT", true},
{"fdo", true},
{"FDO", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
@@ -76,7 +81,7 @@ func Test_optionalFeature(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
// Enable the test feature

View File

@@ -11,16 +11,14 @@ type Connection interface {
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
//Rollback(force bool) error
//MigrateData(migratorParams *database.MigratorParameters, force bool) error
// TODO: this one is very database specific atm
BackupTo(w io.Writer) error
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetDatabaseFilename() string
GetStorePath() string
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
SetServiceName(bucketName string) error
GetObject(bucketName string, key []byte, object interface{}) error
UpdateObject(bucketName string, key []byte, object interface{}) error
@@ -29,12 +27,8 @@ 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
ConvertToKey(v int) []byte
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
}

View File

@@ -9,11 +9,11 @@ type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", err
return "", nil
}
return string(bytes), err
return string(hash), nil
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.

View File

@@ -1,53 +0,0 @@
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)
}
})
}
}

View File

@@ -2,7 +2,6 @@ package boltdb
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -10,129 +9,45 @@ import (
"path"
"time"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/dataservices/errors"
)
const (
DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb"
)
var (
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
DatabaseFileName = "portainer.db"
)
type DbConnection struct {
Path string
MaxBatchSize int
MaxBatchDelay time.Duration
InitialMmapSize int
EncryptionKey []byte
isEncrypted bool
Path string
*bolt.DB
}
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
func (connection *DbConnection) GetDatabaseFilename() string {
return DatabaseFileName
}
// GetDataseFilePath get the path + filename for the database file
func (connection *DbConnection) GetDatabaseFilePath() string {
if connection.IsEncryptedStore() {
return path.Join(connection.Path, EncryptedDatabaseFileName)
}
return path.Join(connection.Path, DatabaseFileName)
}
// GetStorePath get the filename and path for the database file
func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag
}
// Return true if the database is encrypted
func (connection *DbConnection) IsEncryptedStore() bool {
return connection.getEncryptionKey() != nil
}
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb => ERROR Fatal!
// If we have a loaded encryption key, always set encrypted
if connection.EncryptionKey != nil {
connection.SetEncrypted(true)
}
// Check for portainer.db
dbFile := path.Join(connection.Path, DatabaseFileName)
_, err := os.Stat(dbFile)
haveDbFile := err == nil
// Check for portainer.edb
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
_, err = os.Stat(edbFile)
haveEdbFile := err == nil
if haveDbFile && haveEdbFile {
// 7 - encrypted and unencrypted db?
return false, ErrHaveEncryptedAndUnencrypted
}
if haveDbFile && connection.EncryptionKey != nil {
// 3 - needs migration
return true, nil
}
if haveEdbFile && connection.EncryptionKey == nil {
// 2 - encrypted db, but no key?
return false, ErrHaveEncryptedWithNoKey
}
// 1, 4, 5, 6
return false, nil
}
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
logrus.Infof("Loading PortainerDB: %s", connection.GetDatabaseFileName())
// Disabled for now. Can't use feature flags due to the way that works
// databaseExportPath := path.Join(connection.Path, fmt.Sprintf("raw-%s-%d.json", DatabaseFileName, time.Now().Unix()))
// if err := connection.ExportRaw(databaseExportPath); err != nil {
// log.Printf("raw export to %s error: %s", databaseExportPath, err)
// } else {
// log.Printf("raw export to %s success", databaseExportPath)
// }
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
})
databasePath := path.Join(connection.Path, DatabaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
db.MaxBatchSize = connection.MaxBatchSize
db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db
return nil
}
@@ -156,12 +71,12 @@ func (connection *DbConnection) BackupTo(w io.Writer) error {
}
func (connection *DbConnection) ExportRaw(filename string) error {
databasePath := connection.GetDatabaseFilePath()
databasePath := path.Join(connection.Path, DatabaseFileName)
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
b, err := connection.ExportJson(databasePath, true)
b, err := exportJson(databasePath)
if err != nil {
return err
}
@@ -179,9 +94,12 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
// CreateBucket is a generic function used to create a bucket inside a database database.
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
return err
if err != nil {
return err
}
return nil
})
}
@@ -194,7 +112,7 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
value := bucket.Get(key)
if value == nil {
return dserrors.ErrObjectNotFound
return errors.ErrObjectNotFound
}
data = make([]byte, len(value))
@@ -206,33 +124,31 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
return err
}
return connection.UnmarshalObjectWithJsoniter(data, object)
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
return UnmarshalObject(data, object)
}
// UpdateObject is a generic function used to update an object inside a database database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := connection.MarshalObject(object)
if err != nil {
return err
}
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Put(key, data)
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
})
}
// DeleteObject is a generic function used to delete an object inside a database database.
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
@@ -241,13 +157,13 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
// DeleteAllObjects delete all objects where matching() returns (id, ok).
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
func (connection *DbConnection) DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var obj interface{}
err := connection.UnmarshalObject(v, &obj)
err := UnmarshalObject(v, &obj)
if err != nil {
return err
}
@@ -268,7 +184,7 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
var identifier int
connection.Batch(func(tx *bolt.Tx) error {
connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
@@ -283,13 +199,13 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
// CreateObject creates a new object in the bucket, using the next bucket sequence id
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -300,9 +216,10 @@ func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64)
// CreateObjectWithId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -311,23 +228,10 @@ 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 {
return connection.Batch(func(tx *bolt.Tx) error {
return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
// We manually manage sequences for schedules
@@ -336,7 +240,7 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
return err
}
data, err := connection.MarshalObject(obj)
data, err := MarshalObject(obj)
if err != nil {
return err
}
@@ -348,9 +252,10 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObject(v, obj)
err := UnmarshalObject(v, obj)
if err != nil {
return err
}
@@ -372,7 +277,7 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObjectWithJsoniter(v, obj)
err := UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
@@ -386,43 +291,3 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
})
return err
}
func (connection *DbConnection) BackupMetadata() (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
}
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
logrus.Errorf("Failed to restore metadata to bucket %s, skipped", bucketName)
continue
}
err = connection.Batch(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return bucket.SetSequence(uint64(id))
})
}
return err
}

View File

@@ -1,124 +0,0 @@
package boltdb
import (
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NeedsEncryptionMigration(t *testing.T) {
// Test the specific scenarios mentioned in NeedsEncryptionMigration
// i.e.
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb (key not important) => ERROR Fatal!
is := assert.New(t)
dir := t.TempDir()
cases := []struct {
name string
dbname string
key bool
expectError error
expectResult bool
}{
{
name: "portainer.edb + key",
dbname: EncryptedDatabaseFileName,
key: true,
expectError: nil,
expectResult: false,
},
{
name: "portainer.db + key (migration needed)",
dbname: DatabaseFileName,
key: true,
expectError: nil,
expectResult: true,
},
{
name: "portainer.db + no key",
dbname: DatabaseFileName,
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + no key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
// error tests
{
name: "portainer.edb + no key",
dbname: EncryptedDatabaseFileName,
key: false,
expectError: ErrHaveEncryptedWithNoKey,
expectResult: false,
},
{
name: "portainer.db & portainer.edb",
dbname: "both",
key: true,
expectError: ErrHaveEncryptedAndUnencrypted,
expectResult: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
connection := DbConnection{Path: dir}
if tc.dbname == "both" {
// Special case. If portainer.db and portainer.edb exist.
dbFile1 := path.Join(connection.Path, DatabaseFileName)
f, _ := os.Create(dbFile1)
f.Close()
defer os.Remove(dbFile1)
dbFile2 := path.Join(connection.Path, EncryptedDatabaseFileName)
f, _ = os.Create(dbFile2)
f.Close()
defer os.Remove(dbFile2)
} else if tc.dbname != "" {
dbFile := path.Join(connection.Path, tc.dbname)
f, _ := os.Create(dbFile)
f.Close()
defer os.Remove(dbFile)
}
if tc.key {
connection.EncryptionKey = []byte("secret")
}
result, err := connection.NeedsEncryptionMigration()
is.Equal(tc.expectError, err, "Fatal Error failure. Test: %s", tc.name)
is.Equal(result, tc.expectResult, "Failed test: %s", tc.name)
})
}
}

View File

@@ -4,36 +4,14 @@ 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, metadata bool) ([]byte, error) {
logrus.WithField("databasePath", databasePath).Infof("exportJson")
func exportJson(databasePath string) ([]byte, error) {
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
if err != nil {
return []byte("{}"), err
@@ -41,13 +19,6 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
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 {
@@ -60,7 +31,7 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
continue
}
var obj interface{}
err := c.UnmarshalObject(v, &obj)
err := UnmarshalObject(v, &obj)
if err != nil {
logrus.WithError(err).Errorf("Failed to unmarshal (bucket %s): %v", bucketName, string(v))
obj = v
@@ -73,20 +44,15 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
}
if bucketName == "version" {
backup[bucketName] = version
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
backup[bucketName] = list[0]
return nil
}
backup[bucketName] = list
return nil
}
return nil

View File

@@ -1,72 +1,26 @@
package boltdb
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
// MarshalObject encodes an object to binary format
func (connection *DbConnection) MarshalObject(object interface{}) (data []byte, err error) {
func MarshalObject(object interface{}) ([]byte, error) {
// Special case for the VERSION bucket. Here we're not using json
if v, ok := object.(string); ok {
data = []byte(v)
} else {
data, err = json.Marshal(object)
if err != nil {
return data, err
}
return []byte(v), nil
}
if connection.getEncryptionKey() == nil {
return data, nil
}
return encrypt(data, connection.getEncryptionKey())
return json.Marshal(object)
}
// UnmarshalObject decodes an object from binary data
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
var err error
if connection.getEncryptionKey() != nil {
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return errors.Wrap(err, "Failed decrypting object")
}
}
e := json.Unmarshal(data, object)
if e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
if !ok {
return errors.Wrap(err, e.Error())
}
*s = string(data)
}
return err
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
if connection.getEncryptionKey() != nil {
var err error
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return err
}
}
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
err := jsoni.Unmarshal(data, &object)
func UnmarshalObject(data []byte, object interface{}) error {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
err := json.Unmarshal(data, object)
if err != nil {
if s, ok := object.(*string); ok {
*s = string(data)
@@ -79,55 +33,10 @@ func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object
return nil
}
// mmm, don't have a KMS .... aes GCM seems the most likely from
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
block, _ := aes.NewCipher(passphrase)
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return encrypted, err
}
ciphertextByte := gcm.Seal(
nonce,
nonce,
plaintext,
nil)
return ciphertextByte, nil
}
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
if string(encrypted) == "false" {
return []byte("false"), nil
}
block, err := aes.NewCipher(passphrase)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating cypher block")
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating GCM")
}
nonceSize := gcm.NonceSize()
if len(encrypted) < nonceSize {
return encrypted, errEncryptedStringTooShort
}
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
plaintextByte, err = gcm.Open(
nil,
nonce,
ciphertextByteClean,
nil)
if err != nil {
return encrypted, errors.Wrap(err, "Error decrypting text")
}
return plaintextByte, err
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
return jsoni.Unmarshal(data, &object)
}

View File

@@ -1,7 +1,6 @@
package boltdb
import (
"crypto/sha256"
"fmt"
"testing"
@@ -9,17 +8,9 @@ import (
"github.com/stretchr/testify/assert"
)
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}`
passphrase = "my secret key"
)
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":"","Credentials":{"MPSUser":"","MPSPassword":"","MPSToken":""},"DomainConfiguration":{"CertFileText":"","CertPassword":"","DomainName":""},"WirelessConfiguration":null},"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}`
func secretToEncryptionKey(passphrase string) []byte {
hash := sha256.Sum256([]byte(passphrase))
return hash[:]
}
func Test_MarshalObjectUnencrypted(t *testing.T) {
func Test_MarshalObject(t *testing.T) {
is := assert.New(t)
uuid := uuid.Must(uuid.NewV4())
@@ -82,18 +73,16 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
data, err := MarshalObject(test.object)
is.NoError(err)
is.Equal(test.expected, string(data))
})
}
}
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
func Test_UnMarshalObject(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
@@ -116,62 +105,18 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6",
},
{
// An un-marshalled json object string should return the same as a string without error also
// An unmarshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
expected: jsonobject,
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
var object string
err := conn.UnmarshalObject(test.object, &object)
err := UnmarshalObject(test.object, &object)
is.NoError(err)
is.Equal(test.expected, string(object))
})
}
}
func Test_ObjectMarshallingEncrypted(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
tests := []struct {
object []byte
expected string
}{
{
object: []byte(""),
},
{
object: []byte("35"),
},
{
// An unmarshalled byte string should return the same without error
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
},
{
// An un-marshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
},
}
key := secretToEncryptionKey(passphrase)
conn := DbConnection{EncryptionKey: key}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
is.NoError(err)
var object []byte
err = conn.UnmarshalObject(data, &object)
is.NoError(err)
is.Equal(test.object, object)
})
}
}

View File

@@ -2,19 +2,15 @@ package database
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
)
// NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
func NewDatabase(storeType, storePath string) (connection portainer.Connection, err error) {
switch storeType {
case "boltdb":
return &boltdb.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,
}, nil
return &boltdb.DbConnection{Path: storePath}, nil
}
return nil, fmt.Errorf("unknown storage database: %s", storeType)
return nil, fmt.Errorf("Unknown storage database: %s", storeType)
}

View File

@@ -67,14 +67,14 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.connection.CreateObjectWithId(
// CreateEdgeJob creates a new Edge job
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
return service.connection.CreateObject(
BucketName,
int(edgeJob.ID),
edgeJob,
func(id uint64) (int, interface{}) {
edgeJob.ID = portainer.EdgeJobID(id)
return int(edgeJob.ID), edgeJob
},
)
}

View File

@@ -67,15 +67,14 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
return &stack, nil
}
// CreateEdgeStack saves an Edge stack object to db.
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
return service.connection.CreateObjectWithId(
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) Create(edgeStack *portainer.EdgeStack) error {
return service.connection.CreateObject(
BucketName,
int(edgeStack.ID),
edgeStack,
func(id uint64) (int, interface{}) {
edgeStack.ID = portainer.EdgeStackID(id)
return int(edgeStack.ID), edgeStack
},
)
}

View File

@@ -69,7 +69,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
return nil, fmt.Errorf("Failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil

View File

@@ -4,7 +4,6 @@ import "errors"
var (
// TODO: i'm pretty sure this needs wrapping at several levels
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrObjectNotFound = errors.New("Object not found inside the database")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
)

View File

@@ -1,93 +0,0 @@
package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "fdo_profiles"
)
// Service represents a service for managingFDO Profiles data.
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// FDOProfiles return an array containing all the FDO Profiles.
func (service *Service) FDOProfiles() ([]portainer.FDOProfile, error) {
var fdoProfiles = make([]portainer.FDOProfile, 0)
err := service.connection.GetAll(
BucketName,
&portainer.FDOProfile{},
func(obj interface{}) (interface{}, error) {
fdoProfile, ok := obj.(*portainer.FDOProfile)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to FDOProfile object")
return nil, fmt.Errorf("failed to convert to FDOProfile object: %s", obj)
}
fdoProfiles = append(fdoProfiles, *fdoProfile)
return &portainer.FDOProfile{}, nil
})
return fdoProfiles, err
}
// FDOProfile returns an FDO Profile by ID.
func (service *Service) FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error) {
var FDOProfile portainer.FDOProfile
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
if err != nil {
return nil, err
}
return &FDOProfile, nil
}
// Create assign an ID to a new FDO Profile and saves it.
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
return service.connection.CreateObjectWithId(
BucketName,
int(FDOProfile.ID),
FDOProfile,
)
}
// Update updates an FDO Profile.
func (service *Service) Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
}
// Delete deletes an FDO Profile.
func (service *Service) Delete(ID portainer.FDOProfileID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -34,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
}
//HelmUserRepository returns an array of all HelmUserRepository
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
func (service *Service) HelmUserRepositorys() ([]portainer.HelmUserRepository, error) {
var repos = make([]portainer.HelmUserRepository, 0)
err := service.connection.GetAll(

View File

@@ -31,7 +31,6 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@@ -74,7 +73,7 @@ type (
EdgeJobService interface {
EdgeJobs() ([]portainer.EdgeJob, error)
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
Create(edgeJob *portainer.EdgeJob) error
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
DeleteEdgeJob(ID portainer.EdgeJobID) error
GetNextIdentifier() int
@@ -85,7 +84,7 @@ type (
EdgeStackService interface {
EdgeStacks() ([]portainer.EdgeStack, error)
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
Create(edgeStack *portainer.EdgeStack) error
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
DeleteEdgeStack(ID portainer.EdgeStackID) error
GetNextIdentifier() int
@@ -123,20 +122,9 @@ type (
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
FDOProfiles() ([]portainer.FDOProfile, error)
FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error)
Create(FDOProfile *portainer.FDOProfile) error
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
Delete(ID portainer.FDOProfileID) error
GetNextIdentifier() int
BucketName() string
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
HelmUserRepositories() ([]portainer.HelmUserRepository, error)
HelmUserRepositorys() ([]portainer.HelmUserRepository, error)
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
Create(record *portainer.HelmUserRepository) error
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error

View File

@@ -4,6 +4,7 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
)
@@ -78,6 +79,9 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
if err == stop {
return resourceControl, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}

View File

@@ -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, true)
_, store, teardown := datastore.MustNewTestStore(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, true)
_, store, teardown := datastore.MustNewTestStore(true)
defer teardown()
staticStack := portainer.Stack{ID: 1}

View File

@@ -63,7 +63,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
logrus.WithField("obj", obj).Errorf("Failed to convert to Team object")
return nil, fmt.Errorf("Failed to convert to Team object: %s", obj)
}
if strings.EqualFold(team.Name, name) {
if strings.EqualFold(t.Name, name) {
t = team
return nil, stop
}

View File

@@ -1,53 +0,0 @@
package tests
import (
"testing"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
func Test_teamByName(t *testing.T) {
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
_, err := store.Team().TeamByName("name")
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
teamBuilder := teamBuilder{
t: t,
store: store,
count: 0,
}
teamBuilder.createNew("name1")
_, err := store.Team().TeamByName("name")
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
teamBuilder := teamBuilder{
t: t,
store: store,
count: 0,
}
expectedTeam := teamBuilder.createNew("name1")
team, err := store.Team().TeamByName("name1")
assert.NoError(t, err, "TeamByName should succeed")
assert.Equal(t, expectedTeam, team)
})
}

View File

@@ -1,28 +0,0 @@
package tests
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
type teamBuilder struct {
t *testing.T
count int
store *datastore.Store
}
func (b *teamBuilder) createNew(name string) *portainer.Team {
b.count++
team := &portainer.Team{
ID: portainer.TeamID(b.count),
Name: name,
}
err := b.store.Team().Create(team)
assert.NoError(b.t, err)
return team
}

View File

@@ -35,7 +35,7 @@ func (store *Store) createBackupFolders() {
}
func (store *Store) databasePath() string {
return store.connection.GetDatabaseFilePath()
return path.Join(store.connection.GetStorePath(), store.connection.GetDatabaseFilename())
}
func (store *Store) commonBackupDir() string {
@@ -69,11 +69,6 @@ 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{}
@@ -89,7 +84,7 @@ func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFilename(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)

View File

@@ -10,7 +10,7 @@ import (
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
connection := store.GetConnection()
@@ -27,7 +27,7 @@ func TestCreateBackupFolders(t *testing.T) {
}
func TestStoreCreation(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
if store == nil {
@@ -40,7 +40,7 @@ func TestStoreCreation(t *testing.T) {
}
func TestBackup(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
connection := store.GetConnection()
defer teardown()
@@ -48,7 +48,7 @@ func TestBackup(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion))
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
@@ -67,7 +67,7 @@ func TestBackup(t *testing.T) {
}
func TestRemoveWithOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(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=%v", err)
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
}
if isFileExist(f.Name()) {

View File

@@ -1,15 +1,10 @@
package datastore
import (
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/portainer/portainer/api/dataservices/errors"
)
func (store *Store) version() (int, error) {
@@ -39,19 +34,6 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
// Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) {
newStore = true
encryptionReq, err := store.connection.NeedsEncryptionMigration()
if err != nil {
return false, err
}
if encryptionReq {
err = store.encryptDB()
if err != nil {
return false, err
}
}
err = store.connection.Open()
if err != nil {
return newStore, err
@@ -63,18 +45,8 @@ func (store *Store) Open() (newStore bool, err error) {
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
version, err := store.VersionService.DBVersion()
if err != nil {
if store.IsErrObjectNotFound(err) {
return newStore, nil
}
return newStore, err
}
if version > 0 {
logrus.WithField("version", version).Infof("Opened existing store")
return false, nil
if _, err := store.VersionService.DBVersion(); err == nil {
newStore = false
}
return newStore, nil
@@ -93,81 +65,16 @@ func (store *Store) BackupTo(w io.Writer) error {
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
return portainerErrors.ErrWrongDBEdition
return errors.ErrWrongDBEdition
}
return nil
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == portainerErrors.ErrObjectNotFound
return e == errors.ErrObjectNotFound
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
}
func (store *Store) encryptDB() error {
store.connection.SetEncrypted(false)
err := store.connection.Open()
if err != nil {
return err
}
err = store.initServices()
if err != nil {
return err
}
// The DB is not currently encrypted. First save the encrypted db filename
oldFilename := store.connection.GetDatabaseFilePath()
logrus.Infof("Encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
logrus.Infof("Exporting database backup to %s", exportFilename)
err = store.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
return err
}
logrus.Infof("Database backup exported")
// Close existing un-encrypted db so that we can delete the file later
store.connection.Close()
// Tell the db layer to create an encrypted db when opened
store.connection.SetEncrypted(true)
store.connection.Open()
// We have to init services before import
err = store.initServices()
if err != nil {
return err
}
err = store.Import(exportFilename)
if err != nil {
// Remove the new encrypted file that we failed to import
os.Remove(store.connection.GetDatabaseFilePath())
logrus.Fatal(portainerErrors.ErrDBImportFailed.Error())
}
err = os.Remove(oldFilename)
if err != nil {
logrus.Errorf("Failed to remove the un-encrypted db file")
}
err = os.Remove(exportFilename)
if err != nil {
logrus.Errorf("Failed to remove the json backup file")
}
// Close db connection
store.connection.Close()
logrus.Info("Database successfully encrypted")
return nil
}

View File

@@ -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, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
testCases := map[string]func(t *testing.T){
@@ -96,6 +96,7 @@ func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, n
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -153,8 +154,7 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
case portainer.EdgeAgentOnKubernetesEnvironment:
cs := chisel.NewService(store, nil)
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
edgeKey, err := cs.GenerateEdgeKey(URL, "", int(id))
is.NoError(err, "GenerateEdgeKey() should not return an error")
edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
expectedEndpoint.EdgeKey = edgeKey
store.testTunnelServer(t)

View File

@@ -7,44 +7,26 @@ import (
// Init creates the default data set.
func (store *Store) Init() error {
err := store.checkOrCreateInstanceID()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSSLSettings()
if err != nil {
return err
}
return store.checkOrCreateDefaultData()
}
func (store *Store) checkOrCreateInstanceID() error {
_, err := store.VersionService.InstanceID()
instanceID, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID := uid.String()
return store.VersionService.StoreInstanceID(instanceID)
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
return err
}
func (store *Store) checkOrCreateDefaultSettings() error {
// TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings()
if store.IsErrObjectNotFound(err) {
defaultSettings := &portainer.Settings{
EnableTelemetry: true,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
@@ -52,16 +34,14 @@ func (store *Store) checkOrCreateDefaultSettings() error {
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
{},
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
{},
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{
SSO: true,
},
SnapshotInterval: portainer.DefaultSnapshotInterval,
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
@@ -70,33 +50,35 @@ func (store *Store) checkOrCreateDefaultSettings() error {
KubectlShellImage: portainer.DefaultKubectlShellImage,
}
return store.SettingsService.UpdateSettings(defaultSettings)
}
if err != nil {
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
} else if err == nil {
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
store.Settings().UpdateSettings(settings)
}
}
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
return store.Settings().UpdateSettings(settings)
}
return nil
}
_, err = store.SSLSettings().Settings()
if err != nil {
if !store.IsErrObjectNotFound(err) {
return err
}
func (store *Store) checkOrCreateDefaultSSLSettings() error {
_, err := store.SSLSettings().Settings()
if store.IsErrObjectNotFound(err) {
defaultSSLSettings := &portainer.SSLSettings{
HTTPEnabled: true,
}
return store.SSLSettings().UpdateSettings(defaultSSLSettings)
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
if err != nil {
return err
}
}
return err
}
func (store *Store) checkOrCreateDefaultData() error {
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -117,5 +99,6 @@ func (store *Store) checkOrCreateDefaultData() error {
return err
}
}
return nil
}

View File

@@ -28,20 +28,10 @@ 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))
}

View File

@@ -9,7 +9,6 @@ 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"
@@ -25,19 +24,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,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
FDOProfilesService: store.FDOProfilesService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
@@ -53,27 +45,7 @@ func (store *Store) MigrateData() error {
AuthorizationService: authorization.NewService(store),
}
// 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
return store.connectionMigrateData(migratorParams)
}
// FailSafeMigrate backup and restore DB if migration fail

View File

@@ -1,19 +1,13 @@
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
@@ -28,32 +22,8 @@ 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 35",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_35.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, true)
newStore, store, teardown := MustNewTestStore(false)
defer teardown()
if !newStore {
@@ -80,7 +50,7 @@ func TestMigrateData(t *testing.T) {
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(true, true)
_, store, teardown := MustNewTestStore(true)
defer teardown()
// Setup data
@@ -105,7 +75,7 @@ func TestMigrateData(t *testing.T) {
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
version := 17
@@ -117,7 +87,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(0)
@@ -131,7 +101,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, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreIsUpdating(true)
@@ -146,7 +116,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, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.MigrateData()
@@ -161,7 +131,7 @@ func TestMigrateData(t *testing.T) {
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
@@ -180,7 +150,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, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(version)
@@ -215,250 +185,3 @@ 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
}

View File

@@ -33,7 +33,7 @@ func setup(store *Store) error {
}
func TestMigrateSettings(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
err := setup(store)

View File

@@ -10,7 +10,7 @@ import (
)
func TestMigrateStackEntryPoint(t *testing.T) {
_, store, teardown := MustNewTestStore(false, true)
_, store, teardown := MustNewTestStore(false)
defer teardown()
stackService := store.Stack()

View File

@@ -1,38 +1,16 @@
package migrator
import (
"errors"
"reflect"
"runtime"
"fmt"
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
@@ -41,90 +19,175 @@ func (m *Migrator) Migrate() error {
return migrationError(err, "StoreIsUpdating")
}
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),
if m.currentDBVersion < 17 {
return migrationError(err, "migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
var lastDbVersion int
for _, migration := range migrations {
if m.currentDBVersion < migration.dbversion {
// Print the next line only when the version changes
if migration.dbversion > lastDbVersion {
migrateLog.Infof("Migrating DB to version %d", migration.dbversion)
}
err := migration.migrate()
if err != nil {
return migrationError(err, GetFunctionName(migration.migrate))
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return migrationError(err, "updateUsersToDBVersion18")
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion18")
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion19")
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return migrationError(err, "updateUsersToDBVersion20")
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion20")
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return migrationError(err, "updateSchedulesToDBVersion20")
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion22")
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return migrationError(err, "updateUsersAndRolesToDBVersion22")
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
migrateLog.Info("Migrating to DB 23")
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
migrateLog.Info("Migrating to DB 24")
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
migrateLog.Info("Migrating to DB 25")
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
}
err = m.updateStacksToDB24()
if err != nil {
return migrationError(err, "updateStacksToDB24")
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
migrateLog.Info("Migrating to DB 26")
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
migrateLog.Info("Migrating to DB 27")
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
migrateLog.Info("Migrating to DB 30")
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return migrationError(err, "migrateDBVersionToDB32")
}
}
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
migrateLog.Info("Migrating to DB 33")
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
migrateLog.Info("Migrating to DB 34")
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
migrateLog.Info("Migrating to DB 35")
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
}
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.Infof("Updated DB version to %d", portainer.DBVersion)
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
// reset DB updating status
return m.versionService.StoreIsUpdating(false)

View File

@@ -5,7 +5,6 @@ import (
)
func (m *Migrator) updateUsersToDBVersion18() error {
migrateLog.Info("- updating users")
legacyUsers, err := m.userService.Users()
if err != nil {
return err
@@ -40,7 +39,6 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
migrateLog.Info("- updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -71,7 +69,6 @@ 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
@@ -102,7 +99,6 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
migrateLog.Info("- updating registries")
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err

View File

@@ -3,7 +3,6 @@ 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

View File

@@ -7,7 +7,6 @@ import (
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
migrateLog.Info("- updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
@@ -23,7 +22,6 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
migrateLog.Info("- updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -6,7 +6,6 @@ import (
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
migrateLog.Info("- updating resource controls")
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
@@ -25,7 +24,6 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,8 @@ package migrator
import (
"fmt"
"log"
"github.com/portainer/portainer/api/dataservices/errors"
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
@@ -12,24 +11,29 @@ import (
)
func (m *Migrator) migrateDBVersionToDB32() error {
migrateLog.Info("Updating registries")
err := m.updateRegistriesToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating dockerhub")
err = m.updateDockerhubToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating resource controls")
if err := m.updateVolumeResourceControlToDB32(); err != nil {
return err
}
migrateLog.Info("Updating kubeconfig expiry")
if err := m.kubeconfigExpiryToDB32(); err != nil {
return err
}
migrateLog.Info("Setting default helm repository url")
if err := m.helmRepositoryURLToDB32(); err != nil {
return err
}
@@ -38,7 +42,6 @@ func (m *Migrator) migrateDBVersionToDB32() error {
}
func (m *Migrator) updateRegistriesToDB32() error {
migrateLog.Info("- updating registries")
registries, err := m.registryService.Registries()
if err != nil {
return err
@@ -81,7 +84,6 @@ 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
@@ -170,7 +172,6 @@ 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)
@@ -263,7 +264,6 @@ 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,7 +273,6 @@ 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

View File

@@ -1,11 +1,8 @@
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
}
@@ -19,7 +16,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)
}

View File

@@ -1,11 +1,9 @@
package migrator
import (
"github.com/portainer/portainer/api/dataservices"
)
import "github.com/portainer/portainer/api/dataservices"
func (m *Migrator) migrateDBVersionToDB34() error {
migrateLog.Info("- updating stacks")
migrateLog.Info("Migrating stacks")
err := MigrateStackEntryPoint(m.stackService)
if err != nil {
return err

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
@@ -27,12 +26,12 @@ var migrateLog = plog.NewScopedLog("database, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
currentDBVersion int
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@@ -55,7 +54,6 @@ type (
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@@ -80,7 +78,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,

View File

@@ -17,7 +17,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
@@ -51,7 +50,6 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@@ -131,12 +129,6 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@@ -265,11 +257,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
@@ -369,7 +356,6 @@ type storeExport struct {
User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
@@ -377,184 +363,118 @@ func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().CustomTemplates(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Custom Templates")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.CustomTemplate = c
}
if e, err := store.EdgeGroup().EdgeGroups(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Groups")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeGroup = e
}
if e, err := store.EdgeJob().EdgeJobs(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Jobs")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeJob = e
}
if e, err := store.EdgeStack().EdgeStacks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Stacks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeStack = e
}
if e, err := store.Endpoint().Endpoints(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoints")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Endpoint = e
}
if e, err := store.EndpointGroup().EndpointGroups(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Groups")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointGroup = e
}
if r, err := store.EndpointRelation().EndpointRelations(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Relations")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointRelation = r
}
if r, err := store.ExtensionService.Extensions(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Extensions")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Extensions = r
}
if r, err := store.HelmUserRepository().HelmUserRepositories(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Helm User Repositories")
}
if r, err := store.HelmUserRepository().HelmUserRepositorys(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.HelmUserRepository = r
}
if r, err := store.Registry().Registries(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Registries")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Registry = r
}
if c, err := store.ResourceControl().ResourceControls(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Resource Controls")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.ResourceControl = c
}
if role, err := store.Role().Roles(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Roles")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Role = role
}
if r, err := store.ScheduleService.Schedules(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Schedules")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Schedules = r
}
if settings, err := store.Settings().Settings(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Settings")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Settings = *settings
}
if settings, err := store.SSLSettings().Settings(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting SSL Settings")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.SSLSettings = *settings
}
if t, err := store.Stack().Stacks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Stacks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Stack = t
}
if t, err := store.Tag().Tags(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tags")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Tag = t
}
if t, err := store.TeamMembership().TeamMemberships(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Team Memberships")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TeamMembership = t
}
if t, err := store.Team().Teams(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Teams")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Team = t
}
if info, err := store.TunnelServer().Info(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tunnel Server")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TunnelServer = *info
}
if users, err := store.User().Users(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Users")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.User = users
}
if webhooks, err := store.Webhook().Webhooks(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Webhooks")
}
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Webhook = webhooks
}
v, err := store.Version().DBVersion()
if err != nil && !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting DB version")
if err != nil {
logrus.WithError(err).Debugf("Export boom")
}
instance, _ := store.Version().InstanceID()
backup.Version = map[string]string{
@@ -562,11 +482,6 @@ func (store *Store) Export(filename string) (err error) {
"INSTANCE_ID": instance,
}
backup.Metadata, err = store.connection.BackupMetadata()
if err != nil {
logrus.WithError(err).Errorf("Exporting Metadata")
}
b, err := json.MarshalIndent(backup, "", " ")
if err != nil {
return err
@@ -575,7 +490,6 @@ func (store *Store) Export(filename string) (err error) {
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
@@ -604,66 +518,50 @@ func (store *Store) Import(filename string) (err error) {
for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)
}
for _, v := range backup.EdgeGroup {
store.EdgeGroup().UpdateEdgeGroup(v.ID, &v)
}
for _, v := range backup.EdgeJob {
store.EdgeJob().UpdateEdgeJob(v.ID, &v)
}
for _, v := range backup.EdgeStack {
store.EdgeStack().UpdateEdgeStack(v.ID, &v)
}
for _, v := range backup.Endpoint {
store.Endpoint().UpdateEndpoint(v.ID, &v)
}
for _, v := range backup.EndpointGroup {
store.EndpointGroup().UpdateEndpointGroup(v.ID, &v)
}
for _, v := range backup.EndpointRelation {
store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v)
}
for _, v := range backup.HelmUserRepository {
store.HelmUserRepository().UpdateHelmUserRepository(v.ID, &v)
}
for _, v := range backup.Registry {
store.Registry().UpdateRegistry(v.ID, &v)
}
for _, v := range backup.ResourceControl {
store.ResourceControl().UpdateResourceControl(v.ID, &v)
}
for _, v := range backup.Role {
store.Role().UpdateRole(v.ID, &v)
}
store.Settings().UpdateSettings(&backup.Settings)
store.SSLSettings().UpdateSettings(&backup.SSLSettings)
for _, v := range backup.Stack {
store.Stack().UpdateStack(v.ID, &v)
}
for _, v := range backup.Tag {
store.Tag().UpdateTag(v.ID, &v)
}
for _, v := range backup.TeamMembership {
store.TeamMembership().UpdateTeamMembership(v.ID, &v)
}
for _, v := range backup.Team {
store.Team().UpdateTeam(v.ID, &v)
}
store.TunnelServer().UpdateInfo(&backup.TunnelServer)
for _, user := range backup.User {
@@ -672,9 +570,10 @@ func (store *Store) Import(filename string) (err error) {
}
}
for _, v := range backup.Webhook {
store.Webhook().UpdateWebhook(v.ID, &v)
}
// backup[store.Webhook().BucketName()], err = store.Webhook().Webhooks()
// if err != nil {
// logrus.WithError(err).Debugf("Export boom")
// }
return store.connection.RestoreMetadata(backup.Metadata)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,808 +0,0 @@
{
"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": "",
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"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": null,
"Networks": null,
"Version": null,
"Volumes": null
},
"DockerVersion": "20.10.13",
"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,
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
"EnableHostManagementFeatures": false,
"EnableTelemetry": true,
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
"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": "",
"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": "",
"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": "",
"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": "35",
"INSTANCE_ID": "null"
}
}

View File

@@ -18,8 +18,8 @@ func (store *Store) GetConnection() portainer.Connection {
return store.connection
}
func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init, secure)
func MustNewTestStore(init bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
@@ -30,7 +30,7 @@ func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
return newStore, store, teardown
}
func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
func NewTestStore(init bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
storePath, err := ioutil.TempDir("", "test-store")
if err != nil {
@@ -42,12 +42,7 @@ func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
return false, nil, nil, err
}
secretKey := []byte("apassphrasewhichneedstobe32bytes")
if !secure {
secretKey = nil
}
connection, err := database.NewDatabase("boltdb", storePath, secretKey)
connection, err := database.NewDatabase("boltdb", storePath)
if err != nil {
panic(err)
}

View File

@@ -1,15 +0,0 @@
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)
}

View File

@@ -1,61 +0,0 @@
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)
}
})
}
}

View File

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

View File

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

View File

@@ -1,79 +0,0 @@
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")
}

View File

@@ -15,7 +15,7 @@ import (
var errUnsupportedEnvironmentType = errors.New("Environment not supported")
const (
defaultDockerRequestTimeout = 60 * time.Second
defaultDockerRequestTimeout = 60
dockerClientVersion = "1.37"
)
@@ -33,23 +33,22 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
}
}
// CreateClient is a generic function to create a Docker client based on
// createClient is a generic function to create a Docker client based on
// a specific environment(endpoint) configuration. The nodeName parameter can be used
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
// The underlying http client timeout may be specified, a default value is used otherwise.
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
return nil, errUnsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
return createAgentClient(endpoint, factory.signatureService, nodeName)
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName)
}
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return createLocalClient(endpoint)
}
return createTCPClient(endpoint, timeout)
return createTCPClient(endpoint)
}
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
@@ -59,8 +58,8 @@ func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
)
}
func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) {
httpCli, err := httpClient(endpoint, timeout)
func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
@@ -72,8 +71,8 @@ func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*cli
)
}
func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, nodeName string, timeout *time.Duration) (*client.Client, error) {
httpCli, err := httpClient(endpoint, timeout)
func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
@@ -96,7 +95,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
if err != nil {
return nil, err
}
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
return client.NewClientWithOpts(
@@ -107,8 +106,8 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
)
}
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string, timeout *time.Duration) (*client.Client, error) {
httpCli, err := httpClient(endpoint, timeout)
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
httpCli, err := httpClient(endpoint)
if err != nil {
return nil, err
}
@@ -135,7 +134,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
)
}
func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) {
func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
@@ -146,13 +145,8 @@ func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Cli
transport.TLSClientConfig = tlsConfig
}
clientTimeout := defaultDockerRequestTimeout
if timeout != nil {
clientTimeout = *timeout
}
return &http.Client{
Transport: transport,
Timeout: clientTimeout,
Timeout: defaultDockerRequestTimeout * time.Second,
}, nil
}

View File

@@ -26,7 +26,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
// CreateSnapshot creates a snapshot of a specific Docker environment(endpoint)
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "", nil)
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
if err != nil {
return nil, err
}

View File

@@ -33,7 +33,7 @@ func (s StringSet) List() []string {
list := make([]string, s.Len())
i := 0
for k := range s {
for k, _ := range s {
list[i] = k
i++
}

View File

@@ -46,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -62,7 +62,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
}
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, envFilePath)
return errors.Wrap(err, "failed to deploy a stack")
}

View File

@@ -50,7 +50,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, false)
err = w.Up(ctx, stack, endpoint)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}

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