Compare commits
13 Commits
debug-api-
...
feat/EE-24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df0a28a42 | ||
|
|
1723ca1481 | ||
|
|
0ea1739bca | ||
|
|
4f0e09fad8 | ||
|
|
428d2a03bf | ||
|
|
82a13560b3 | ||
|
|
6f354833b7 | ||
|
|
ad5ece52b1 | ||
|
|
7ddd02bf91 | ||
|
|
71c8932f76 | ||
|
|
eab2ae6230 | ||
|
|
133bd15dad | ||
|
|
b93a11b4e5 |
@@ -1,5 +1,3 @@
|
||||
*
|
||||
!dist
|
||||
!build
|
||||
!metadata.json
|
||||
!docker-extension/build
|
||||
|
||||
17
.github/workflows/lint.yml
vendored
17
.github/workflows/lint.yml
vendored
@@ -18,12 +18,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
node-version: 12
|
||||
|
||||
# ESLint and Prettier must be in `package.json`
|
||||
- name: Install Node.js dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
@@ -34,5 +39,3 @@ jobs:
|
||||
prettier_dir: app/
|
||||
gofmt: true
|
||||
gofmt_dir: api/
|
||||
- name: Typecheck
|
||||
uses: icrawl/action-tsc@v1
|
||||
|
||||
230
.github/workflows/nightly-security-scan.yml
vendored
230
.github/workflows/nightly-security-scan.yml
vendored
@@ -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
|
||||
233
.github/workflows/pr-security.yml
vendored
233
.github/workflows/pr-security.yml
vendored
@@ -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
|
||||
11
.github/workflows/test-client.yaml
vendored
Normal file
11
.github/workflows/test-client.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: Test Frontend
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
29
.github/workflows/test.yaml
vendored
29
.github/workflows/test.yaml
vendored
@@ -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 ./...
|
||||
@@ -1,2 +1 @@
|
||||
dist
|
||||
api/datastore/test_data
|
||||
dist
|
||||
@@ -16,9 +16,6 @@ module.exports = {
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -181,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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,11 +47,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
HTTPDisabled: kingpin.Flag("http-disabled", "Serve portainer only on https").Default(defaultHTTPDisabled).Bool(),
|
||||
HTTPEnabled: kingpin.Flag("http-enabled", "Serve portainer on http").Default(defaultHTTPEnabled).Bool(),
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
SSLCACert: kingpin.Flag("sslcacert", "Path to the SSL CA certificate used to validate the edge agent cert").Default(defaultSSLCACertPath).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(),
|
||||
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(),
|
||||
|
||||
@@ -18,6 +18,9 @@ const (
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSSLCACertPath = "/certs/portainer-ca.crt"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
)
|
||||
|
||||
@@ -15,6 +15,9 @@ const (
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCACertPath = "C:\\certs\\portainer-ca.crt"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type portainerFormatter struct {
|
||||
logrus.TextFormatter
|
||||
}
|
||||
|
||||
func configureLogger() {
|
||||
logger := logrus.New() // logger is to implicitly substitute stdlib's log
|
||||
log.SetOutput(logger.Writer())
|
||||
|
||||
@@ -208,7 +208,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, certPath, keyPath, caCertPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
||||
slices := strings.Split(addr, ":")
|
||||
host := slices[0]
|
||||
if host == "" {
|
||||
@@ -217,7 +217,7 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
|
||||
|
||||
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
|
||||
|
||||
err := sslService.Init(host, certPath, keyPath)
|
||||
err := sslService.Init(host, certPath, keyPath, caCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -278,12 +278,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
|
||||
@@ -574,7 +568,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
cryptoService := initCryptoService()
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, *flags.SSLCACert, fileService, dataStore, shutdownTrigger)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@@ -605,7 +599,7 @@ 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)
|
||||
|
||||
@@ -712,7 +706,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
OpenAMTService: openAMTService,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
KubeClusterAccessService: kubeClusterAccessService,
|
||||
KubeConfigService: kubeConfigService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSLService: sslService,
|
||||
@@ -722,6 +716,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
BaseURL: *flags.BaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -76,7 +76,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
|
||||
|
||||
@@ -29,7 +29,6 @@ type Connection interface {
|
||||
GetNextIdentifier(bucketName string) int
|
||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
||||
CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error
|
||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
|
||||
@@ -9,11 +9,11 @@ type Service struct{}
|
||||
|
||||
// Hash hashes a string using the bcrypt algorithm
|
||||
func (*Service) Hash(data string) (string, error) {
|
||||
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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||
func CreateServerTLSConfiguration() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -161,7 +161,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
|
||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
||||
}
|
||||
|
||||
b, err := connection.ExportJson(databasePath, true)
|
||||
b, err := connection.exportJson(databasePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,7 +181,10 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
|
||||
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -311,19 +314,6 @@ 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 {
|
||||
@@ -416,10 +406,7 @@ func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error
|
||||
}
|
||||
|
||||
err = connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
return bucket.SetSequence(uint64(id))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,34 +4,13 @@ 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) {
|
||||
func (c *DbConnection) exportJson(databasePath string) ([]byte, error) {
|
||||
logrus.WithField("databasePath", databasePath).Infof("exportJson")
|
||||
|
||||
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
|
||||
@@ -41,13 +20,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 {
|
||||
@@ -73,20 +45,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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -84,15 +83,6 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||
return service.connection.CreateObjectWithSetSequence(BucketName, int(endpoint.ID), endpoint)
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service *Service) CreateWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
|
||||
if endpoint.ID > 0 {
|
||||
return errors.New("the endpoint must not have an ID")
|
||||
}
|
||||
|
||||
return service.connection.CreateObject(BucketName, fn)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.connection.GetNextIdentifier(BucketName)
|
||||
|
||||
@@ -74,7 +74,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
|
||||
@@ -97,7 +97,6 @@ type (
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
Endpoints() ([]portainer.Endpoint, error)
|
||||
Create(endpoint *portainer.Endpoint) error
|
||||
CreateWithCallback(endpoint *portainer.Endpoint, fn func(uint64) (int, interface{})) error
|
||||
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
||||
DeleteEndpoint(ID portainer.EndpointID) error
|
||||
GetNextIdentifier() int
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
@@ -15,30 +13,6 @@ const (
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
cache *portainer.Settings
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func cloneSettings(src *portainer.Settings) *portainer.Settings {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := *src
|
||||
|
||||
if c.BlackListedLabels != nil {
|
||||
c.BlackListedLabels = make([]portainer.Pair, len(src.BlackListedLabels))
|
||||
copy(c.BlackListedLabels, src.BlackListedLabels)
|
||||
}
|
||||
|
||||
if src.FeatureFlagSettings != nil {
|
||||
c.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
for k, v := range src.FeatureFlagSettings {
|
||||
c.FeatureFlagSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -59,18 +33,6 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
service.mu.RLock()
|
||||
if service.cache != nil {
|
||||
s := cloneSettings(service.cache)
|
||||
service.mu.RUnlock()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
service.mu.RUnlock()
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
var settings portainer.Settings
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||
@@ -78,24 +40,12 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service.cache = cloneSettings(&settings)
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a Settings object.
|
||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
err := service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.cache = cloneSettings(settings)
|
||||
|
||||
return nil
|
||||
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
|
||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
@@ -111,9 +61,3 @@ func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *Service) InvalidateCache() {
|
||||
service.mu.Lock()
|
||||
service.cache = nil
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func Test_teamByName(t *testing.T) {
|
||||
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
_, err := store.Team().TeamByName("name")
|
||||
@@ -19,7 +19,7 @@ func Test_teamByName(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
@@ -35,7 +35,7 @@ func Test_teamByName(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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){
|
||||
@@ -177,7 +177,7 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
|
||||
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: id,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
store.EndpointRelation().Create(relation)
|
||||
|
||||
@@ -22,18 +22,23 @@ func (store *Store) Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.checkOrCreateDefaultData()
|
||||
err = store.checkOrCreateDefaultData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
instanceID = uid.String()
|
||||
return store.VersionService.StoreInstanceID(instanceID)
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,14 +24,6 @@ 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")
|
||||
}
|
||||
|
||||
store.SettingsService.InvalidateCache()
|
||||
|
||||
migratorParams := &migrator.MigratorParameters{
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
@@ -55,27 +46,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(false, true)
|
||||
_, store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
stackService := store.Stack()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -54,7 +54,7 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
|
||||
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err = m.endpointRelationService.Create(relation)
|
||||
|
||||
@@ -3,7 +3,7 @@ package migrator
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
migrateLog.Info("- updating Settings")
|
||||
migrateLog.Info("Updating Settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
@@ -18,7 +18,7 @@ func (m *Migrator) updateSettingsToDB24() error {
|
||||
}
|
||||
|
||||
func (m *Migrator) updateStacksToDB24() error {
|
||||
migrateLog.Info("- updating stacks")
|
||||
migrateLog.Info("Updating stacks")
|
||||
stacks, err := m.stackService.Stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB25() error {
|
||||
migrateLog.Info("- updating settings")
|
||||
migrateLog.Info("Updating settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
migrateLog.Info("- updating endpoint settings")
|
||||
migrateLog.Info("Updating endpoint settings")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
migrateLog.Info("- updating stack resource controls")
|
||||
migrateLog.Info("Updating stack resource controls")
|
||||
resourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB30() error {
|
||||
migrateLog.Info("- updating legacy settings")
|
||||
migrateLog.Info("Updating legacy settings")
|
||||
if err := m.MigrateSettingsToDB30(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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, []byte("apassphrasewhichneedstobe32bytes"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package exec
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -124,8 +123,6 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
output, err := cmd.Output()
|
||||
|
||||
@@ -56,12 +56,12 @@ const (
|
||||
TempPath = "tmp"
|
||||
// SSLCertPath represents the default ssl certificates path
|
||||
SSLCertPath = "certs"
|
||||
// SSLCertFilename represents the ssl certificate file name
|
||||
SSLCertFilename = "cert.pem"
|
||||
// SSLKeyFilename represents the ssl key file name
|
||||
SSLKeyFilename = "key.pem"
|
||||
// SSLCACertFilename represents the CA ssl certificate file name for mTLS
|
||||
SSLCACertFilename = "ca-cert.pem"
|
||||
// DefaultSSLCertFilename represents the default ssl certificate file name
|
||||
DefaultSSLCertFilename = "cert.pem"
|
||||
// DefaultSSLKeyFilename represents the default ssl key file name
|
||||
DefaultSSLKeyFilename = "key.pem"
|
||||
// DefaultSSLCACertFilename represents the default CA ssl certificate file name for mTLS
|
||||
DefaultSSLCACertFilename = "ca-cert.pem"
|
||||
)
|
||||
|
||||
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
|
||||
@@ -354,6 +354,9 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
|
||||
|
||||
// GetFileContent returns the content of a file as bytes.
|
||||
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
|
||||
if trustedRoot == "" {
|
||||
trustedRoot = "/"
|
||||
}
|
||||
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
|
||||
if err != nil {
|
||||
if filePath == "" {
|
||||
@@ -582,8 +585,8 @@ func (service *Service) wrapFileStore(filepath string) string {
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := JoinPaths(SSLCertPath, SSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, SSLKeyFilename)
|
||||
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
@@ -629,16 +632,23 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
|
||||
return defCertPath, defKeyPath, nil
|
||||
}
|
||||
|
||||
// CopySSLCACert copies the specified caCert pem file
|
||||
func (service *Service) CopySSLCACert(caCertPath string) (string, error) {
|
||||
toFilePath := service.wrapFileStore(JoinPaths(SSLCertPath, SSLCACertFilename))
|
||||
// GetDefaultSSLCACertsPath returns the ssl caCert path
|
||||
func (service *Service) GetDefaultSSLCACertsPath() string {
|
||||
caCertPath := JoinPaths(SSLCertPath, DefaultSSLCACertFilename)
|
||||
|
||||
err := service.Copy(caCertPath, toFilePath, true)
|
||||
return service.wrapFileStore(caCertPath)
|
||||
}
|
||||
|
||||
// CopySSLCACert copies the specified caCert pem file
|
||||
func (service *Service) CopySSLCACert(cacertPath string) (string, error) {
|
||||
defCacertPath := service.GetDefaultSSLCACertsPath()
|
||||
|
||||
err := service.Copy(cacertPath, defCacertPath, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return toFilePath, nil
|
||||
return defCacertPath, nil
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WriteToFile creates a file in the filesystem storage
|
||||
func WriteToFile(dst string, content []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
|
||||
|
||||
92
api/go.mod
92
api/go.mod
@@ -3,57 +3,59 @@ module github.com/portainer/portainer/api
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1
|
||||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/gops v0.3.22
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220407011010-3c7408969ad3
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220225003350-cec58db3549e
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
github.com/swaggo/swag v1.7.8
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
k8s.io/api v0.22.5
|
||||
k8s.io/apimachinery v0.22.5
|
||||
k8s.io/client-go v0.22.5
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
|
||||
@@ -61,43 +63,54 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/containerd/containerd v1.6.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/ansi v1.0.2 // indirect
|
||||
github.com/jpillora/requestlog v1.0.0 // indirect
|
||||
github.com/jpillora/sizestr v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
|
||||
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
|
||||
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.10.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
|
||||
@@ -106,21 +119,22 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
golang.org/x/tools v0.1.7 // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
|
||||
google.golang.org/grpc v1.33.2 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
|
||||
k8s.io/klog/v2 v2.9.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
|
||||
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
562
api/go.sum
562
api/go.sum
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/passwordutils"
|
||||
)
|
||||
|
||||
type authenticatePayload struct {
|
||||
@@ -101,8 +100,7 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
forceChangePassword := !passwordutils.StrengthCheck(password)
|
||||
return handler.writeToken(w, user, forceChangePassword)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
@@ -133,11 +131,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user, false)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, forceChangePassword bool) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user, forceChangePassword)
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := composeTokenData(user)
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
@@ -208,11 +206,10 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
|
||||
return false
|
||||
}
|
||||
|
||||
func composeTokenData(user *portainer.User, forceChangePassword bool) *portainer.TokenData {
|
||||
func composeTokenData(user *portainer.User) *portainer.TokenData {
|
||||
return &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
ForceChangePassword: forceChangePassword,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user, false)
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
@@ -272,19 +271,14 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isValidProject := true
|
||||
defer func() {
|
||||
if !isValidProject {
|
||||
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
|
||||
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
|
||||
|
||||
exists, err := handler.FileService.FileExists(entryPath)
|
||||
if err != nil || !exists {
|
||||
isValidProject = false
|
||||
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
|
||||
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -295,16 +289,6 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
|
||||
}
|
||||
|
||||
info, err := os.Lstat(entryPath)
|
||||
if err != nil {
|
||||
isValidProject = false
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
|
||||
isValidProject = false
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file is not a symbolic link")
|
||||
}
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -164,9 +164,7 @@ func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
|
||||
edgeStackSet[edgeStackID] = true
|
||||
}
|
||||
|
||||
for edgeStackID := range edgeStackSet {
|
||||
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
|
||||
}
|
||||
relation.EdgeStacks = edgeStackSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Environments", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid environments")
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
}
|
||||
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob)
|
||||
}
|
||||
|
||||
func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeJobEndpointMeta {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
continue
|
||||
}
|
||||
|
||||
if meta, exists := edgeJob.Endpoints[endpointID]; exists {
|
||||
if meta, ok := edgeJob.Endpoints[endpointID]; ok {
|
||||
endpointsMap[endpointID] = meta
|
||||
} else {
|
||||
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
|
||||
@@ -103,19 +103,13 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
|
||||
updateVersion := false
|
||||
if payload.CronExpression != nil && *payload.CronExpression != edgeJob.CronExpression {
|
||||
if payload.CronExpression != nil {
|
||||
edgeJob.CronExpression = *payload.CronExpression
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.FileContent != nil && *payload.FileContent != string(fileContent) {
|
||||
fileContent = []byte(*payload.FileContent)
|
||||
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), fileContent)
|
||||
if payload.FileContent != nil {
|
||||
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), []byte(*payload.FileContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,7 +117,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if payload.Recurring != nil && *payload.Recurring != edgeJob.Recurring {
|
||||
if payload.Recurring != nil {
|
||||
edgeJob.Recurring = *payload.Recurring
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
@@ -227,6 +228,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por
|
||||
Name: payload.Name,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
DeploymentType: payload.DeploymentType,
|
||||
Version: 1,
|
||||
}
|
||||
@@ -335,6 +337,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai
|
||||
DeploymentType: payload.DeploymentType,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
@@ -408,7 +411,7 @@ func updateEndpointRelations(endpointRelationService dataservices.EndpointRelati
|
||||
return fmt.Errorf("unable to find endpoint relation in database: %w", err)
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStackID] = portainer.EdgeStackStatus{}
|
||||
relation.EdgeStacks[edgeStackID] = true
|
||||
|
||||
err = endpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package edgestacks
|
||||
|
||||
/*
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
|
||||
edgeStackID := portainer.EdgeStackID(5)
|
||||
endpointRelations := []portainer.EndpointRelation{
|
||||
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{}},
|
||||
{EndpointID: 1, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 2, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 3, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 4, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
{EndpointID: 5, EdgeStacks: map[portainer.EdgeStackID]bool{}},
|
||||
}
|
||||
|
||||
relatedIds := []portainer.EndpointID{2, 3}
|
||||
@@ -29,4 +36,3 @@ func Test_updateEndpointRelation_successfulRuns(t *testing.T) {
|
||||
assert.Equal(t, shouldBeRelated, relation.EdgeStacks[edgeStackID])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// @id EdgeStackList
|
||||
@@ -26,35 +25,5 @@ func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
|
||||
}
|
||||
|
||||
endpointRels, err := handler.DataStore.EndpointRelation().EndpointRelations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint relations from the database", err}
|
||||
}
|
||||
|
||||
m := make(map[portainer.EdgeStackID]map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
|
||||
for _, r := range endpointRels {
|
||||
for edgeStackID, status := range r.EdgeStacks {
|
||||
if m[edgeStackID] == nil {
|
||||
m[edgeStackID] = make(map[portainer.EndpointID]portainer.EdgeStackStatus)
|
||||
}
|
||||
|
||||
m[edgeStackID][r.EndpointID] = status
|
||||
}
|
||||
}
|
||||
|
||||
type EdgeStackWithStatus struct {
|
||||
portainer.EdgeStack
|
||||
Status map[portainer.EndpointID]portainer.EdgeStackStatus
|
||||
}
|
||||
|
||||
var edgeStacksWS []EdgeStackWithStatus
|
||||
for _, s := range edgeStacks {
|
||||
edgeStacksWS = append(edgeStacksWS, EdgeStackWithStatus{
|
||||
EdgeStack: s,
|
||||
Status: m[s.ID],
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStacksWS)
|
||||
return response.JSON(w, edgeStacks)
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
|
||||
httpErr := &httperror.HandlerError{http.StatusInternalServerError, msg, err}
|
||||
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
httpErr.StatusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
return httpErr
|
||||
}
|
||||
|
||||
// @id EdgeStackStatusDelete
|
||||
// @summary Delete an EdgeStack status
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 404
|
||||
// @failure 403
|
||||
// @router /edge_stacks/{id}/status/{endpoint_id} [delete]
|
||||
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a valid endpoint from the handler context", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err != nil {
|
||||
return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
@@ -49,6 +49,13 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload updateStatusPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
@@ -67,28 +74,17 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment relations", err}
|
||||
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
Type: *payload.Status,
|
||||
Error: payload.Error,
|
||||
EndpointID: *payload.EndpointID,
|
||||
}
|
||||
|
||||
endpointRelation.EdgeStacks[portainer.EdgeStackID(stackID)] = portainer.EdgeStackStatus{
|
||||
Type: *payload.Status,
|
||||
Error: payload.Error,
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -82,8 +80,8 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related environments from database", err}
|
||||
}
|
||||
|
||||
oldRelatedSet := endpointutils.EndpointSet(relatedEndpointIds)
|
||||
newRelatedSet := endpointutils.EndpointSet(newRelated)
|
||||
oldRelatedSet := EndpointSet(relatedEndpointIds)
|
||||
newRelatedSet := EndpointSet(newRelated)
|
||||
|
||||
endpointsToRemove := map[portainer.EndpointID]bool{}
|
||||
for endpointID := range oldRelatedSet {
|
||||
@@ -119,7 +117,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find environment relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[stack.ID] = portainer.EdgeStackStatus{}
|
||||
relation.EdgeStacks[stack.ID] = true
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
@@ -181,6 +179,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if payload.Version != nil && *payload.Version != stack.Version {
|
||||
stack.Version = *payload.Version
|
||||
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
@@ -190,3 +189,13 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
|
||||
set := map[portainer.EndpointID]bool{}
|
||||
|
||||
for _, endpointID := range endpointIDs {
|
||||
set[endpointID] = true
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -25,11 +24,10 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
DataStore: dataStore,
|
||||
}
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
@@ -45,12 +43,6 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
|
||||
edgeStackStatusRouter := h.NewRoute().Subrouter()
|
||||
edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id"))
|
||||
|
||||
edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
286
api/http/handler/endpointedge/async.go
Normal file
286
api/http/handler/endpointedge/async.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6902
|
||||
// Doing this manually, because at this point, i don't want to marshal to json, make a diff - for now, just using 'add' (as its really an upsert)
|
||||
type JSONPatch struct {
|
||||
Operation string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// TODO: copied from edgestack_status_update
|
||||
type updateStatusPayload struct {
|
||||
Error string
|
||||
Status *portainer.EdgeStackStatusType
|
||||
EndpointID *portainer.EndpointID
|
||||
}
|
||||
|
||||
// An empty request ~~ just a ping.
|
||||
type Snapshot struct {
|
||||
Docker *portainer.DockerSnapshot
|
||||
Kubernetes *portainer.KubernetesSnapshot
|
||||
}
|
||||
type AsyncRequest struct {
|
||||
CommandId string `json: optional`
|
||||
Snapshot *Snapshot `json: optional` // todo
|
||||
StackStatus map[portainer.EdgeStackID]updateStatusPayload
|
||||
}
|
||||
|
||||
func (payload *AsyncRequest) Validate(r *http.Request) error {
|
||||
// TODO:
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AsyncResponse struct {
|
||||
CommandInterval time.Duration `json: optional`
|
||||
PingInterval time.Duration `json: optional`
|
||||
SnapshotInterval time.Duration `json: optional`
|
||||
|
||||
ServerCommandId string // should be easy to detect if its larger / smaller: this is the response that tells the agent there are new commands waiting for it
|
||||
SendDiffSnapshotTime time.Time `json: optional` // might be optional
|
||||
Commands []JSONPatch `json: optional` // todo
|
||||
}
|
||||
|
||||
// for testing with mTLS..:
|
||||
//sven@p1:~/src/portainer/portainer$ curl -k --cacert ~/.config/portainer/certs/ca.pem --cert ~/.config/portainer/certs/agent-cert.pem --key ~/.config/portainer/certs/agent-key.pem -X POST --header "X-PortainerAgent-EdgeID: 7e2b0143-c511-43c3-844c-a7a91ab0bedc" --data '{"CommandId": "okok", "Snapshot": {}}' https://p1:9443/api/endpoints/edge/async/
|
||||
//{"CommandInterval":0,"PingInterval":0,"SnapshotInterval":0,"ServerCommandId":"8888","SendDiffSnapshotTime":"0001-01-01T00:00:00Z","Commands":{}}
|
||||
|
||||
// @id endpointAsync
|
||||
// @summary Get environment(endpoint) status
|
||||
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
|
||||
// @description **Access policy**: restricted only to Edge environments(endpoints) TODO: with mTLS cert....
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 {object} AsyncResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access environment(endpoint)"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/edge/async/ [post]
|
||||
func (handler *Handler) endpointAsync(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// TODO: get endpointID from the mTLS cert info
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
if edgeIdentifier == "" {
|
||||
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).Debug("missing agent edge id")
|
||||
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "missing Edge identifier", errors.New("missing Edge identifier")}
|
||||
}
|
||||
|
||||
// TODO: if the mTLS certs are valid, and we don't have a matching environment registered, CREATE IT (and maybe untrusted...)
|
||||
endpoint, err := handler.getEdgeEndpoint(edgeIdentifier)
|
||||
if err != nil {
|
||||
// TODO: if its a valid cert, or the user hasn't limited to mTLS / portainer set id, the
|
||||
// create new untrusted environment
|
||||
// portainer.HTTPResponseAgentPlatform tells us what platform it is too...
|
||||
logrus.WithField("portainer.PortainerAgentEdgeIDHeader", edgeIdentifier).Debug("edge id not found in existing endpoints!")
|
||||
}
|
||||
// if agent mTLS is on, drop the connection if the client cert isn't CA'd (or if its revoked)
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
|
||||
}
|
||||
|
||||
// Any request we can identify as coming from a valid agent is treated as a Ping
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
var payload AsyncRequest
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
// an "" request ~~ same as {}
|
||||
logrus.WithError(err).WithField("payload", r).Debug("decode payload")
|
||||
}
|
||||
|
||||
asyncResponse := AsyncResponse{
|
||||
ServerCommandId: "8888", // the most current id of a new command on the server
|
||||
}
|
||||
|
||||
// TODO: need a way to detect that these are changed, and send them to the agent...
|
||||
// CommandInterval time.Duration `json: optional`
|
||||
// PingInterval time.Duration `json: optional`
|
||||
// SnapshotInterval time.Duration `json: optional`
|
||||
|
||||
if payload.CommandId == "" && payload.Snapshot == nil {
|
||||
// just a ping.
|
||||
return response.JSON(w, asyncResponse)
|
||||
}
|
||||
|
||||
if payload.Snapshot != nil {
|
||||
asyncResponse.SendDiffSnapshotTime = handler.saveSnapshot(endpoint, payload)
|
||||
}
|
||||
if payload.CommandId != "" {
|
||||
asyncResponse.Commands = handler.sendCommandsSince(endpoint, payload.CommandId)
|
||||
}
|
||||
|
||||
return response.JSON(w, asyncResponse)
|
||||
}
|
||||
|
||||
// TODO: yup, next step is for these to be JSONDiff's and to be rehydrated
|
||||
func (handler *Handler) saveSnapshot(endpoint *portainer.Endpoint, payload AsyncRequest) time.Time {
|
||||
for stackID, status := range payload.StackStatus {
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
// TODO: work out what we can do with the errors
|
||||
if err == nil {
|
||||
stack.Status[*status.EndpointID] = portainer.EdgeStackStatus{
|
||||
Type: *status.Status,
|
||||
Error: status.Error,
|
||||
EndpointID: *status.EndpointID,
|
||||
}
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
}
|
||||
}
|
||||
|
||||
switch endpoint.Type {
|
||||
// case portainer.AzureEnvironment:
|
||||
// return time.Now()
|
||||
case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
logrus.Debug("Got a Kubernetes Snapshot")
|
||||
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*payload.Snapshot.Kubernetes}
|
||||
return time.Unix(payload.Snapshot.Kubernetes.Time, 0)
|
||||
case portainer.DockerEnvironment, portainer.AgentOnDockerEnvironment, portainer.EdgeAgentOnDockerEnvironment:
|
||||
logrus.Debug("Got a Docker Snapshot")
|
||||
endpoint.Snapshots = []portainer.DockerSnapshot{*payload.Snapshot.Docker}
|
||||
return time.Unix(payload.Snapshot.Docker.Time, 0)
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Handler) sendCommandsSince(endpoint *portainer.Endpoint, commandId string) []JSONPatch {
|
||||
var commandList []JSONPatch
|
||||
|
||||
// TODO: later, figure out if it is scalable to do diff's, as it means the server needs to store what it sent to all million agents (if the database had time based versioning, this would be trivial...)
|
||||
// I suspect the easiest thing will be to add a "modified timestamp" to edge stacks and edge jobs, and to send them only when the modified time > requested time
|
||||
logrus.WithField("endpoint", endpoint.Name).WithField("from command", commandId).Debug("Sending commands")
|
||||
|
||||
// schedules := []edgeJobResponse{}
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
for _, job := range tunnel.Jobs {
|
||||
schedule := edgeJobResponse{
|
||||
ID: job.ID,
|
||||
CronExpression: job.CronExpression,
|
||||
CollectLogs: job.Endpoints[endpoint.ID].CollectLogs,
|
||||
Version: job.Version,
|
||||
}
|
||||
|
||||
file, err := handler.FileService.GetFileContent("/", job.ScriptPath)
|
||||
if err != nil {
|
||||
// TODO: this should maybe just skip thi job?
|
||||
logrus.WithError(err).Error("Unable to retrieve Edge job script file")
|
||||
continue
|
||||
}
|
||||
|
||||
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
|
||||
cmd := JSONPatch{
|
||||
Operation: "add",
|
||||
Path: fmt.Sprintf("/edgejob/%d", schedule.ID),
|
||||
Value: schedule,
|
||||
}
|
||||
commandList = append(commandList, cmd)
|
||||
}
|
||||
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Unable to retrieve relation object from the database")
|
||||
return commandList
|
||||
}
|
||||
|
||||
// TODO: this is the datatype the agent uses in the end
|
||||
type edgeStackData struct {
|
||||
ID portainer.EdgeStackID
|
||||
Version int
|
||||
StackFileContent string
|
||||
Name string
|
||||
}
|
||||
|
||||
for stackID := range relation.EdgeStacks {
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Unable to retrieve edge stack from the database")
|
||||
continue
|
||||
}
|
||||
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
logrus.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
|
||||
continue
|
||||
} else if err != nil {
|
||||
logrus.WithError(err).Error("Unable to find an edge stack with the specified identifier inside the database")
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := edgeStack.EntryPoint
|
||||
if endpointutils.IsDockerEndpoint(endpoint) {
|
||||
if fileName == "" {
|
||||
logrus.Error("Docker is not supported by this stack")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
fileName = edgeStack.ManifestPath
|
||||
|
||||
if fileName == "" {
|
||||
logrus.Error("Kubernetes is not supported by this stack")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Unable to retrieve Compose file from disk")
|
||||
continue
|
||||
}
|
||||
|
||||
stackStatus := edgeStackData{
|
||||
StackFileContent: string(stackFileContent),
|
||||
Name: edgeStack.Name,
|
||||
ID: stack.ID,
|
||||
Version: stack.Version,
|
||||
}
|
||||
|
||||
cmd := JSONPatch{
|
||||
Operation: "add",
|
||||
Path: fmt.Sprintf("/edgestack/%d", stack.ID),
|
||||
Value: stackStatus,
|
||||
}
|
||||
commandList = append(commandList, cmd)
|
||||
}
|
||||
return commandList
|
||||
}
|
||||
|
||||
// TODO: this probably should be in the data layer.. (like, somewhere that depends dataservices/errors)
|
||||
func (handler *Handler) getEdgeEndpoint(edgeIdentifier string) (*portainer.Endpoint, error) {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.EdgeID == edgeIdentifier {
|
||||
return &endpoint, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
)
|
||||
|
||||
type logsPayload struct {
|
||||
@@ -32,9 +31,16 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
// @failure 400
|
||||
// @router /endpoints/{id}/edge/jobs/{jobID}/logs [post]
|
||||
func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Unable to find an environment on request context", err)
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
@@ -60,7 +66,7 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(int(endpoint.ID)), []byte(payload.FileContent))
|
||||
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(endpointID), []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save task log to the filesystem", err}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
@@ -30,9 +29,16 @@ type configResponse struct {
|
||||
// @failure 404
|
||||
// @router /endpoints/{id}/edge/stacks/{stackId} [get]
|
||||
func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Unable to find an environment on request context", err)
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointTestCase struct {
|
||||
endpoint portainer.Endpoint
|
||||
endpointRelation portainer.EndpointRelation
|
||||
expectedStatusCode int
|
||||
}
|
||||
|
||||
var endpointTestCases = []endpointTestCase{
|
||||
{
|
||||
portainer.Endpoint{},
|
||||
portainer.EndpointRelation{},
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: -1,
|
||||
Name: "endpoint-id--1",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: -1,
|
||||
},
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: 2,
|
||||
Name: "endpoint-id-2",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 2,
|
||||
},
|
||||
http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
ID: 4,
|
||||
Name: "endpoint-id-4",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
},
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: 4,
|
||||
},
|
||||
http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
func setupHandler() (*Handler, func(), error) {
|
||||
tmpDir, err := os.MkdirTemp(os.TempDir(), "portainer-test")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create a tmp dir: %w", err)
|
||||
}
|
||||
|
||||
fs, err := filesystem.NewService(tmpDir, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not start a new filesystem service: %w", err)
|
||||
}
|
||||
|
||||
_, store, storeTeardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
ctx := context.Background()
|
||||
shutdownCtx, cancelFn := context.WithCancel(ctx)
|
||||
|
||||
teardown := func() {
|
||||
cancelFn()
|
||||
storeTeardown()
|
||||
}
|
||||
|
||||
jwtService, err := jwt.NewService("1h", store)
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not start a new jwt service: %w", err)
|
||||
}
|
||||
|
||||
apiKeyService := apikey.NewAPIKeyService(nil, nil)
|
||||
|
||||
settings, err := store.Settings().Settings()
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not create new settings: %w", err)
|
||||
}
|
||||
settings.TrustOnFirstConnect = true
|
||||
|
||||
err = store.Settings().UpdateSettings(settings)
|
||||
if err != nil {
|
||||
teardown()
|
||||
return nil, nil, fmt.Errorf("could not update settings: %w", err)
|
||||
}
|
||||
|
||||
handler := NewHandler(
|
||||
security.NewRequestBouncer(store, jwtService, apiKeyService),
|
||||
store,
|
||||
fs,
|
||||
chisel.NewService(store, shutdownCtx),
|
||||
)
|
||||
|
||||
handler.ReverseTunnelService = chisel.NewService(store, shutdownCtx)
|
||||
|
||||
return handler, teardown, nil
|
||||
}
|
||||
|
||||
func createEndpoint(handler *Handler, endpoint portainer.Endpoint, endpointRelation portainer.EndpointRelation) (err error) {
|
||||
// Avoid setting ID below 0 to generate invalid test cases
|
||||
if endpoint.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().Create(&endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
}
|
||||
|
||||
func TestMissingEdgeIdentifier(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(45)
|
||||
err = createEndpoint(handler, portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "endpoint-id-45",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
}, portainer.EndpointRelation{EndpointID: endpointID})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithEndpoints(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range endpointTestCases {
|
||||
err = createEndpoint(handler, test.endpoint, test.endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != test.expectedStatusCode {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastCheckInDateIncreases(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(56)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-56",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Greater(t, updatedEndpoint.LastCheckInDate, endpoint.LastCheckInDate)
|
||||
}
|
||||
|
||||
func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(44)
|
||||
edgeId := "edge-id"
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-44",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "",
|
||||
}
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, edgeId)
|
||||
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, updatedEndpoint.EdgeID, edgeId)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestEdgeStackStatus(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(7)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-7",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(17)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-17",
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {Type: portainer.StatusOk, Error: "", EndpointID: endpoint.ID},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||||
ProjectPath: "/project/path",
|
||||
EntryPoint: "entrypoint",
|
||||
Version: 237,
|
||||
ManifestPath: "/manifest/path",
|
||||
DeploymentType: 1,
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{
|
||||
edgeStack.ID: true,
|
||||
},
|
||||
}
|
||||
handler.DataStore.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
var data endpointEdgeStatusInspectResponse
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
assert.Len(t, data.Stacks, 1)
|
||||
assert.Equal(t, edgeStack.ID, data.Stacks[0].ID)
|
||||
assert.Equal(t, edgeStack.Version, data.Stacks[0].Version)
|
||||
}
|
||||
*/
|
||||
|
||||
func TestEdgeJobsResponse(t *testing.T) {
|
||||
handler, teardown, err := setupHandler()
|
||||
defer teardown()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(77)
|
||||
endpoint := portainer.Endpoint{
|
||||
ID: endpointID,
|
||||
Name: "test-endpoint-77",
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
URL: "https://portainer.io:9443",
|
||||
EdgeID: "edge-id",
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
|
||||
err = createEndpoint(handler, endpoint, endpointRelation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeJobID := portainer.EdgeJobID(35)
|
||||
edgeJob := portainer.EdgeJob{
|
||||
ID: edgeJobID,
|
||||
Created: time.Now().Unix(),
|
||||
CronExpression: "* * * * *",
|
||||
Name: "test-edge-job",
|
||||
ScriptPath: path,
|
||||
Recurring: true,
|
||||
Version: 57,
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
|
||||
}
|
||||
|
||||
var data endpointEdgeStatusInspectResponse
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
assert.Len(t, data.Schedules, 1)
|
||||
assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
|
||||
assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
|
||||
assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
|
||||
}
|
||||
@@ -3,15 +3,15 @@ package endpointedge
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type stackStatusResponse struct {
|
||||
@@ -34,7 +34,7 @@ type edgeJobResponse struct {
|
||||
Version int `json:"Version" example:"2"`
|
||||
}
|
||||
|
||||
type endpointEdgeStatusInspectResponse struct {
|
||||
type endpointStatusInspectResponse struct {
|
||||
// Status represents the environment(endpoint) status
|
||||
Status string `json:"status" example:"REQUIRED"`
|
||||
// The tunnel port
|
||||
@@ -44,29 +44,37 @@ type endpointEdgeStatusInspectResponse struct {
|
||||
// The current value of CheckinInterval
|
||||
CheckinInterval int `json:"checkin" example:"5"`
|
||||
//
|
||||
Credentials string `json:"credentials"`
|
||||
Credentials string `json:"credentials" example:""`
|
||||
// List of stacks to be deployed on the environments(endpoints)
|
||||
Stacks []stackStatusResponse `json:"stacks"`
|
||||
}
|
||||
|
||||
// @id EndpointEdgeStatusInspect
|
||||
// @id EndpointStatusInspect
|
||||
// @summary Get environment(endpoint) status
|
||||
// @description environment(endpoint) for edge agent to check status of environment(endpoint)
|
||||
// @description Environment(Endpoint) for edge agent to check status of environment(endpoint)
|
||||
// @description **Access policy**: restricted only to Edge environments(endpoints)
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 {object} endpointEdgeStatusInspectResponse "Success"
|
||||
// @success 200 {object} endpointStatusInspectResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access environment(endpoint)"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/edge/status [get]
|
||||
func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
// @router /endpoints/{id}/status [get]
|
||||
func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Unable to find an environment on request context", err)
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
logrus.WithError(err).WithField("env", endpointID).WithField("remote", r.RemoteAddr).Error("Unable to find an environment with the specified identifier inside the database")
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
@@ -78,11 +86,32 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
|
||||
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
|
||||
if agentPlatformErr != nil {
|
||||
return httperror.BadRequest("agent platform header is not valid", err)
|
||||
agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Agent Platform Header is missing", errors.New("Agent Platform Header is missing")}
|
||||
}
|
||||
endpoint.Type = agentPlatform
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse agent platform header", err}
|
||||
}
|
||||
|
||||
agentPlatform := portainer.AgentPlatform(agentPlatformNumber)
|
||||
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
endpoint.Type = portainer.EdgeAgentOnDockerEnvironment
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
endpoint.LastCheckInDate = time.Now().Unix()
|
||||
@@ -92,98 +121,50 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist environment changes inside the database", err}
|
||||
}
|
||||
|
||||
checkinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
checkinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
statusResponse := endpointEdgeStatusInspectResponse{
|
||||
Status: tunnel.Status,
|
||||
Port: tunnel.Port,
|
||||
CheckinInterval: checkinInterval,
|
||||
Credentials: tunnel.Credentials,
|
||||
}
|
||||
|
||||
schedules, handlerErr := handler.buildSchedules(endpoint.ID, tunnel)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
statusResponse.Schedules = schedules
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
|
||||
}
|
||||
|
||||
edgeStacksStatus, handlerErr := handler.buildEdgeStacks(endpoint.ID)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
statusResponse.Stacks = edgeStacksStatus
|
||||
|
||||
return response.JSON(w, statusResponse)
|
||||
}
|
||||
|
||||
func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
|
||||
agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, errors.New("agent platform header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
agentPlatform := portainer.AgentPlatform(agentPlatformNumber)
|
||||
|
||||
switch agentPlatform {
|
||||
case portainer.AgentPlatformDocker:
|
||||
return portainer.EdgeAgentOnDockerEnvironment, nil
|
||||
case portainer.AgentPlatformKubernetes:
|
||||
return portainer.EdgeAgentOnKubernetesEnvironment, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("agent platform %v is not valid", agentPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Handler) buildSchedules(endpointID portainer.EndpointID, tunnel portainer.TunnelDetails) ([]edgeJobResponse, *httperror.HandlerError) {
|
||||
schedules := []edgeJobResponse{}
|
||||
for _, job := range tunnel.Jobs {
|
||||
schedule := edgeJobResponse{
|
||||
ID: job.ID,
|
||||
CronExpression: job.CronExpression,
|
||||
CollectLogs: job.Endpoints[endpointID].CollectLogs,
|
||||
CollectLogs: job.Endpoints[endpoint.ID].CollectLogs,
|
||||
Version: job.Version,
|
||||
}
|
||||
|
||||
file, err := handler.FileService.GetFileContent(job.ScriptPath, "")
|
||||
file, err := handler.FileService.GetFileContent("", job.ScriptPath)
|
||||
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
|
||||
}
|
||||
|
||||
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
|
||||
|
||||
schedules = append(schedules, schedule)
|
||||
}
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) buildEdgeStacks(endpointID portainer.EndpointID) ([]stackStatusResponse, *httperror.HandlerError) {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
statusResponse := endpointStatusInspectResponse{
|
||||
Status: tunnel.Status,
|
||||
Port: tunnel.Port,
|
||||
Schedules: schedules,
|
||||
CheckinInterval: endpoint.EdgeCheckinInterval,
|
||||
Credentials: tunnel.Credentials,
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
|
||||
}
|
||||
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err}
|
||||
}
|
||||
|
||||
edgeStacksStatus := []stackStatusResponse{}
|
||||
for stackID := range relation.EdgeStacks {
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err}
|
||||
}
|
||||
|
||||
stackStatus := stackStatusResponse{
|
||||
@@ -193,5 +174,8 @@ func (handler *Handler) buildEdgeStacks(endpointID portainer.EndpointID) ([]stac
|
||||
|
||||
edgeStacksStatus = append(edgeStacksStatus, stackStatus)
|
||||
}
|
||||
return edgeStacksStatus, nil
|
||||
|
||||
statusResponse.Stacks = edgeStacksStatus
|
||||
|
||||
return response.JSON(w, statusResponse)
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package endpointedge
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -23,23 +21,19 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
DataStore: dataStore,
|
||||
FileService: fileService,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
endpointRouter := h.PathPrefix("/{id}").Subrouter()
|
||||
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||
|
||||
endpointRouter.PathPrefix("/edge/status").Handler(
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
|
||||
endpointRouter.PathPrefix("/edge/stacks/{stackId}").Handler(
|
||||
h.Handle("/{id}/edge/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/{id}/edge/stacks/{stackId}",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
|
||||
endpointRouter.PathPrefix("/edge/jobs/{jobID}/logs").Handler(
|
||||
h.Handle("/{id}/edge/jobs/{jobID}/logs",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
|
||||
h.Handle("/edge/async/",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointAsync))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -35,12 +35,11 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en
|
||||
}
|
||||
|
||||
endpointStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
updatedStacks := make(map[portainer.EdgeStackID]portainer.EdgeStackStatus)
|
||||
|
||||
stacksSet := map[portainer.EdgeStackID]bool{}
|
||||
for _, edgeStackID := range endpointStacks {
|
||||
updatedStacks[edgeStackID] = endpointRelation.EdgeStacks[edgeStackID]
|
||||
stacksSet[edgeStackID] = true
|
||||
}
|
||||
endpointRelation.EdgeStacks = updatedStacks
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package endpoints
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
@@ -209,15 +210,13 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
relationObject := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
for _, stackID := range relatedEdgeStacks {
|
||||
relationObject.EdgeStacks[stackID] = portainer.EdgeStackStatus{
|
||||
Type: portainer.StatusAcknowledged,
|
||||
}
|
||||
relationObject.EdgeStacks[stackID] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,24 +293,29 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occurred while trying to create the environment", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
//endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
|
||||
portainerHost, err := edge.ParseHostForEdge(payload.URL)
|
||||
portainerURL, err := url.Parse(payload.URL)
|
||||
if err != nil {
|
||||
return nil, httperror.BadRequest("Unable to parse host", err)
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid environment URL", err}
|
||||
}
|
||||
|
||||
//edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
portainerHost, _, err := net.SplitHostPort(portainerURL.Host)
|
||||
if err != nil {
|
||||
portainerHost = portainerURL.Host
|
||||
}
|
||||
|
||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
//ID: portainer.EndpointID(endpointID),
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: payload.Name,
|
||||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
@@ -319,16 +323,15 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
//EdgeKey: edgeKey,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
EdgeKey: edgeKey,
|
||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
UserTrusted: true,
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
@@ -345,15 +348,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
endpoint.EdgeID = edgeID.String()
|
||||
}
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizationsWithCallback(endpoint, func(id uint64) (int, interface{}) {
|
||||
endpoint.ID = portainer.EndpointID(id)
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
endpoint.EdgeKey = handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, int(id))
|
||||
}
|
||||
|
||||
return int(id), endpoint
|
||||
})
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the environment", err}
|
||||
}
|
||||
@@ -471,7 +466,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
err := handler.SnapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Invalid request signature") || strings.Contains(err.Error(), "unknown") {
|
||||
if strings.Contains(err.Error(), "Invalid request signature") {
|
||||
err = errors.New("agent already paired with another Portainer instance")
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with environment", err}
|
||||
@@ -521,42 +516,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveEndpointAndUpdateAuthorizationsWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
|
||||
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().CreateWithCallback(endpoint, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag.Endpoints[endpoint.ID] = true
|
||||
|
||||
err = handler.DataStore.Tag().UpdateTag(tagID, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *endpointCreatePayload) *httperror.HandlerError {
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type endpointCreateGlobalKeyResponse struct {
|
||||
EndpointID portainer.EndpointID `json:"endpointID"`
|
||||
}
|
||||
|
||||
// @id EndpointCreateGlobalKey
|
||||
// @summary Create or retrieve the endpoint for an EdgeID
|
||||
// @tags endpoints
|
||||
// @success 200 {object} endpointCreateGlobalKeyResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/global-key [post]
|
||||
func (handler *Handler) endpointCreateGlobalKey(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeID := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
if edgeID == "" {
|
||||
return httperror.BadRequest("Invalid Edge ID", errors.New("the Edge ID cannot be empty"))
|
||||
}
|
||||
|
||||
// Search for existing endpoints for the given edgeID
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.EdgeID == edgeID {
|
||||
return response.JSON(w, endpointCreateGlobalKeyResponse{endpoint.ID})
|
||||
}
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint in the database", err}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func TestEmptyGlobalKey(t *testing.T) {
|
||||
handler := NewHandler(
|
||||
helper.NewTestRequestBouncer(),
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://portainer.io:9443/endpoints/global-key", nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatal("expected a 400 response, found:", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,22 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
|
||||
}
|
||||
|
||||
for idx := range edgeStacks {
|
||||
edgeStack := &edgeStacks[idx]
|
||||
if _, ok := edgeStack.Status[endpoint.ID]; ok {
|
||||
delete(edgeStack.Status, endpoint.ID)
|
||||
err = handler.DataStore.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := handler.DataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
|
||||
@@ -2,10 +2,8 @@ package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
@@ -13,24 +11,8 @@ import (
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceFilterAll = "all"
|
||||
EdgeDeviceFilterTrusted = "trusted"
|
||||
EdgeDeviceFilterUntrusted = "untrusted"
|
||||
EdgeDeviceFilterNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceIntervalMultiplier = 2
|
||||
EdgeDeviceIntervalAdd = 20
|
||||
)
|
||||
|
||||
var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
|
||||
// @id EndpointList
|
||||
// @summary List environments(endpoints)
|
||||
// @description List all environments(endpoints) based on the current user authorizations. Will
|
||||
@@ -49,7 +31,6 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints [get]
|
||||
@@ -66,8 +47,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
|
||||
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
|
||||
|
||||
var endpointTypes []int
|
||||
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
|
||||
@@ -80,23 +59,11 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
var endpointIDs []portainer.EndpointID
|
||||
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
|
||||
|
||||
var statuses []int
|
||||
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
|
||||
|
||||
var groupIDs []int
|
||||
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
}
|
||||
|
||||
// create endpoint groups as a map for more convenient access
|
||||
endpointGroupNames = make(map[portainer.EndpointGroupID]string, 0)
|
||||
for _, group := range endpointGroups {
|
||||
endpointGroupNames[group.ID] = group.Name
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
@@ -113,29 +80,20 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
|
||||
}
|
||||
|
||||
if endpointIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
|
||||
}
|
||||
|
||||
if len(groupIDs) > 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
|
||||
}
|
||||
|
||||
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilter != "" {
|
||||
edgeDeviceFilter, edgeDeviceFilterErr := request.RetrieveBooleanQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilterErr == nil {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
|
||||
}
|
||||
|
||||
if len(statuses) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
@@ -156,9 +114,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
|
||||
}
|
||||
|
||||
// Sort endpoints by field
|
||||
sortEndpointsByField(filteredEndpoints, sortField, sortOrder == "desc")
|
||||
|
||||
filteredEndpointCount := len(filteredEndpoints)
|
||||
|
||||
paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit)
|
||||
@@ -169,11 +124,9 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
if paginatedEndpoints[idx].EdgeCheckinInterval == 0 {
|
||||
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
paginatedEndpoints[idx].QueryDate = time.Now().Unix()
|
||||
}
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
w.Header().Set("X-Total-Available", strconv.Itoa(totalAvailableEndpoints))
|
||||
return response.JSON(w, paginatedEndpoints)
|
||||
}
|
||||
|
||||
@@ -196,11 +149,11 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
||||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
|
||||
func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID portainer.EndpointGroupID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
|
||||
if endpoint.GroupID == endpointGroupID {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
@@ -226,64 +179,6 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
}
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
status = portainer.EndpointStatusUp // Online
|
||||
}
|
||||
}
|
||||
|
||||
if utils.Contains(statuses, int(status)) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func sortEndpointsByField(endpoints []portainer.Endpoint, sortField string, isSortDesc bool) {
|
||||
|
||||
switch sortField {
|
||||
case "Name":
|
||||
if isSortDesc {
|
||||
sort.Stable(sort.Reverse(EndpointsByName(endpoints)))
|
||||
} else {
|
||||
sort.Stable(EndpointsByName(endpoints))
|
||||
}
|
||||
|
||||
case "Group":
|
||||
if isSortDesc {
|
||||
sort.Stable(sort.Reverse(EndpointsByGroup(endpoints)))
|
||||
} else {
|
||||
sort.Stable(EndpointsByGroup(endpoints))
|
||||
}
|
||||
|
||||
case "Status":
|
||||
if isSortDesc {
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
return endpoints[i].Status > endpoints[j].Status
|
||||
})
|
||||
} else {
|
||||
sort.Slice(endpoints, func(i, j int) bool {
|
||||
return endpoints[i].Status < endpoints[j].Status
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
@@ -341,39 +236,17 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int)
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
|
||||
if edgeDeviceFilter == endpoint.IsEdgeDevice {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
|
||||
// none - return all endpoints that are not edge devices
|
||||
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
|
||||
return true
|
||||
}
|
||||
|
||||
if !endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch edgeDeviceFilter {
|
||||
case EdgeDeviceFilterAll:
|
||||
return true
|
||||
case EdgeDeviceFilterTrusted:
|
||||
return endpoint.UserTrusted
|
||||
case EdgeDeviceFilterUntrusted:
|
||||
return !endpoint.UserTrusted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
filter string
|
||||
}
|
||||
|
||||
func Test_endpointList(t *testing.T) {
|
||||
var err error
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEndpoint,
|
||||
untrustedEndpoint,
|
||||
regularUntrustedEdgeEndpoint,
|
||||
regularTrustedEdgeEndpoint,
|
||||
regularEndpoint,
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err = store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
tests := []endpointListEdgeDeviceTest{
|
||||
{
|
||||
"should show all edge endpoints",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterAll,
|
||||
},
|
||||
{
|
||||
"should show only trusted edge devices",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterTrusted,
|
||||
},
|
||||
{
|
||||
"should show only untrusted edge devices",
|
||||
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterUntrusted,
|
||||
},
|
||||
{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterNone,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
req := buildEndpointListRequest(test.filter)
|
||||
resp, err := doEndpointListRequest(req, h, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range resp {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.ElementsMatch(test.expected, respIds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(filter string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
|
||||
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
restrictedCtx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
|
||||
req = req.WithContext(restrictedCtx)
|
||||
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func doEndpointListRequest(req *http.Request, h *Handler, is *assert.Assertions) ([]portainer.Endpoint, error) {
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := []portainer.Endpoint{}
|
||||
err = json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
63
api/http/handler/endpoints/endpoint_registries_inspect.go
Normal file
63
api/http/handler/endpoints/endpoint_registries_inspect.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id endpointRegistryInspect
|
||||
// @summary get registry for environment
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "identifier"
|
||||
// @param registryId path int true "Registry identifier"
|
||||
// @success 200 {object} portainer.Registry "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Registry not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries/{registryId} [get]
|
||||
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a registry with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user from the database", Err: err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedRegistryAccess(registry, user, securityContext.UserMemberships, portainer.EndpointID(endpointID)) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
hideRegistryFields(registry, !securityContext.IsAdmin)
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
@@ -55,9 +55,29 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
|
||||
registries, handleError := handler.filterRegistriesByAccess(r, registries, endpoint, user, securityContext.UserMemberships)
|
||||
if handleError != nil {
|
||||
return handleError
|
||||
if endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
|
||||
if namespace == "" && !isAdmin {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Missing namespace query parameter", Err: errors.New("missing namespace query parameter")}
|
||||
}
|
||||
|
||||
if namespace != "" {
|
||||
|
||||
authorized, err := handler.isNamespaceAuthorized(endpoint, namespace, user.ID, securityContext.UserMemberships, isAdmin)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to check for namespace authorization", err}
|
||||
}
|
||||
|
||||
if !authorized {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")}
|
||||
}
|
||||
|
||||
registries = filterRegistriesByNamespace(registries, endpoint.ID, namespace)
|
||||
}
|
||||
|
||||
} else if !isAdmin {
|
||||
registries = security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
}
|
||||
|
||||
for idx := range registries {
|
||||
@@ -67,40 +87,6 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re
|
||||
return response.JSON(w, registries)
|
||||
}
|
||||
|
||||
func (handler *Handler) filterRegistriesByAccess(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
return security.FilterRegistries(registries, user, memberships, endpoint.ID), nil
|
||||
}
|
||||
|
||||
return handler.filterKubernetesEndpointRegistries(r, registries, endpoint, user, memberships)
|
||||
}
|
||||
|
||||
func (handler *Handler) filterKubernetesEndpointRegistries(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
namespaceParam, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
isAdmin, err := security.IsAdmin(r)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check user role", Err: err}
|
||||
}
|
||||
|
||||
if namespaceParam != "" {
|
||||
authorized, err := handler.isNamespaceAuthorized(endpoint, namespaceParam, user.ID, memberships, isAdmin)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to check for namespace authorization", Err: err}
|
||||
}
|
||||
if !authorized {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")}
|
||||
}
|
||||
|
||||
return filterRegistriesByNamespaces(registries, endpoint.ID, []string{namespaceParam}), nil
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
return handler.filterKubernetesRegistriesByUserRole(r, registries, endpoint, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) {
|
||||
if isAdmin || namespace == "" {
|
||||
return true, nil
|
||||
@@ -128,78 +114,24 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
|
||||
return security.AuthorizedAccess(userId, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies), nil
|
||||
}
|
||||
|
||||
func filterRegistriesByNamespaces(registries []portainer.Registry, endpointId portainer.EndpointID, namespaces []string) []portainer.Registry {
|
||||
func filterRegistriesByNamespace(registries []portainer.Registry, endpointId portainer.EndpointID, namespace string) []portainer.Registry {
|
||||
if namespace == "" {
|
||||
return registries
|
||||
}
|
||||
|
||||
filteredRegistries := []portainer.Registry{}
|
||||
|
||||
for _, registry := range registries {
|
||||
if registryAccessPoliciesContainsNamespace(registry.RegistryAccesses[endpointId], namespaces) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
for _, authorizedNamespace := range registry.RegistryAccesses[endpointId].Namespaces {
|
||||
if authorizedNamespace == namespace {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRegistries
|
||||
}
|
||||
|
||||
func registryAccessPoliciesContainsNamespace(registryAccess portainer.RegistryAccessPolicies, namespaces []string) bool {
|
||||
for _, authorizedNamespace := range registryAccess.Namespaces {
|
||||
for _, namespace := range namespaces {
|
||||
if namespace == authorizedNamespace {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err == security.ErrAuthorizationRequired {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized", Err: errors.New("missing namespace query parameter")}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
userNamespaces, err := handler.userNamespaces(endpoint, user)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "unable to retrieve user namespaces", Err: err}
|
||||
}
|
||||
|
||||
return filterRegistriesByNamespaces(registries, endpoint.ID, userNamespaces), nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
|
||||
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespaceAuthorizations, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userNamespaces []string
|
||||
for namespace, namespaceAuthorization := range namespaceAuthorizations {
|
||||
if _, ok := namespaceAuthorization.UserAccessPolicies[user.ID]; ok {
|
||||
userNamespaces = append(userNamespaces, namespace)
|
||||
continue
|
||||
}
|
||||
for _, userTeam := range userMemberships {
|
||||
if _, ok := namespaceAuthorization.TeamAccessPolicies[userTeam.TeamID]; ok {
|
||||
userNamespaces = append(userNamespaces, namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return userNamespaces, nil
|
||||
}
|
||||
|
||||
func hideRegistryFields(registry *portainer.Registry, hideAccesses bool) {
|
||||
registry.Password = ""
|
||||
registry.ManagementConfiguration = nil
|
||||
|
||||
@@ -43,6 +43,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint)
|
||||
|
||||
// TODO: so huh? why are we getting the endpoint a second time? if there's a reason to do this - please add that as a comment!
|
||||
latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user