Compare commits
4 Commits
2.19.3
...
feat/fedex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eab395555c | ||
|
|
ab627107ad | ||
|
|
cdfa0f8f34 | ||
|
|
a304b9c7ef |
44
.codeclimate.yml
Normal file
44
.codeclimate.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
version: "2"
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
complex-logic:
|
||||
enabled: false
|
||||
file-lines:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: false
|
||||
method-lines:
|
||||
enabled: false
|
||||
nested-control-flow:
|
||||
enabled: false
|
||||
return-statements:
|
||||
enabled: false
|
||||
similar-code:
|
||||
enabled: false
|
||||
identical-code:
|
||||
enabled: false
|
||||
plugins:
|
||||
gofmt:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: "eslint-5"
|
||||
config:
|
||||
config: .eslintrc.yml
|
||||
exclude_patterns:
|
||||
- assets/
|
||||
- build/
|
||||
- dist/
|
||||
- distribution/
|
||||
- node_modules
|
||||
- test/
|
||||
- webpack/
|
||||
- gruntfile.js
|
||||
- webpack.config.js
|
||||
- api/
|
||||
- "!app/kubernetes/**"
|
||||
- .github/
|
||||
- .tmp/
|
||||
@@ -83,11 +83,10 @@ overrides:
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
]
|
||||
no-plusplus: off
|
||||
func-style: [error, 'declaration']
|
||||
import/prefer-default-export: off
|
||||
no-use-before-define: "off"
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }]
|
||||
no-use-before-define: ['error', { functions: false }]
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
|
||||
no-shadow: 'off'
|
||||
'@typescript-eslint/no-shadow': off
|
||||
jsx-a11y/no-autofocus: warn
|
||||
@@ -100,17 +99,12 @@ overrides:
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
|
||||
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
||||
'react/jsx-no-bind': off
|
||||
'no-await-in-loop': 'off'
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
||||
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
|
||||
overrides: # allow props spreading for hoc files
|
||||
- files:
|
||||
- app/**/with*.ts{,x}
|
||||
rules:
|
||||
'react/jsx-props-no-spreading': off
|
||||
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
|
||||
@@ -2,7 +2,4 @@
|
||||
cf5056d9c03b62d91a25c3b9127caac838695f98
|
||||
|
||||
# prettier v2
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
|
||||
# tailwind prettier
|
||||
58d66d3142950bb90a7d85511c034ac9fabba9ba
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Portainer Business Edition - Get 3 nodes free
|
||||
url: https://www.portainer.io/take-3
|
||||
about: Portainer Business Edition has more features, more support and you can now get 3 nodes free for as long as you want.
|
||||
- name: Portainer Business Edition - Get 5 nodes free
|
||||
url: https://portainer.io/pricing/take5
|
||||
about: Portainer Business Edition has more features, more support and you can now get 5 nodes free for as long as you want.
|
||||
|
||||
4
.github/workflows/label-conflcts.yaml
vendored
4
.github/workflows/label-conflcts.yaml
vendored
@@ -11,5 +11,5 @@ jobs:
|
||||
with:
|
||||
CONFLICT_LABEL_NAME: 'has conflicts'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAX_RETRIES: 10
|
||||
WAIT_MS: 60000
|
||||
MAX_RETRIES: 5
|
||||
WAIT_MS: 5000
|
||||
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -21,12 +21,10 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
with:
|
||||
@@ -38,9 +36,3 @@ jobs:
|
||||
gofmt_dir: api/
|
||||
- name: Typecheck
|
||||
uses: icrawl/action-tsc@v1
|
||||
- name: GolangCI-Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.54.1
|
||||
working-directory: api
|
||||
args: --timeout=10m -c .golangci.yaml
|
||||
|
||||
157
.github/workflows/nightly-security-scan.yml
vendored
157
.github/workflows/nightly-security-scan.yml
vendored
@@ -1,23 +1,22 @@
|
||||
name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 20 * * *'
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- 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:
|
||||
@@ -25,140 +24,166 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
- 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 html file as artifact
|
||||
- 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 vulnerabilities
|
||||
- name: Analyse the js result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "js_result=${result}" >> $GITHUB_OUTPUT
|
||||
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
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: download Go modules
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- 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 }}
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: Upload go security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
- 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 html file as artifact
|
||||
- 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 vulnerabilities
|
||||
- name: Analyse the go result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "go_result=${result}" >> $GITHUB_OUTPUT
|
||||
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: Image Vulnerability Check
|
||||
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: scan vulnerabilities by Trivy
|
||||
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 portainerci/portainer:develop
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: upload image security scan result as artifact
|
||||
- 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: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
- 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 html file as artifact
|
||||
- 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 vulnerabilities
|
||||
- name: Analyse the trivy result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||
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 Results
|
||||
name: Analyse scan result
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
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 scan
|
||||
- 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 }}"
|
||||
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 message to Slack
|
||||
- name: Send Slack message
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
uses: slackapi/slack-github-action@v1.18.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
|
||||
149
.github/workflows/pr-security.yml
vendored
149
.github/workflows/pr-security.yml
vendored
@@ -12,11 +12,10 @@ on:
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
- '.github/workflows/pr-security.yml'
|
||||
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -24,10 +23,9 @@ jobs:
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- 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:
|
||||
@@ -35,13 +33,13 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: upload scan result as pull-request artifact
|
||||
- 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 built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -53,24 +51,24 @@ jobs:
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest 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: 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 html file as artifact
|
||||
- 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 different vulnerabilities against develop branch
|
||||
- name: Analyse the js diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
|
||||
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
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
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -78,32 +76,27 @@ jobs:
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: download Go modules
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- 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 }}
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
|
||||
- name: upload scan result as pull-request artifact
|
||||
- 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 built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -115,24 +108,24 @@ jobs:
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest 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: 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 html file as artifact
|
||||
- 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 different vulnerabilities against develop branch
|
||||
- name: Analyse the go diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
|
||||
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
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: Image Vulnerability Check
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -140,53 +133,50 @@ jobs:
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
- name: checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: install Go 1.19.5
|
||||
- name: Use golang 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: '1.18'
|
||||
|
||||
- name: install Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 12.x
|
||||
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: build
|
||||
run: make build-all
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: set up docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: build and compress image
|
||||
uses: docker/build-push-action@v4
|
||||
- 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
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: scan vulnerabilities by Trivy
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
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 }}
|
||||
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
|
||||
- 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 built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -198,45 +188,46 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest 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: 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 html file as artifact
|
||||
- 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 different vulnerabilities against develop branch
|
||||
- name: Analyse the image diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
|
||||
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
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 Against develop Branch
|
||||
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:
|
||||
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
|
||||
|
||||
- name: Check job status of diff result
|
||||
if: >-
|
||||
matrix.jsdiff.status == 'failure' ||
|
||||
matrix.godiff.status == 'failure' ||
|
||||
matrix.imagediff.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 }}"
|
||||
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
|
||||
|
||||
26
.github/workflows/test.yaml
vendored
26
.github/workflows/test.yaml
vendored
@@ -8,18 +8,22 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn jest --maxWorkers=2
|
||||
test-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
- name: Run tests
|
||||
run: make test-server
|
||||
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 ./...
|
||||
|
||||
29
.github/workflows/validate-openapi-spec.yaml
vendored
29
.github/workflows/validate-openapi-spec.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Validate OpenAPI specs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Download golang modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Validate OpenAPI Spec
|
||||
run: make docs-validate
|
||||
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
53
.github/workflows/validate-openapi-spec.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup Go v1.17.3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17.3'
|
||||
|
||||
- name: Prebuild docs
|
||||
run: yarn prebuild:docs
|
||||
|
||||
- name: Build OpenAPI 2.0 Spec
|
||||
run: yarn build:docs
|
||||
|
||||
# Install dependencies globally to bypass installing all frontend deps
|
||||
- name: Install swagger2openapi and swagger-cli
|
||||
run: yarn global add swagger2openapi @apidevtools/swagger-cli
|
||||
|
||||
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
|
||||
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
|
||||
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
|
||||
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
|
||||
run: yarn validate:docs
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,10 +11,8 @@ storybook-static
|
||||
*.DS_Store
|
||||
|
||||
.eslintcache
|
||||
__debug_bin*
|
||||
__debug_bin
|
||||
|
||||
api/docs
|
||||
.idea
|
||||
.env
|
||||
go.work.sum
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
38
.storybook/main.js
Normal file
38
.storybook/main.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
cssLoaderOptions: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[path][name]__[local]',
|
||||
auto: true,
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: (config) => {
|
||||
config.resolve.plugins = [
|
||||
...(config.resolve.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve.extensions,
|
||||
}),
|
||||
];
|
||||
return config;
|
||||
},
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
staticDirs: ['./public'],
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
import { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
import postcss from 'postcss';
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../app/**/*.stories.@(ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
name: '@storybook/addon-styling',
|
||||
options: {
|
||||
cssLoaderOptions: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[path][name]__[local]',
|
||||
auto: true,
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
postCss: {
|
||||
implementation: postcss,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: (config) => {
|
||||
const rules = config?.module?.rules || [];
|
||||
|
||||
const imageRule = rules.find((rule) => {
|
||||
const test = (rule as { test: RegExp }).test;
|
||||
|
||||
if (!test) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return test.test('.svg');
|
||||
}) as { [key: string]: any };
|
||||
|
||||
imageRule.exclude = /\.svg$/;
|
||||
|
||||
rules.unshift({
|
||||
test: /\.svg$/i,
|
||||
type: 'asset',
|
||||
resourceQuery: {
|
||||
not: [/c/],
|
||||
}, // exclude react component if *.svg?url
|
||||
});
|
||||
|
||||
rules.unshift({
|
||||
test: /\.svg$/i,
|
||||
issuer: /\.(js|ts)(x)?$/,
|
||||
resourceQuery: /c/,
|
||||
// *.svg?c
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
plugins: [
|
||||
...(config.resolve?.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve?.extensions,
|
||||
}),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
...config.module,
|
||||
rules,
|
||||
},
|
||||
} satisfies Configuration;
|
||||
},
|
||||
staticDirs: ['./public'],
|
||||
typescript: {
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
},
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -15,15 +15,6 @@
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"React Named Export Component": {
|
||||
"prefix": "rnec",
|
||||
"body": [
|
||||
"export function $TM_FILENAME_BASE() {",
|
||||
" return <div>$TM_FILENAME_BASE</div>;",
|
||||
"}"
|
||||
],
|
||||
"description": "React Named Export Component"
|
||||
},
|
||||
"Component": {
|
||||
"scope": "javascript",
|
||||
"prefix": "mycomponent",
|
||||
|
||||
@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
|
||||
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
|
||||
|
||||
```
|
||||
#271 feat(containers): add exposed ports in the containers view
|
||||
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
|
||||
|
||||
## Contribution process
|
||||
|
||||
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||
|
||||
### Bug report
|
||||
|
||||
@@ -79,42 +79,30 @@ The feature request process is similar to the bug report process but has an extr
|
||||
|
||||
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
||||
|
||||
Install dependencies:
|
||||
Install dependencies with yarn:
|
||||
|
||||
```sh
|
||||
$ make deps
|
||||
$ yarn
|
||||
```
|
||||
|
||||
Then build and run the project in a Docker container:
|
||||
|
||||
```sh
|
||||
$ make dev
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
Portainer server can now be accessed at <https://localhost:9443>. and UI dev server runs on <http://localhost:8999>.
|
||||
Portainer can now be accessed at <https://localhost:9443>.
|
||||
|
||||
if you want to build the project you can run:
|
||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||
|
||||
```sh
|
||||
make build-all
|
||||
```
|
||||
### Build customisation
|
||||
|
||||
For additional make commands, run `make help`.
|
||||
|
||||
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
|
||||
|
||||
### Build customization
|
||||
|
||||
You can customize the following settings:
|
||||
You can customise the following settings:
|
||||
|
||||
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
|
||||
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
||||
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
||||
|
||||
## Testing your build
|
||||
|
||||
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
|
||||
|
||||
## Adding api docs
|
||||
|
||||
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
||||
|
||||
126
Makefile
126
Makefile
@@ -1,126 +0,0 @@
|
||||
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
|
||||
# For a list of valid GOOS and GOARCH values
|
||||
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
|
||||
PLATFORM=$(shell go env GOOS)
|
||||
ARCH=$(shell go env GOARCH)
|
||||
|
||||
# build target, can be one of "production", "testing", "development"
|
||||
ENV=development
|
||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
TAG=latest
|
||||
|
||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.8.11
|
||||
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||
|
||||
# Don't change anything below this line unless you know what you're doing
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
|
||||
##@ Building
|
||||
.PHONY: init-dist build-storybook build build-client build-server build-image devops
|
||||
init-dist:
|
||||
@mkdir -p dist
|
||||
|
||||
build-all: deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
|
||||
|
||||
build-client: init-dist ## Build the client
|
||||
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
|
||||
|
||||
build-server: init-dist ## Build the server binary
|
||||
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
build-image: build-all ## Build the Portainer image locally
|
||||
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
||||
|
||||
build-storybook: ## Build and serve the storybook files
|
||||
yarn storybook:build
|
||||
|
||||
devops: clean deps build-client ## Build the everything target specifically for CI
|
||||
echo "Building the devops binary..."
|
||||
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
##@ Build dependencies
|
||||
.PHONY: deps server-deps client-deps tidy
|
||||
deps: server-deps client-deps ## Download all client and server build dependancies
|
||||
|
||||
server-deps: init-dist ## Download dependant server binaries
|
||||
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||
|
||||
client-deps: ## Install client dependencies
|
||||
yarn
|
||||
|
||||
tidy: ## Tidy up the go.mod file
|
||||
cd api && go mod tidy
|
||||
|
||||
|
||||
##@ Cleanup
|
||||
.PHONY: clean
|
||||
clean: ## Remove all build and download artifacts
|
||||
@echo "Clearing the dist directory..."
|
||||
@rm -rf dist/*
|
||||
|
||||
|
||||
##@ Testing
|
||||
.PHONY: test test-client test-server
|
||||
test: test-server test-client ## Run all tests
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test
|
||||
|
||||
test-server: ## Run server tests
|
||||
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
||||
|
||||
##@ Dev
|
||||
.PHONY: dev dev-client dev-server
|
||||
dev: ## Run both the client and server in development mode
|
||||
make dev-server
|
||||
make dev-client
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
|
||||
dev-server: build-server ## Run the server in development mode
|
||||
@./dev/run_container.sh
|
||||
|
||||
|
||||
##@ Format
|
||||
.PHONY: format format-client format-server
|
||||
|
||||
format: format-client format-server ## Format all code
|
||||
|
||||
format-client: ## Format client code
|
||||
yarn format
|
||||
|
||||
format-server: ## Format server code
|
||||
cd api && go fmt ./...
|
||||
|
||||
##@ Lint
|
||||
.PHONY: lint lint-client lint-server
|
||||
lint: lint-client lint-server ## Lint all code
|
||||
|
||||
lint-client: ## Lint client code
|
||||
yarn lint
|
||||
|
||||
lint-server: ## Lint server code
|
||||
cd api && go vet ./...
|
||||
|
||||
|
||||
##@ Extension
|
||||
.PHONY: dev-extension
|
||||
dev-extension: build-server build-client ## Run the extension in development mode
|
||||
make local -f build/docker-extension/Makefile
|
||||
|
||||
|
||||
##@ Docs
|
||||
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
||||
docs-build: init-dist ## Build docs
|
||||
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
|
||||
|
||||
docs-validate: docs-build ## Validate docs
|
||||
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||
yarn swagger-cli validate dist/docs/openapi.yaml
|
||||
|
||||
##@ Helpers
|
||||
.PHONY: help
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
15
README.md
15
README.md
@@ -9,7 +9,7 @@ Portainer consists of a single container that can run on any cluster. It can be
|
||||
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
|
||||
|
||||
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
|
||||
- [Take3 – get 3 free nodes of Portainer Business for as long as you want them](https://www.portainer.io/take-3)
|
||||
- [Take5 – get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
|
||||
- [Portainer BE install guide](https://install.portainer.io)
|
||||
|
||||
## Latest Version
|
||||
@@ -21,8 +21,8 @@ Portainer CE is updated regularly. We aim to do an update release every couple o
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://docs.portainer.io/start/install)
|
||||
- [Documentation](https://docs.portainer.io)
|
||||
- [Contribute to the project](https://docs.portainer.io/contribute/contribute)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
|
||||
|
||||
## Features & Functions
|
||||
|
||||
@@ -30,22 +30,23 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
|
||||
|
||||
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
|
||||
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
|
||||
- [Portainer CE for Azure ACI](https://www.portainer.io/solutions/serverless-containers)
|
||||
|
||||
## Getting help
|
||||
|
||||
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
||||
|
||||
Learn more about Portainer's community support channels [here.](https://www.portainer.io/get-support-for-portainer)
|
||||
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
|
||||
|
||||
You can join the Portainer Community by visiting [https://www.portainer.io/join-our-community](https://www.portainer.io/join-our-community). This will give you advance notice of events, content and other related Portainer content.
|
||||
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
|
||||
|
||||
## Reporting bugs and contributing
|
||||
|
||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://docs.portainer.io/contribute/contribute) to build it locally and make a pull request.
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
|
||||
|
||||
## Security
|
||||
|
||||
@@ -59,7 +60,7 @@ If you are a developer, and our code in this repo makes sense to you, we would l
|
||||
|
||||
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
|
||||
|
||||
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/privacy-policy). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
|
||||
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
|
||||
|
||||
## Limitations
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
linters:
|
||||
# Disable all linters, the defaults don't pass on our code yet
|
||||
disable-all: true
|
||||
|
||||
# Enable these for now
|
||||
enable:
|
||||
- depguard
|
||||
- govet
|
||||
- errorlint
|
||||
- exportloopref
|
||||
linters-settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
deny:
|
||||
- pkg: 'github.com/sirupsen/logrus'
|
||||
desc: 'logging is allowed only by github.com/rs/zerolog'
|
||||
- pkg: 'golang.org/x/exp'
|
||||
desc: 'exp is not allowed'
|
||||
files:
|
||||
- '!**/*_test.go'
|
||||
- '!**/base.go'
|
||||
- '!**/base_tx.go'
|
||||
|
||||
# errorlint is causing a typecheck error for some reason. The go compiler will report these
|
||||
# anyway, so ignore them from the linter
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: ./
|
||||
linters:
|
||||
- typecheck
|
||||
@@ -21,7 +21,7 @@ type Monitor struct {
|
||||
datastore dataservices.DataStore
|
||||
shutdownCtx context.Context
|
||||
cancellationFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
mu sync.Mutex
|
||||
adminInitDisabled bool
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ func (m *Monitor) Start() {
|
||||
case <-time.After(m.timeout):
|
||||
initialized, err := m.WasInitialized()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
|
||||
return
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
if !initialized {
|
||||
@@ -83,7 +82,6 @@ func (m *Monitor) Stop() {
|
||||
if m.cancellationFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.cancellationFunc()
|
||||
m.cancellationFunc = nil
|
||||
}
|
||||
@@ -94,14 +92,12 @@ func (m *Monitor) WasInitialized() (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(users) > 0, nil
|
||||
}
|
||||
|
||||
func (m *Monitor) WasInstanceDisabled() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.adminInitDisabled
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -43,9 +42,7 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
@@ -15,3 +18,13 @@ type APIKeyService interface {
|
||||
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
||||
InvalidateUserKeyCache(userId portainer.UserID) bool
|
||||
}
|
||||
|
||||
// generateRandomKey generates a random key of specified length
|
||||
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
|
||||
func generateRandomKey(length int) []byte {
|
||||
k := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||
return nil
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package apikey
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/securecookie"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -34,7 +33,7 @@ func Test_generateRandomKey(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := securecookie.GenerateRandomKey(tt.wantLenth)
|
||||
got := generateRandomKey(tt.wantLenth)
|
||||
is.Equal(tt.wantLenth, len(got))
|
||||
})
|
||||
}
|
||||
@@ -42,7 +41,7 @@ func Test_generateRandomKey(t *testing.T) {
|
||||
t.Run("Generated keys are unique", func(t *testing.T) {
|
||||
keys := make(map[string]bool)
|
||||
for i := 0; i < 100; i++ {
|
||||
key := securecookie.GenerateRandomKey(8)
|
||||
key := generateRandomKey(8)
|
||||
_, ok := keys[string(key)]
|
||||
is.False(ok)
|
||||
keys[string(key)] = true
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/securecookie"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -40,7 +39,7 @@ func (a *apiKeyService) HashRaw(rawKey string) []byte {
|
||||
// GenerateApiKey generates a raw API key for a user (for one-time display).
|
||||
// The generated API key is stored in the cache and database.
|
||||
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
|
||||
randKey := securecookie.GenerateRandomKey(32)
|
||||
randKey := generateRandomKey(32)
|
||||
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
||||
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
||||
|
||||
@@ -54,7 +53,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
|
||||
Digest: hashDigest,
|
||||
}
|
||||
|
||||
err := a.apiKeyRepository.Create(apiKey)
|
||||
err := a.apiKeyRepository.CreateAPIKey(apiKey)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "Unable to create API key")
|
||||
}
|
||||
@@ -67,7 +66,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
|
||||
|
||||
// GetAPIKey returns an API key by its ID.
|
||||
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
||||
return a.apiKeyRepository.Read(apiKeyID)
|
||||
return a.apiKeyRepository.GetAPIKey(apiKeyID)
|
||||
}
|
||||
|
||||
// GetAPIKeys returns all the API keys associated to a user.
|
||||
@@ -89,7 +88,7 @@ func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, port
|
||||
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
|
||||
}
|
||||
|
||||
user, err := a.userRepository.Read(apiKey.UserID)
|
||||
user, err := a.userRepository.User(apiKey.UserID)
|
||||
if err != nil {
|
||||
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
|
||||
}
|
||||
@@ -107,20 +106,20 @@ func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
|
||||
return errors.Wrap(err, "Unable to retrieve API key")
|
||||
}
|
||||
a.cache.Set(apiKey.Digest, user, *apiKey)
|
||||
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
|
||||
return a.apiKeyRepository.UpdateAPIKey(apiKey)
|
||||
}
|
||||
|
||||
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
|
||||
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
|
||||
// get api-key digest to remove from cache
|
||||
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
|
||||
apiKey, err := a.apiKeyRepository.GetAPIKey(apiKeyID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
|
||||
}
|
||||
|
||||
// delete the user/api-key from cache
|
||||
a.cache.Delete(apiKey.Digest)
|
||||
return a.apiKeyRepository.Delete(apiKeyID)
|
||||
return a.apiKeyRepository.DeleteAPIKey(apiKeyID)
|
||||
}
|
||||
|
||||
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
|
||||
|
||||
@@ -22,7 +22,8 @@ func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
|
||||
func Test_GenerateApiKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -75,7 +76,8 @@ func Test_GenerateApiKey(t *testing.T) {
|
||||
func Test_GetAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -94,7 +96,8 @@ func Test_GetAPIKey(t *testing.T) {
|
||||
func Test_GetAPIKeys(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -114,7 +117,8 @@ func Test_GetAPIKeys(t *testing.T) {
|
||||
func Test_GetDigestUserAndKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -149,7 +153,8 @@ func Test_GetDigestUserAndKey(t *testing.T) {
|
||||
func Test_UpdateAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -194,7 +199,8 @@ func Test_UpdateAPIKey(t *testing.T) {
|
||||
func Test_DeleteAPIKey(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
@@ -234,7 +240,8 @@ func Test_DeleteAPIKey(t *testing.T) {
|
||||
func Test_InvalidateUserKeyCache(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package archive
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -85,7 +84,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
|
||||
if errors.Is(err, io.EOF) {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -110,7 +109,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
||||
}
|
||||
outFile.Close()
|
||||
default:
|
||||
return fmt.Errorf("tar: unknown type: %v in %s",
|
||||
return fmt.Errorf("Tar: uknown type: %v in %s",
|
||||
header.Typeflag,
|
||||
header.Name)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -27,10 +28,10 @@ func listFiles(dir string) []string {
|
||||
func Test_shouldCreateArhive(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
content := []byte("content")
|
||||
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
|
||||
gzPath, err := TarGzDir(tmpdir)
|
||||
assert.Nil(t, err)
|
||||
@@ -47,7 +48,7 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
wasExtracted := func(p string) {
|
||||
fullpath := path.Join(extractionDir, p)
|
||||
assert.Contains(t, extractedFiles, fullpath)
|
||||
copyContent, _ := os.ReadFile(fullpath)
|
||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
||||
assert.Equal(t, content, copyContent)
|
||||
}
|
||||
|
||||
@@ -59,10 +60,10 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
content := []byte("content")
|
||||
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
|
||||
gzPath, err := TarGzDir(tmpdir)
|
||||
assert.Nil(t, err)
|
||||
@@ -79,7 +80,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
wasExtracted := func(p string) {
|
||||
fullpath := path.Join(extractionDir, p)
|
||||
assert.Contains(t, extractedFiles, fullpath)
|
||||
copyContent, _ := os.ReadFile(fullpath)
|
||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
||||
assert.Equal(t, content, copyContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
||||
@@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnzipFile(t *testing.T) {
|
||||
|
||||
@@ -1,111 +1,110 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
// import (
|
||||
// "fmt"
|
||||
// "os"
|
||||
// "path"
|
||||
// "path/filepath"
|
||||
// "time"
|
||||
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
// "github.com/portainer/portainer/api/archive"
|
||||
// "github.com/portainer/portainer/api/crypto"
|
||||
// "github.com/portainer/portainer/api/dataservices"
|
||||
// "github.com/portainer/portainer/api/filesystem"
|
||||
// "github.com/portainer/portainer/api/http/offlinegate"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
// "github.com/pkg/errors"
|
||||
// "github.com/rs/zerolog/log"
|
||||
// )
|
||||
|
||||
const rwxr__r__ os.FileMode = 0744
|
||||
// const rwxr__r__ os.FileMode = 0744
|
||||
|
||||
var filesToBackup = []string{
|
||||
"certs",
|
||||
"compose",
|
||||
"config.json",
|
||||
"custom_templates",
|
||||
"edge_jobs",
|
||||
"edge_stacks",
|
||||
"extensions",
|
||||
"portainer.key",
|
||||
"portainer.pub",
|
||||
"tls",
|
||||
"chisel",
|
||||
}
|
||||
// var filesToBackup = []string{
|
||||
// "certs",
|
||||
// "compose",
|
||||
// "config.json",
|
||||
// "custom_templates",
|
||||
// "edge_jobs",
|
||||
// "edge_stacks",
|
||||
// "extensions",
|
||||
// "portainer.key",
|
||||
// "portainer.pub",
|
||||
// "tls",
|
||||
// }
|
||||
|
||||
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
|
||||
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
|
||||
unlock := gate.Lock()
|
||||
defer unlock()
|
||||
// // Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
|
||||
// func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
|
||||
// unlock := gate.Lock()
|
||||
// defer unlock()
|
||||
|
||||
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
|
||||
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
|
||||
return "", errors.Wrap(err, "Failed to create backup dir")
|
||||
}
|
||||
// backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
|
||||
// if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
|
||||
// return "", errors.Wrap(err, "Failed to create backup dir")
|
||||
// }
|
||||
|
||||
{
|
||||
// new export
|
||||
exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
||||
// {
|
||||
// // new export
|
||||
// exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
||||
|
||||
err := datastore.Export(exportFilename)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
|
||||
} else {
|
||||
log.Debug().Str("filename", exportFilename).Msg("file exported")
|
||||
}
|
||||
}
|
||||
// err := datastore.Export(exportFilename)
|
||||
// if err != nil {
|
||||
// log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
|
||||
// } else {
|
||||
// log.Debug().Str("filename", exportFilename).Msg("file exported")
|
||||
// }
|
||||
// }
|
||||
|
||||
if err := backupDb(backupDirPath, datastore); err != nil {
|
||||
return "", errors.Wrap(err, "Failed to backup database")
|
||||
}
|
||||
// if err := backupDb(backupDirPath, datastore); err != nil {
|
||||
// return "", errors.Wrap(err, "Failed to backup database")
|
||||
// }
|
||||
|
||||
for _, filename := range filesToBackup {
|
||||
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to create backup file")
|
||||
}
|
||||
}
|
||||
// for _, filename := range filesToBackup {
|
||||
// err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
|
||||
// if err != nil {
|
||||
// return "", errors.Wrap(err, "Failed to create backup file")
|
||||
// }
|
||||
// }
|
||||
|
||||
archivePath, err := archive.TarGzDir(backupDirPath)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to make an archive")
|
||||
}
|
||||
// archivePath, err := archive.TarGzDir(backupDirPath)
|
||||
// if err != nil {
|
||||
// return "", errors.Wrap(err, "Failed to make an archive")
|
||||
// }
|
||||
|
||||
if password != "" {
|
||||
archivePath, err = encrypt(archivePath, password)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
|
||||
}
|
||||
}
|
||||
// if password != "" {
|
||||
// archivePath, err = encrypt(archivePath, password)
|
||||
// if err != nil {
|
||||
// return "", errors.Wrap(err, "Failed to encrypt backup with the password")
|
||||
// }
|
||||
// }
|
||||
|
||||
return archivePath, nil
|
||||
}
|
||||
// return archivePath, nil
|
||||
// }
|
||||
|
||||
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
|
||||
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = datastore.BackupTo(backupWriter); err != nil {
|
||||
return err
|
||||
}
|
||||
return backupWriter.Close()
|
||||
}
|
||||
// func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
|
||||
// backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err = datastore.BackupTo(backupWriter); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return backupWriter.Close()
|
||||
// }
|
||||
|
||||
func encrypt(path string, passphrase string) (string, error) {
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer in.Close()
|
||||
// func encrypt(path string, passphrase string) (string, error) {
|
||||
// in, err := os.Open(path)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// defer in.Close()
|
||||
|
||||
outFileName := fmt.Sprintf("%s.encrypted", path)
|
||||
out, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// outFileName := fmt.Sprintf("%s.encrypted", path)
|
||||
// out, err := os.Create(outFileName)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
err = crypto.AesEncrypt(in, out, []byte(passphrase))
|
||||
// err = crypto.AesEncrypt(in, out, []byte(passphrase))
|
||||
|
||||
return outFileName, err
|
||||
}
|
||||
// return outFileName, err
|
||||
// }
|
||||
|
||||
@@ -1,113 +1,85 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
// import (
|
||||
// "context"
|
||||
// "io"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
)
|
||||
// "github.com/pkg/errors"
|
||||
// "github.com/portainer/portainer/api/archive"
|
||||
// "github.com/portainer/portainer/api/crypto"
|
||||
// "github.com/portainer/portainer/api/dataservices"
|
||||
// "github.com/portainer/portainer/api/filesystem"
|
||||
// "github.com/portainer/portainer/api/http/offlinegate"
|
||||
// )
|
||||
|
||||
var filesToRestore = append(filesToBackup, "portainer.db")
|
||||
// var filesToRestore = append(filesToBackup, "portainer.db")
|
||||
|
||||
// Restores system state from backup archive, will trigger system shutdown, when finished.
|
||||
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
|
||||
var err error
|
||||
if password != "" {
|
||||
archive, err = decrypt(archive, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to decrypt the archive")
|
||||
}
|
||||
}
|
||||
// // Restores system state from backup archive, will trigger system shutdown, when finished.
|
||||
// func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, shutdownTrigger context.CancelFunc) error {
|
||||
// var err error
|
||||
// if password != "" {
|
||||
// archive, err = decrypt(archive, password)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "failed to decrypt the archive")
|
||||
// }
|
||||
// }
|
||||
|
||||
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
|
||||
defer os.RemoveAll(filepath.Dir(restorePath))
|
||||
// restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
|
||||
// defer os.RemoveAll(filepath.Dir(restorePath))
|
||||
|
||||
err = extractArchive(archive, restorePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
|
||||
}
|
||||
// err = extractArchive(archive, restorePath)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
|
||||
// }
|
||||
|
||||
unlock := gate.Lock()
|
||||
defer unlock()
|
||||
// unlock := gate.Lock()
|
||||
// defer unlock()
|
||||
|
||||
if err = datastore.Close(); err != nil {
|
||||
return errors.Wrap(err, "Failed to stop db")
|
||||
}
|
||||
// if err = datastore.Close(); err != nil {
|
||||
// return errors.Wrap(err, "Failed to stop db")
|
||||
// }
|
||||
|
||||
// At some point, backups were created containing a subdirectory, now we need to handle both
|
||||
restorePath, err = getRestoreSourcePath(restorePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
|
||||
}
|
||||
// if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||
// return errors.Wrap(err, "failed to restore the system state")
|
||||
// }
|
||||
|
||||
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||
return errors.Wrap(err, "failed to restore the system state")
|
||||
}
|
||||
// shutdownTrigger()
|
||||
// return nil
|
||||
// }
|
||||
|
||||
shutdownTrigger()
|
||||
return nil
|
||||
}
|
||||
// func decrypt(r io.Reader, password string) (io.Reader, error) {
|
||||
// return crypto.AesDecrypt(r, []byte(password))
|
||||
// }
|
||||
|
||||
func decrypt(r io.Reader, password string) (io.Reader, error) {
|
||||
return crypto.AesDecrypt(r, []byte(password))
|
||||
}
|
||||
// func extractArchive(r io.Reader, destinationDirPath string) error {
|
||||
// return archive.ExtractTarGz(r, destinationDirPath)
|
||||
// }
|
||||
|
||||
func extractArchive(r io.Reader, destinationDirPath string) error {
|
||||
return archive.ExtractTarGz(r, destinationDirPath)
|
||||
}
|
||||
// func restoreFiles(srcDir string, destinationDir string) error {
|
||||
// for _, filename := range filesToRestore {
|
||||
// err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
func getRestoreSourcePath(dir string) (string, error) {
|
||||
// find portainer.db or portainer.edb file. Return the parent directory
|
||||
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
|
||||
// // TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
|
||||
|
||||
backupDirPath := dir
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// // Prevent the possibility of having both databases. Remove any default new instance
|
||||
// os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
|
||||
// os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
|
||||
|
||||
if portainerdbRegex.MatchString(d.Name()) {
|
||||
backupDirPath = filepath.Dir(path)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// // Now copy the database. It'll be either portainer.db or portainer.edb
|
||||
|
||||
return backupDirPath, err
|
||||
}
|
||||
// // Note: CopyPath does not return an error if the source file doesn't exist
|
||||
// err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
func restoreFiles(srcDir string, destinationDir string) error {
|
||||
for _, filename := range filesToRestore {
|
||||
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
|
||||
|
||||
// Prevent the possibility of having both databases. Remove any default new instance
|
||||
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
|
||||
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
|
||||
|
||||
// Now copy the database. It'll be either portainer.db or portainer.edb
|
||||
|
||||
// Note: CopyPath does not return an error if the source file doesn't exist
|
||||
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
|
||||
}
|
||||
// // return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
chshare "github.com/jpillora/chisel/share"
|
||||
)
|
||||
|
||||
var one = new(big.Int).SetInt64(1)
|
||||
|
||||
// GenerateGo119CompatibleKey This function is basically copied from chshare.GenerateKey.
|
||||
func GenerateGo119CompatibleKey(seed string) ([]byte, error) {
|
||||
r := chshare.NewDetermRand([]byte(seed))
|
||||
priv, err := ecdsaGenerateKey(elliptic.P256(), r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to marshal ECDSA private key: %w", err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil
|
||||
}
|
||||
|
||||
// This function is copied from Go1.19
|
||||
func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) {
|
||||
params := c.Params()
|
||||
// Note that for P-521 this will actually be 63 bits more than the order, as
|
||||
// division rounds down, but the extra bit is inconsequential.
|
||||
b := make([]byte, params.N.BitLen()/8+8)
|
||||
_, err = io.ReadFull(rand, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k = new(big.Int).SetBytes(b)
|
||||
n := new(big.Int).Sub(params.N, one)
|
||||
k.Mod(k, n)
|
||||
k.Add(k, one)
|
||||
return
|
||||
}
|
||||
|
||||
// This function is copied from Go1.19
|
||||
func ecdsaGenerateKey(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) {
|
||||
k, err := randFieldElement(c, rand)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priv := new(ecdsa.PrivateKey)
|
||||
priv.PublicKey.Curve = c
|
||||
priv.D = k
|
||||
priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes())
|
||||
return priv, nil
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateGo119CompatibleKey(t *testing.T) {
|
||||
type args struct {
|
||||
seed string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Generate Go 1.19 compatible private key with a given seed",
|
||||
args: args{seed: "94qh17MCIk8BOkiI"},
|
||||
want: []byte("-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHeohwk0Gy3RHVVViaHz7pz/HOiqA7fkv1FTM3mGgfT3oAoGCCqGSM49\nAwEHoUQDQgAEN7riX06xDsLNPuUmOvYFluNEakcFwZZRVvOcIYk/9VYnanDzW0Km\n8/BUUiKyJDuuGdS4fj9SlQ4iL8yBK01uKg==\n-----END EC PRIVATE KEY-----\n"),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GenerateGo119CompatibleKey(tt.args.seed)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GenerateGo119CompatibleKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GenerateGo119CompatibleKey()\ngot: Z %v\nwant: %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,17 @@ package chisel
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
)
|
||||
|
||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return
|
||||
}
|
||||
|
||||
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpoint.ID)
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
existingJobIndex := -1
|
||||
for idx, existingJob := range tunnel.Jobs {
|
||||
if existingJob.ID == edgeJob.ID {
|
||||
existingJobIndex = idx
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -29,8 +23,6 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
|
||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
||||
}
|
||||
|
||||
cache.Del(endpoint.ID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -38,7 +30,8 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
|
||||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||
service.mu.Lock()
|
||||
|
||||
for endpointID, tunnel := range service.tunnelDetailsMap {
|
||||
for _, tunnel := range service.tunnelDetailsMap {
|
||||
// Filter in-place
|
||||
n := 0
|
||||
for _, edgeJob := range tunnel.Jobs {
|
||||
if edgeJob.ID != edgeJobID {
|
||||
@@ -48,28 +41,7 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||
}
|
||||
|
||||
tunnel.Jobs = tunnel.Jobs[:n]
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
n := 0
|
||||
for _, edgeJob := range tunnel.Jobs {
|
||||
if edgeJob.ID != edgeJobID {
|
||||
tunnel.Jobs[n] = edgeJob
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
tunnel.Jobs = tunnel.Jobs[:n]
|
||||
|
||||
cache.Del(endpointID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,8 +11,8 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
chserver "github.com/jpillora/chisel/server"
|
||||
"github.com/jpillora/chisel/share/ccrypto"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -36,16 +35,14 @@ type Service struct {
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
mu sync.Mutex
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
|
||||
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
|
||||
return &Service{
|
||||
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
||||
dataStore: dataStore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
fileService: fileService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +58,7 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
||||
httpClient := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
_, err = httpClient.Do(req)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -75,11 +68,10 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("max_alive_minutes", maxAlive.Minutes()).
|
||||
Msg("KeepTunnelAlive: start")
|
||||
Msg("start")
|
||||
|
||||
maxAliveTicker := time.NewTicker(maxAlive)
|
||||
defer maxAliveTicker.Stop()
|
||||
|
||||
pingTicker := time.NewTicker(tunnelCleanupInterval)
|
||||
defer pingTicker.Stop()
|
||||
|
||||
@@ -92,13 +84,13 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("KeepTunnelAlive: ping agent")
|
||||
Msg("ping agent")
|
||||
}
|
||||
case <-maxAliveTicker.C:
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Float64("timeout_minutes", maxAlive.Minutes()).
|
||||
Msg("KeepTunnelAlive: tunnel keep alive timeout")
|
||||
Msg("tunnel keep alive timeout")
|
||||
|
||||
return
|
||||
case <-ctx.Done():
|
||||
@@ -106,7 +98,7 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Err(err).
|
||||
Msg("KeepTunnelAlive: tunnel stop")
|
||||
Msg("tunnel stop")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -120,15 +112,14 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
||||
// It starts the tunnel status verification process in the background.
|
||||
// The snapshotter is used in the tunnel status verification process.
|
||||
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
|
||||
privateKeyFile, err := service.retrievePrivateKeyFile()
|
||||
|
||||
keySeed, err := service.retrievePrivateKeySeed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &chserver.Config{
|
||||
Reverse: true,
|
||||
PrivateKeyFile: privateKeyFile,
|
||||
Reverse: true,
|
||||
KeySeed: keySeed,
|
||||
}
|
||||
|
||||
chiselServer, err := chserver.NewServer(config)
|
||||
@@ -164,41 +155,26 @@ func (service *Service) StopTunnelServer() error {
|
||||
return service.chiselServer.Close()
|
||||
}
|
||||
|
||||
func (service *Service) retrievePrivateKeyFile() (string, error) {
|
||||
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
|
||||
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
||||
var serverInfo *portainer.TunnelServerInfo
|
||||
|
||||
exist, _ := service.fileService.FileExists(privateKeyFile)
|
||||
if !exist {
|
||||
log.Debug().
|
||||
Str("private-key", privateKeyFile).
|
||||
Msg("Chisel private key file does not exist")
|
||||
serverInfo, err := service.dataStore.TunnelServer().Info()
|
||||
if service.dataStore.IsErrObjectNotFound(err) {
|
||||
keySeed := uniuri.NewLen(16)
|
||||
|
||||
privateKey, err := ccrypto.GenerateKey("")
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to generate chisel private key")
|
||||
return "", err
|
||||
serverInfo = &portainer.TunnelServerInfo{
|
||||
PrivateKeySeed: keySeed,
|
||||
}
|
||||
|
||||
err = service.fileService.StoreChiselPrivateKey(privateKey)
|
||||
err := service.dataStore.TunnelServer().UpdateInfo(serverInfo)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to save Chisel private key to disk")
|
||||
return "", err
|
||||
} else {
|
||||
log.Info().
|
||||
Str("private-key", privateKeyFile).
|
||||
Msg("Generated a new Chisel private key file")
|
||||
}
|
||||
} else {
|
||||
log.Info().
|
||||
Str("private-key", privateKeyFile).
|
||||
Msg("Found Chisel private key file on disk")
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return privateKeyFile, nil
|
||||
return serverInfo.PrivateKeySeed, nil
|
||||
}
|
||||
|
||||
func (service *Service) startTunnelVerificationLoop() {
|
||||
@@ -230,23 +206,15 @@ func (service *Service) checkTunnels() {
|
||||
|
||||
service.mu.Lock()
|
||||
for key, tunnel := range service.tunnelDetailsMap {
|
||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||
continue
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
|
||||
continue
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
|
||||
continue
|
||||
}
|
||||
|
||||
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.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
@@ -254,7 +222,9 @@ func (service *Service) checkTunnels() {
|
||||
Float64("status_time_seconds", elapsed.Seconds()).
|
||||
Msg("environment tunnel monitoring")
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Str("status", tunnel.Status).
|
||||
@@ -263,7 +233,9 @@ func (service *Service) checkTunnels() {
|
||||
Msg("REQUIRED state timeout exceeded")
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Str("status", tunnel.Status).
|
||||
@@ -290,7 +262,14 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
|
||||
endpointURL := endpoint.URL
|
||||
|
||||
return service.snapshotService.SnapshotEndpoint(endpoint)
|
||||
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
|
||||
err = service.snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.URL = endpointURL
|
||||
return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@ package chisel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/portainer/libcrypto"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,8 +49,6 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
|
||||
service.tunnelDetailsMap[endpointID] = tunnel
|
||||
|
||||
cache.Del(endpointID)
|
||||
|
||||
return tunnel
|
||||
}
|
||||
|
||||
@@ -67,10 +62,6 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
|
||||
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
|
||||
}
|
||||
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
@@ -108,8 +99,6 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
|
||||
tunnel.Credentials = ""
|
||||
tunnel.LastActivity = time.Now()
|
||||
service.mu.Unlock()
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
|
||||
@@ -126,17 +115,12 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
credentials := tunnel.Credentials
|
||||
if credentials != "" {
|
||||
tunnel.Credentials = ""
|
||||
|
||||
if service.chiselServer != nil {
|
||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||
}
|
||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||
}
|
||||
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
|
||||
service.mu.Unlock()
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
@@ -145,8 +129,6 @@ 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 {
|
||||
defer cache.Del(endpointID)
|
||||
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
service.mu.Lock()
|
||||
@@ -164,12 +146,9 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
|
||||
|
||||
username, password := generateRandomCredentials()
|
||||
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
||||
|
||||
if service.chiselServer != nil {
|
||||
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||
|
||||
@@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||
@@ -62,7 +62,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
|
||||
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
|
||||
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
|
||||
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
@@ -72,7 +71,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||
}
|
||||
|
||||
@@ -81,6 +79,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
// ValidateFlags validates the values of the flags.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
|
||||
displayDeprecationWarnings(flags)
|
||||
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
@@ -111,38 +110,31 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
if endpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
if endpointURL != "" {
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval == "" {
|
||||
return nil
|
||||
if snapshotInterval != "" {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,24 +2,23 @@ package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Confirm starts a rollback db cli application
|
||||
func Confirm(message string) (bool, error) {
|
||||
fmt.Printf("%s [y/N]", message)
|
||||
log.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
answer = strings.ReplaceAll(answer, "\n", "")
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -33,23 +31,3 @@ func setLoggingLevel(level string) {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func setLoggingMode(mode string) {
|
||||
switch mode {
|
||||
case "PRETTY":
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
NoColor: true,
|
||||
TimeFormat: "2006/01/02 03:04PM",
|
||||
FormatMessage: formatMessage})
|
||||
case "JSON":
|
||||
log.Logger = log.Output(os.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func formatMessage(i interface{}) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s |", i)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"math/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
@@ -16,28 +17,22 @@ import (
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/database"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
@@ -45,12 +40,7 @@ import (
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/portainer/portainer/pkg/libhelm"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -79,34 +69,28 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||
}
|
||||
|
||||
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
|
||||
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
|
||||
connection, err := database.NewDatabase("sqlite", *flags.Data, secretKey)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating database connection")
|
||||
}
|
||||
|
||||
if bconn, ok := connection.(*boltdb.DbConnection); ok {
|
||||
bconn.MaxBatchSize = *flags.MaxBatchSize
|
||||
bconn.MaxBatchDelay = *flags.MaxBatchDelay
|
||||
bconn.InitialMmapSize = *flags.InitialMmapSize
|
||||
} else {
|
||||
log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
|
||||
}
|
||||
|
||||
store := datastore.NewStore(*flags.Data, fileService, connection)
|
||||
isNew, err := store.Open()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed opening store")
|
||||
}
|
||||
|
||||
if *flags.Rollback {
|
||||
err := store.Rollback(false)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed rolling back")
|
||||
}
|
||||
// if *flags.Rollback {
|
||||
// err := store.Rollback(false)
|
||||
// if err != nil {
|
||||
// log.Fatal().Err(err).Msg("failed rolling back")
|
||||
// }
|
||||
|
||||
log.Info().Msg("exiting rollback")
|
||||
os.Exit(0)
|
||||
}
|
||||
// log.Info().Msg("exiting rollback")
|
||||
// os.Exit(0)
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Init sets some defaults - it's basically a migration
|
||||
err = store.Init()
|
||||
@@ -115,32 +99,25 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
}
|
||||
|
||||
if isNew {
|
||||
instanceId, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed generating instance id")
|
||||
}
|
||||
|
||||
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
|
||||
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
|
||||
|
||||
// from MigrateData
|
||||
v := models.Version{
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
Edition: int(portainer.PortainerCE),
|
||||
InstanceID: instanceId.String(),
|
||||
MigratorCount: migratorCount,
|
||||
}
|
||||
store.VersionService.UpdateVersion(&v)
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
|
||||
err = updateSettingsFromFlags(store, flags)
|
||||
err := updateSettingsFromFlags(store, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
||||
}
|
||||
} else {
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed migration")
|
||||
}
|
||||
// storedVersion, err := store.VersionService.DBVersion()
|
||||
// if err != nil {
|
||||
// log.Fatal().Err(err).Msg("failure during creation of new database")
|
||||
// }
|
||||
|
||||
// if storedVersion != portainer.DBVersion {
|
||||
// err = store.MigrateData()
|
||||
// if err != nil {
|
||||
// log.Fatal().Err(err).Msg("failed migration")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
err = updateSettingsFromFlags(store, flags)
|
||||
@@ -152,23 +129,22 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
go func() {
|
||||
<-shutdownCtx.Done()
|
||||
defer connection.Close()
|
||||
|
||||
// exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
||||
|
||||
// err := store.Export(exportFilename)
|
||||
// if err != nil {
|
||||
// log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
|
||||
// } else {
|
||||
// log.Debug().Str("filename", exportFilename).Msg("exported")
|
||||
// }
|
||||
}()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// checkDBSchemaServerVersionMatch checks if the server version matches the db scehma version
|
||||
func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersion string, serverEdition int) bool {
|
||||
v, err := dbStore.Version().Version()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||
}
|
||||
|
||||
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating compose manager")
|
||||
}
|
||||
@@ -249,18 +225,18 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
|
||||
return sslService, nil
|
||||
}
|
||||
|
||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
|
||||
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
|
||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
|
||||
}
|
||||
|
||||
func initSnapshotService(
|
||||
snapshotIntervalFromFlag string,
|
||||
dataStore dataservices.DataStore,
|
||||
dockerClientFactory *dockerclient.ClientFactory,
|
||||
dockerClientFactory *docker.ClientFactory,
|
||||
kubernetesClientFactory *kubecli.ClientFactory,
|
||||
shutdownCtx context.Context,
|
||||
) (portainer.SnapshotService, error) {
|
||||
@@ -330,7 +306,51 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
||||
sslSettings.HTTPEnabled = true
|
||||
}
|
||||
|
||||
return dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableFeaturesFromFlags turns on or off feature flags
|
||||
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
|
||||
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
|
||||
func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings == nil {
|
||||
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
}
|
||||
|
||||
// loop through feature flags to check if they are supported
|
||||
for _, feat := range *flags.FeatureFlags {
|
||||
var correspondingFeature *portainer.Feature
|
||||
for i, supportedFeat := range portainer.SupportedFeatureFlags {
|
||||
if strings.EqualFold(feat.Name, string(supportedFeat)) {
|
||||
correspondingFeature = &portainer.SupportedFeatureFlags[i]
|
||||
}
|
||||
}
|
||||
|
||||
if correspondingFeature == nil {
|
||||
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
|
||||
}
|
||||
|
||||
featureState, err := strconv.ParseBool(feat.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
|
||||
}
|
||||
|
||||
log.Info().Str("feature", string(*correspondingFeature)).Bool("state", featureState).Msg("")
|
||||
|
||||
settings.FeatureFlagSettings[*correspondingFeature] = featureState
|
||||
}
|
||||
|
||||
return dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
@@ -362,6 +382,147 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
|
||||
return generateAndStoreKeyPair(fileService, signatureService)
|
||||
}
|
||||
|
||||
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
tlsConfiguration := portainer.TLSConfiguration{
|
||||
TLS: *flags.TLS,
|
||||
TLSSkipVerify: *flags.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if *flags.TLS {
|
||||
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
|
||||
tlsConfiguration.TLSCertPath = *flags.TLSCert
|
||||
tlsConfiguration.TLSKeyPath = *flags.TLSKey
|
||||
} else if !*flags.TLS && *flags.TLSSkipVerify {
|
||||
tlsConfiguration.TLS = true
|
||||
}
|
||||
|
||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: "primary",
|
||||
URL: *flags.EndpointURL,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
Type: portainer.DockerEnvironment,
|
||||
TLSConfig: tlsConfiguration,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agentOnDockerEnvironment {
|
||||
endpoint.Type = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("endpoint", endpoint.Name).
|
||||
Str("URL", endpoint.URL).
|
||||
Err(err).
|
||||
Msg("environment snapshot error")
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().Create(endpoint)
|
||||
}
|
||||
|
||||
func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
if strings.HasPrefix(endpointURL, "tcp://") {
|
||||
_, err := client.ExecutePingOperation(endpointURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: "primary",
|
||||
URL: endpointURL,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
Type: portainer.DockerEnvironment,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("endpoint", endpoint.Name).
|
||||
Str("URL", endpoint.URL).Err(err).
|
||||
Msg("environment snapshot error")
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().Create(endpoint)
|
||||
}
|
||||
|
||||
func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
if *flags.EndpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, err := dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
log.Info().Msg("instance already has defined environments, skipping the environment defined via CLI")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if *flags.TLS || *flags.TLSSkipVerify {
|
||||
return createTLSSecuredEndpoint(flags, dataStore, snapshotService)
|
||||
}
|
||||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
||||
}
|
||||
|
||||
func loadEncryptionSecretKey(keyfilename string) []byte {
|
||||
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
|
||||
if err != nil {
|
||||
@@ -382,10 +543,6 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
|
||||
func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
|
||||
|
||||
if flags.FeatureFlags != nil {
|
||||
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
|
||||
}
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
||||
if encryptionKey == nil {
|
||||
@@ -398,11 +555,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// check if the db schema version matches with server version
|
||||
if !checkDBSchemaServerVersionMatch(dataStore, portainer.APIVersion, int(portainer.Edition)) {
|
||||
log.Fatal().Msg("The database schema version does not align with the server version. Please consider reverting to the previous server version or addressing the database migration issue.")
|
||||
}
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed getting instance id")
|
||||
@@ -420,6 +572,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("failed initializing JWT service")
|
||||
}
|
||||
|
||||
err = enableFeaturesFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed enabling feature flag")
|
||||
}
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
oauthService := initOAuthService()
|
||||
@@ -432,8 +589,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
edgeStacksService := edgestacks.NewService(dataStore)
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
@@ -449,10 +604,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("failed initializing key pair")
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
|
||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||
if err != nil {
|
||||
@@ -473,12 +628,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing compose deployer")
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
||||
if err != nil {
|
||||
@@ -507,10 +657,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
}
|
||||
|
||||
// channel to control when the admin user is created
|
||||
adminCreationDone := make(chan struct{}, 1)
|
||||
|
||||
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing environment")
|
||||
}
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
@@ -545,9 +695,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating admin user")
|
||||
}
|
||||
|
||||
// notify the admin user is created, the endpoint initialization can start
|
||||
adminCreationDone <- struct{}{}
|
||||
} else {
|
||||
log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.")
|
||||
}
|
||||
@@ -559,7 +706,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
|
||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer, dockerClientFactory, dataStore)
|
||||
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
||||
deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||
|
||||
sslDBSettings, err := dataStore.SSLSettings().Settings()
|
||||
@@ -567,24 +714,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||
}
|
||||
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
// Our normal migrations run as part of the database initialization
|
||||
// but some more complex migrations require access to a kubernetes or docker
|
||||
// client. Therefore we run a separate migration process just before
|
||||
// starting the server.
|
||||
postInitMigrator := datastore.NewPostInitMigrator(
|
||||
kubernetesClientFactory,
|
||||
dockerClientFactory,
|
||||
dataStore,
|
||||
)
|
||||
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
AuthorizationService: authorizationService,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
@@ -594,7 +723,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
EdgeStacksService: edgeStacksService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
@@ -620,21 +748,15 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
UpgradeService: upgradeService,
|
||||
AdminCreationDone: adminCreationDone,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
configureLogger()
|
||||
setLoggingMode("PRETTY")
|
||||
|
||||
flags := initCLI()
|
||||
|
||||
setLoggingLevel(*flags.LogLevel)
|
||||
setLoggingMode(*flags.LogMode)
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
|
||||
111
api/cmd/portainer/main_test.go
Normal file
111
api/cmd/portainer/main_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
// import (
|
||||
// "fmt"
|
||||
// "testing"
|
||||
|
||||
// portainer "github.com/portainer/portainer/api"
|
||||
// "github.com/portainer/portainer/api/cli"
|
||||
// "github.com/portainer/portainer/api/dataservices"
|
||||
// "github.com/portainer/portainer/api/datastore"
|
||||
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// "gopkg.in/alecthomas/kingpin.v2"
|
||||
// )
|
||||
|
||||
// type mockKingpinSetting string
|
||||
|
||||
// func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
||||
// value.Set(string(m))
|
||||
// }
|
||||
|
||||
// func Test_enableFeaturesFromFlags(t *testing.T) {
|
||||
// is := assert.New(t)
|
||||
|
||||
// _, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
// defer teardown()
|
||||
|
||||
// tests := []struct {
|
||||
// featureFlag string
|
||||
// isSupported bool
|
||||
// }{
|
||||
// {"test", false},
|
||||
// }
|
||||
// for _, test := range tests {
|
||||
// t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
|
||||
// mockKingpinSetting := mockKingpinSetting(test.featureFlag)
|
||||
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
// err := enableFeaturesFromFlags(store, flags)
|
||||
// if test.isSupported {
|
||||
// is.NoError(err)
|
||||
// } else {
|
||||
// is.Error(err)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// t.Run("passes for all supported feature flags", func(t *testing.T) {
|
||||
// for _, flag := range portainer.SupportedFeatureFlags {
|
||||
// mockKingpinSetting := mockKingpinSetting(flag)
|
||||
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
// err := enableFeaturesFromFlags(store, flags)
|
||||
// is.NoError(err)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// const FeatTest portainer.Feature = "optional-test"
|
||||
|
||||
// func optionalFunc(dataStore dataservices.DataStore) string {
|
||||
|
||||
// // TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
|
||||
// // ideally, the `if` should look more like:
|
||||
// // if featureflags.FlagEnabled(FeatTest) {}
|
||||
// settings, err := dataStore.Settings().Settings()
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
|
||||
// if settings.FeatureFlagSettings[FeatTest] {
|
||||
// return "enabled"
|
||||
// }
|
||||
// return "disabled"
|
||||
// }
|
||||
|
||||
// func Test_optionalFeature(t *testing.T) {
|
||||
// portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
|
||||
|
||||
// is := assert.New(t)
|
||||
|
||||
// _, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
// defer teardown()
|
||||
|
||||
// // Enable the test feature
|
||||
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
// mockKingpinSetting := mockKingpinSetting(FeatTest)
|
||||
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
// err := enableFeaturesFromFlags(store, flags)
|
||||
// is.NoError(err)
|
||||
// is.Equal("enabled", optionalFunc(store))
|
||||
// })
|
||||
|
||||
// // Same store, so the feature flag should still be enabled
|
||||
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
// is.Equal("enabled", optionalFunc(store))
|
||||
// })
|
||||
|
||||
// // disable the test feature
|
||||
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
// mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
|
||||
// flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
// err := enableFeaturesFromFlags(store, flags)
|
||||
// is.NoError(err)
|
||||
// is.Equal("disabled", optionalFunc(store))
|
||||
// })
|
||||
|
||||
// // Same store, so feature flag should still be disabled
|
||||
// t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
// is.Equal("disabled", optionalFunc(store))
|
||||
// })
|
||||
|
||||
// }
|
||||
@@ -1,54 +1,20 @@
|
||||
package portainer
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type ReadTransaction interface {
|
||||
GetObject(bucketName string, key []byte, object 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
|
||||
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
}
|
||||
|
||||
type Transaction interface {
|
||||
ReadTransaction
|
||||
|
||||
SetServiceName(bucketName string) error
|
||||
UpdateObject(bucketName string, key []byte, object interface{}) error
|
||||
DeleteObject(bucketName string, key []byte) error
|
||||
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
|
||||
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
|
||||
GetNextIdentifier(bucketName string) int
|
||||
}
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Connection interface {
|
||||
Transaction
|
||||
|
||||
Open() error
|
||||
Close() error
|
||||
|
||||
UpdateTx(fn func(Transaction) error) error
|
||||
ViewTx(fn func(Transaction) error) error
|
||||
Init() error
|
||||
|
||||
// write the db contents to filename as json (the schema needs defining)
|
||||
ExportRaw(filename string) error
|
||||
GetDB() *gorm.DB
|
||||
|
||||
// TODO: this one is very database specific atm
|
||||
BackupTo(w io.Writer) error
|
||||
GetDatabaseFileName() string
|
||||
GetDatabaseFilePath() string
|
||||
GetStorePath() string
|
||||
|
||||
SetEncrypted(encrypted bool)
|
||||
IsEncryptedStore() bool
|
||||
NeedsEncryptionMigration() (bool, error)
|
||||
SetEncrypted(encrypted bool)
|
||||
|
||||
BackupMetadata() (map[string]interface{}, error)
|
||||
RestoreMetadata(s map[string]interface{}) error
|
||||
|
||||
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
|
||||
ConvertToKey(v int) []byte
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// Person with better knowledge is welcomed to improve it.
|
||||
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
||||
|
||||
var emptySalt []byte = make([]byte, 0)
|
||||
var emptySalt []byte = make([]byte, 0, 0)
|
||||
|
||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
||||
// passphrase is used to generate an encryption key.
|
||||
|
||||
@@ -2,6 +2,7 @@ package crypto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -19,7 +20,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -29,7 +30,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -44,7 +45,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -68,7 +69,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -83,7 +84,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -107,7 +108,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -122,6 +123,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
@@ -114,6 +115,9 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -3,11 +3,11 @@ package crypto
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// CreateTLSConfiguration creates a basic tls.Config with recommended TLS settings
|
||||
func CreateTLSConfiguration() *tls.Config {
|
||||
// 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,
|
||||
CipherSuites: []uint16{
|
||||
@@ -20,8 +20,6 @@ func CreateTLSConfiguration() *tls.Config {
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -29,7 +27,7 @@ func CreateTLSConfiguration() *tls.Config {
|
||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from memory.
|
||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||
config := CreateTLSConfiguration()
|
||||
config := &tls.Config{}
|
||||
config.InsecureSkipVerify = skipServerVerification
|
||||
|
||||
if !skipClientVerification {
|
||||
@@ -52,7 +50,7 @@ func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerific
|
||||
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from disk.
|
||||
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
|
||||
config := CreateTLSConfiguration()
|
||||
config := &tls.Config{}
|
||||
config.InsecureSkipVerify = skipServerVerification
|
||||
|
||||
if certPath != "" && keyPath != "" {
|
||||
@@ -65,7 +63,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe
|
||||
}
|
||||
|
||||
if !skipServerVerification && caCertPath != "" {
|
||||
caCert, err := os.ReadFile(caCertPath)
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
DatabaseFileName = "portainer.db"
|
||||
EncryptedDatabaseFileName = "portainer.edb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
|
||||
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
|
||||
)
|
||||
|
||||
type DbConnection struct {
|
||||
Path string
|
||||
MaxBatchSize int
|
||||
MaxBatchDelay time.Duration
|
||||
InitialMmapSize int
|
||||
EncryptionKey []byte
|
||||
isEncrypted bool
|
||||
|
||||
*bolt.DB
|
||||
}
|
||||
|
||||
// GetDatabaseFileName get the database filename
|
||||
func (connection *DbConnection) GetDatabaseFileName() string {
|
||||
if connection.IsEncryptedStore() {
|
||||
return EncryptedDatabaseFileName
|
||||
}
|
||||
|
||||
return DatabaseFileName
|
||||
}
|
||||
|
||||
// GetDataseFilePath get the path + filename for the database file
|
||||
func (connection *DbConnection) GetDatabaseFilePath() string {
|
||||
if connection.IsEncryptedStore() {
|
||||
return path.Join(connection.Path, EncryptedDatabaseFileName)
|
||||
}
|
||||
|
||||
return path.Join(connection.Path, DatabaseFileName)
|
||||
}
|
||||
|
||||
// GetStorePath get the filename and path for the database file
|
||||
func (connection *DbConnection) GetStorePath() string {
|
||||
return connection.Path
|
||||
}
|
||||
|
||||
func (connection *DbConnection) SetEncrypted(flag bool) {
|
||||
connection.isEncrypted = flag
|
||||
}
|
||||
|
||||
// Return true if the database is encrypted
|
||||
func (connection *DbConnection) IsEncryptedStore() bool {
|
||||
return connection.getEncryptionKey() != nil
|
||||
}
|
||||
|
||||
// NeedsEncryptionMigration returns true if database encryption is enabled and
|
||||
// we have an un-encrypted DB that requires migration to an encrypted DB
|
||||
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
||||
|
||||
// Cases: Note, we need to check both portainer.db and portainer.edb
|
||||
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
|
||||
|
||||
// 1) portainer.edb + key => False
|
||||
// 2) portainer.edb + no key => ERROR Fatal!
|
||||
// 3) portainer.db + key => True (needs migration)
|
||||
// 4) portainer.db + no key => False
|
||||
// 5) NoDB (new) + key => False
|
||||
// 6) NoDB (new) + no key => False
|
||||
// 7) portainer.db & portainer.edb => ERROR Fatal!
|
||||
|
||||
// If we have a loaded encryption key, always set encrypted
|
||||
if connection.EncryptionKey != nil {
|
||||
connection.SetEncrypted(true)
|
||||
}
|
||||
|
||||
// Check for portainer.db
|
||||
dbFile := path.Join(connection.Path, DatabaseFileName)
|
||||
_, err := os.Stat(dbFile)
|
||||
haveDbFile := err == nil
|
||||
|
||||
// Check for portainer.edb
|
||||
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
|
||||
_, err = os.Stat(edbFile)
|
||||
haveEdbFile := err == nil
|
||||
|
||||
if haveDbFile && haveEdbFile {
|
||||
// 7 - encrypted and unencrypted db?
|
||||
return false, ErrHaveEncryptedAndUnencrypted
|
||||
}
|
||||
|
||||
if haveDbFile && connection.EncryptionKey != nil {
|
||||
// 3 - needs migration
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if haveEdbFile && connection.EncryptionKey == nil {
|
||||
// 2 - encrypted db, but no key?
|
||||
return false, ErrHaveEncryptedWithNoKey
|
||||
}
|
||||
|
||||
// 1, 4, 5, 6
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (connection *DbConnection) Open() error {
|
||||
|
||||
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
|
||||
|
||||
// Now we open the db
|
||||
databasePath := connection.GetDatabaseFilePath()
|
||||
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
InitialMmapSize: connection.InitialMmapSize,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db.MaxBatchSize = connection.MaxBatchSize
|
||||
db.MaxBatchDelay = connection.MaxBatchDelay
|
||||
connection.DB = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
// Safe to being called multiple times.
|
||||
func (connection *DbConnection) Close() error {
|
||||
if connection.DB != nil {
|
||||
return connection.DB.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (connection *DbConnection) txFn(fn func(portainer.Transaction) error) func(*bolt.Tx) error {
|
||||
return func(tx *bolt.Tx) error {
|
||||
return fn(&DbTransaction{conn: connection, tx: tx})
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTx executes the given function inside a read-write transaction
|
||||
func (connection *DbConnection) UpdateTx(fn func(portainer.Transaction) error) error {
|
||||
if connection.MaxBatchDelay > 0 && connection.MaxBatchSize > 1 {
|
||||
return connection.Batch(connection.txFn(fn))
|
||||
}
|
||||
|
||||
return connection.Update(connection.txFn(fn))
|
||||
}
|
||||
|
||||
// ViewTx executes the given function inside a read-only transaction
|
||||
func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) error {
|
||||
return connection.View(connection.txFn(fn))
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
||||
return connection.View(func(tx *bolt.Tx) error {
|
||||
_, err := tx.WriteTo(w)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) ExportRaw(filename string) error {
|
||||
databasePath := connection.GetDatabaseFilePath()
|
||||
if _, err := os.Stat(databasePath); err != nil {
|
||||
return fmt.Errorf("stat on %s failed, error: %w", databasePath, err)
|
||||
}
|
||||
|
||||
b, err := connection.ExportJSON(databasePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filename, b, 0600)
|
||||
}
|
||||
|
||||
// ConvertToKey returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// so that they can be used as BoltDB keys.
|
||||
func (connection *DbConnection) ConvertToKey(v int) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(v))
|
||||
return b
|
||||
}
|
||||
|
||||
// keyToString Converts a key to a string value suitable for logging
|
||||
func keyToString(b []byte) string {
|
||||
if len(b) != 8 {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
v := binary.BigEndian.Uint64(b)
|
||||
if v <= math.MaxInt32 {
|
||||
return fmt.Sprintf("%d", v)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// CreateBucket is a generic function used to create a bucket inside a database.
|
||||
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.SetServiceName(bucketName)
|
||||
})
|
||||
}
|
||||
|
||||
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
|
||||
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(bucketName, key, object)
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) getEncryptionKey() []byte {
|
||||
if !connection.isEncrypted {
|
||||
return nil
|
||||
}
|
||||
|
||||
return connection.EncryptionKey
|
||||
}
|
||||
|
||||
// UpdateObject is a generic function used to update an object inside a database.
|
||||
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.UpdateObject(bucketName, key, object)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateObjectFunc is a generic function used to update an object safely without race conditions.
|
||||
func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
data := bucket.Get(key)
|
||||
if data == nil {
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
err := connection.UnmarshalObjectWithJsoniter(data, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFn()
|
||||
|
||||
data, err = connection.MarshalObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteObject is a generic function used to delete an object inside a database.
|
||||
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteObject(bucketName, key)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllObjects delete all objects where matching() returns (id, ok).
|
||||
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
|
||||
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteAllObjects(bucketName, obj, matching)
|
||||
})
|
||||
}
|
||||
|
||||
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
|
||||
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
||||
var identifier int
|
||||
|
||||
_ = connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
identifier = tx.GetNextIdentifier(bucketName)
|
||||
return nil
|
||||
})
|
||||
|
||||
return identifier
|
||||
}
|
||||
|
||||
// CreateObject creates a new object in the bucket, using the next bucket sequence id
|
||||
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObject(bucketName, fn)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateObjectWithId creates a new object in the bucket, using the specified id
|
||||
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithId(bucketName, id, obj)
|
||||
})
|
||||
}
|
||||
|
||||
// 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.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithStringId(bucketName, id, obj)
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAll(bucketName, obj, append)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: decide which Unmarshal to use, and use one...
|
||||
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAllWithJsoniter(bucketName, obj, append)
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
|
||||
})
|
||||
}
|
||||
|
||||
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
|
||||
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
|
||||
buckets := map[string]interface{}{}
|
||||
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
seqId := bucket.Sequence()
|
||||
buckets[bucketName] = int(seqId)
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return buckets, err
|
||||
}
|
||||
|
||||
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
|
||||
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
|
||||
var err error
|
||||
|
||||
for bucketName, v := range s {
|
||||
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
|
||||
if !ok {
|
||||
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
|
||||
continue
|
||||
}
|
||||
|
||||
err = connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.SetSequence(uint64(id))
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NeedsEncryptionMigration(t *testing.T) {
|
||||
// Test the specific scenarios mentioned in NeedsEncryptionMigration
|
||||
|
||||
// i.e.
|
||||
// Cases: Note, we need to check both portainer.db and portainer.edb
|
||||
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
|
||||
|
||||
// 1) portainer.edb + key => False
|
||||
// 2) portainer.edb + no key => ERROR Fatal!
|
||||
// 3) portainer.db + key => True (needs migration)
|
||||
// 4) portainer.db + no key => False
|
||||
// 5) NoDB (new) + key => False
|
||||
// 6) NoDB (new) + no key => False
|
||||
// 7) portainer.db & portainer.edb (key not important) => ERROR Fatal!
|
||||
|
||||
is := assert.New(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
dbname string
|
||||
key bool
|
||||
expectError error
|
||||
expectResult bool
|
||||
}{
|
||||
{
|
||||
name: "portainer.edb + key",
|
||||
dbname: EncryptedDatabaseFileName,
|
||||
key: true,
|
||||
expectError: nil,
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "portainer.db + key (migration needed)",
|
||||
dbname: DatabaseFileName,
|
||||
key: true,
|
||||
expectError: nil,
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "portainer.db + no key",
|
||||
dbname: DatabaseFileName,
|
||||
key: false,
|
||||
expectError: nil,
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "NoDB (new) + key",
|
||||
dbname: "",
|
||||
key: false,
|
||||
expectError: nil,
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "NoDB (new) + no key",
|
||||
dbname: "",
|
||||
key: false,
|
||||
expectError: nil,
|
||||
expectResult: false,
|
||||
},
|
||||
|
||||
// error tests
|
||||
{
|
||||
name: "portainer.edb + no key",
|
||||
dbname: EncryptedDatabaseFileName,
|
||||
key: false,
|
||||
expectError: ErrHaveEncryptedWithNoKey,
|
||||
expectResult: false,
|
||||
},
|
||||
{
|
||||
name: "portainer.db & portainer.edb",
|
||||
dbname: "both",
|
||||
key: true,
|
||||
expectError: ErrHaveEncryptedAndUnencrypted,
|
||||
expectResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
connection := DbConnection{Path: dir}
|
||||
|
||||
if tc.dbname == "both" {
|
||||
// Special case. If portainer.db and portainer.edb exist.
|
||||
dbFile1 := path.Join(connection.Path, DatabaseFileName)
|
||||
f, _ := os.Create(dbFile1)
|
||||
f.Close()
|
||||
defer os.Remove(dbFile1)
|
||||
|
||||
dbFile2 := path.Join(connection.Path, EncryptedDatabaseFileName)
|
||||
f, _ = os.Create(dbFile2)
|
||||
f.Close()
|
||||
defer os.Remove(dbFile2)
|
||||
} else if tc.dbname != "" {
|
||||
dbFile := path.Join(connection.Path, tc.dbname)
|
||||
f, _ := os.Create(dbFile)
|
||||
f.Close()
|
||||
defer os.Remove(dbFile)
|
||||
}
|
||||
|
||||
if tc.key {
|
||||
connection.EncryptionKey = []byte("secret")
|
||||
}
|
||||
|
||||
result, err := connection.NeedsEncryptionMigration()
|
||||
|
||||
is.Equal(tc.expectError, err, "Fatal Error failure. Test: %s", tc.name)
|
||||
is.Equal(result, tc.expectResult, "Failed test: %s", tc.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
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)
|
||||
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) {
|
||||
log.Debug().Str("databasePath", databasePath).Msg("exportJson")
|
||||
|
||||
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
|
||||
if err != nil {
|
||||
return []byte("{}"), err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
backup := make(map[string]interface{})
|
||||
if metadata {
|
||||
meta, err := backupMetadata(connection)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed exporting metadata")
|
||||
}
|
||||
|
||||
backup["__metadata"] = meta
|
||||
}
|
||||
|
||||
err = connection.View(func(tx *bolt.Tx) error {
|
||||
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
var list []interface{}
|
||||
version := make(map[string]string)
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var obj interface{}
|
||||
err := c.UnmarshalObject(v, &obj)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("bucket", bucketName).
|
||||
Str("object", string(v)).
|
||||
Err(err).
|
||||
Msg("failed to unmarshal")
|
||||
|
||||
obj = v
|
||||
}
|
||||
|
||||
if bucketName == "version" {
|
||||
version[string(k)] = string(v)
|
||||
} else {
|
||||
list = append(list, obj)
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
backup[bucketName] = list
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return []byte("{}"), err
|
||||
}
|
||||
|
||||
return json.MarshalIndent(backup, "", " ")
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
|
||||
|
||||
// MarshalObject encodes an object to binary format
|
||||
func (connection *DbConnection) MarshalObject(object interface{}) (data []byte, err error) {
|
||||
// Special case for the VERSION bucket. Here we're not using json
|
||||
if v, ok := object.(string); ok {
|
||||
data = []byte(v)
|
||||
} else {
|
||||
data, err = json.Marshal(object)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
if connection.getEncryptionKey() == nil {
|
||||
return data, nil
|
||||
}
|
||||
return encrypt(data, connection.getEncryptionKey())
|
||||
}
|
||||
|
||||
// UnmarshalObject decodes an object from binary data
|
||||
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
|
||||
var err error
|
||||
if connection.getEncryptionKey() != nil {
|
||||
data, err = decrypt(data, connection.getEncryptionKey())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed decrypting object")
|
||||
}
|
||||
}
|
||||
e := json.Unmarshal(data, object)
|
||||
if e != nil {
|
||||
// Special case for the VERSION bucket. Here we're not using json
|
||||
// So we need to return it as a string
|
||||
s, ok := object.(*string)
|
||||
if !ok {
|
||||
return errors.Wrap(err, e.Error())
|
||||
}
|
||||
|
||||
*s = string(data)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
|
||||
// decoding at the moment.
|
||||
func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||
if connection.getEncryptionKey() != nil {
|
||||
var err error
|
||||
data, err = decrypt(data, connection.getEncryptionKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err := jsoni.Unmarshal(data, &object)
|
||||
if err != nil {
|
||||
if s, ok := object.(*string); ok {
|
||||
*s = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mmm, don't have a KMS .... aes GCM seems the most likely from
|
||||
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
|
||||
|
||||
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
|
||||
block, _ := aes.NewCipher(passphrase)
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return encrypted, err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return encrypted, err
|
||||
}
|
||||
ciphertextByte := gcm.Seal(
|
||||
nonce,
|
||||
nonce,
|
||||
plaintext,
|
||||
nil)
|
||||
return ciphertextByte, nil
|
||||
}
|
||||
|
||||
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
|
||||
if string(encrypted) == "false" {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
block, err := aes.NewCipher(passphrase)
|
||||
if err != nil {
|
||||
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return encrypted, errors.Wrap(err, "Error creating GCM")
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return encrypted, errEncryptedStringTooShort
|
||||
}
|
||||
|
||||
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||
plaintextByte, err = gcm.Open(
|
||||
nil,
|
||||
nonce,
|
||||
ciphertextByteClean,
|
||||
nil)
|
||||
if err != nil {
|
||||
return encrypted, errors.Wrap(err, "Error decrypting text")
|
||||
}
|
||||
|
||||
return plaintextByte, err
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
|
||||
passphrase = "my secret key"
|
||||
)
|
||||
|
||||
func secretToEncryptionKey(passphrase string) []byte {
|
||||
hash := sha256.Sum256([]byte(passphrase))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func Test_MarshalObjectUnencrypted(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
uuid := uuid.Must(uuid.NewV4())
|
||||
|
||||
tests := []struct {
|
||||
object interface{}
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
object: nil,
|
||||
expected: `null`,
|
||||
},
|
||||
{
|
||||
object: true,
|
||||
expected: `true`,
|
||||
},
|
||||
{
|
||||
object: false,
|
||||
expected: `false`,
|
||||
},
|
||||
{
|
||||
object: 123,
|
||||
expected: `123`,
|
||||
},
|
||||
{
|
||||
object: "456",
|
||||
expected: "456",
|
||||
},
|
||||
{
|
||||
object: uuid,
|
||||
expected: "\"" + uuid.String() + "\"",
|
||||
},
|
||||
{
|
||||
object: uuid.String(),
|
||||
expected: uuid.String(),
|
||||
},
|
||||
{
|
||||
object: map[string]interface{}{"key": "value"},
|
||||
expected: `{"key":"value"}`,
|
||||
},
|
||||
{
|
||||
object: []bool{true, false},
|
||||
expected: `[true,false]`,
|
||||
},
|
||||
{
|
||||
object: []int{1, 2, 3},
|
||||
expected: `[1,2,3]`,
|
||||
},
|
||||
{
|
||||
object: []string{"1", "2", "3"},
|
||||
expected: `["1","2","3"]`,
|
||||
},
|
||||
{
|
||||
object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}},
|
||||
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
|
||||
},
|
||||
{
|
||||
object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}},
|
||||
expected: `[1,"2",false,{"key1":"value1"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
conn := DbConnection{}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
|
||||
data, err := conn.MarshalObject(test.object)
|
||||
is.NoError(err)
|
||||
is.Equal(test.expected, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
// Based on actual data entering and what we expect out of the function
|
||||
|
||||
tests := []struct {
|
||||
object []byte
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
object: []byte(""),
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
object: []byte("35"),
|
||||
expected: "35",
|
||||
},
|
||||
{
|
||||
// An unmarshalled byte string should return the same without error
|
||||
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
|
||||
expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6",
|
||||
},
|
||||
{
|
||||
// An un-marshalled json object string should return the same as a string without error also
|
||||
object: []byte(jsonobject),
|
||||
expected: jsonobject,
|
||||
},
|
||||
}
|
||||
|
||||
conn := DbConnection{}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
|
||||
var object string
|
||||
err := conn.UnmarshalObject(test.object, &object)
|
||||
is.NoError(err)
|
||||
is.Equal(test.expected, object)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ObjectMarshallingEncrypted(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
// Based on actual data entering and what we expect out of the function
|
||||
|
||||
tests := []struct {
|
||||
object []byte
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
object: []byte(""),
|
||||
},
|
||||
{
|
||||
object: []byte("35"),
|
||||
},
|
||||
{
|
||||
// An unmarshalled byte string should return the same without error
|
||||
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
|
||||
},
|
||||
{
|
||||
// An un-marshalled json object string should return the same as a string without error also
|
||||
object: []byte(jsonobject),
|
||||
},
|
||||
}
|
||||
|
||||
key := secretToEncryptionKey(passphrase)
|
||||
conn := DbConnection{EncryptionKey: key}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
|
||||
|
||||
data, err := conn.MarshalObject(test.object)
|
||||
is.NoError(err)
|
||||
|
||||
var object []byte
|
||||
err = conn.UnmarshalObject(data, &object)
|
||||
|
||||
is.NoError(err)
|
||||
is.Equal(test.object, object)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type DbTransaction struct {
|
||||
conn *DbConnection
|
||||
tx *bolt.Tx
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) SetServiceName(bucketName string) error {
|
||||
_, err := tx.tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
return err
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
value := bucket.Get(key)
|
||||
if value == nil {
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
return tx.conn.UnmarshalObjectWithJsoniter(value, object)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||
data, err := tx.conn.MarshalObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
return bucket.Put(key, data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
return bucket.Delete(key)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matchingFn func(o interface{}) (id int, ok bool)) error {
|
||||
var ids []int
|
||||
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObject(v, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id, ok := matchingFn(obj); ok {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err := bucket.Delete(tx.conn.ConvertToKey(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(id)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
seqId, _ := bucket.NextSequence()
|
||||
id, obj := fn(seqId)
|
||||
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||
err := tx.conn.UnmarshalObject(v, obj)
|
||||
if err == nil {
|
||||
obj, err = appendFn(obj)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err == nil {
|
||||
obj, err = appendFn(obj)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
||||
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
|
||||
|
||||
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = appendFn(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const testBucketName = "test-bucket"
|
||||
const testId = 1234
|
||||
|
||||
type testStruct struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func TestTxs(t *testing.T) {
|
||||
conn := DbConnection{
|
||||
Path: t.TempDir(),
|
||||
}
|
||||
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Error propagation
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return errors.New("this is an error")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected, got nil instead")
|
||||
}
|
||||
|
||||
// Create an object
|
||||
newObj := testStruct{
|
||||
Key: "key",
|
||||
Value: "value",
|
||||
}
|
||||
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
err = tx.SetServiceName(testBucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
obj := testStruct{}
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if obj.Key != newObj.Key || obj.Value != newObj.Value {
|
||||
t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
|
||||
}
|
||||
|
||||
// Update an object
|
||||
updatedObj := testStruct{
|
||||
Key: "updated-key",
|
||||
Value: "updated-value",
|
||||
}
|
||||
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
|
||||
})
|
||||
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
|
||||
t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
|
||||
}
|
||||
|
||||
// Delete an object
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if !dataservices.IsErrObjectNotFound(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get next identifier
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
id1 := tx.GetNextIdentifier(testBucketName)
|
||||
id2 := tx.GetNextIdentifier(testBucketName)
|
||||
|
||||
if id1+1 != id2 {
|
||||
return errors.New("unexpected identifier sequence")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to write in a read transaction
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected, got nil instead")
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/sqlite"
|
||||
)
|
||||
|
||||
// NewDatabase should use config options to return a connection to the requested database
|
||||
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
||||
if storeType == "boltdb" {
|
||||
return &boltdb.DbConnection{
|
||||
switch storeType {
|
||||
case "sqlite":
|
||||
return &sqlite.DbConnection{
|
||||
Path: storePath,
|
||||
EncryptionKey: encryptionKey,
|
||||
}, nil
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
VersionKey string = "DB_VERSION"
|
||||
InstanceKey string = "INSTANCE_ID"
|
||||
EditionKey string = "EDITION"
|
||||
UpdatingKey string = "DB_UPDATING"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
SchemaVersion string
|
||||
MigratorCount int
|
||||
Edition int
|
||||
InstanceID string
|
||||
Key string `json:"Key" gorm:"unique,primaryKey"`
|
||||
Value string `json:"Value"`
|
||||
}
|
||||
|
||||
func (r *Version) Validate(request *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
169
api/database/sqlite/db.go
Normal file
169
api/database/sqlite/db.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
DatabaseFileName = "portainer.db"
|
||||
EncryptedDatabaseFileName = "portainer.edb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
|
||||
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
|
||||
)
|
||||
|
||||
type DbConnection struct {
|
||||
Path string
|
||||
EncryptionKey []byte
|
||||
isEncrypted bool
|
||||
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetDB() *gorm.DB {
|
||||
if connection.DB == nil {
|
||||
err := connection.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return connection.DB
|
||||
}
|
||||
|
||||
// GetDatabaseFileName get the database filename
|
||||
func (connection *DbConnection) GetDatabaseFileName() string {
|
||||
if connection.IsEncryptedStore() {
|
||||
return EncryptedDatabaseFileName
|
||||
}
|
||||
|
||||
return DatabaseFileName
|
||||
}
|
||||
|
||||
// GetDataseFilePath get the path + filename for the database file
|
||||
func (connection *DbConnection) GetDatabaseFilePath() string {
|
||||
if connection.IsEncryptedStore() {
|
||||
return path.Join(connection.Path, EncryptedDatabaseFileName)
|
||||
}
|
||||
|
||||
return path.Join(connection.Path, DatabaseFileName)
|
||||
}
|
||||
|
||||
// GetStorePath get the filename and path for the database file
|
||||
func (connection *DbConnection) GetStorePath() string {
|
||||
return connection.Path
|
||||
}
|
||||
|
||||
func (connection *DbConnection) SetEncrypted(flag bool) {
|
||||
connection.isEncrypted = flag
|
||||
}
|
||||
|
||||
// Return true if the database is encrypted
|
||||
func (connection *DbConnection) IsEncryptedStore() bool {
|
||||
return connection.getEncryptionKey() != nil
|
||||
}
|
||||
|
||||
// NeedsEncryptionMigration returns true if database encryption is enabled and
|
||||
// we have an un-encrypted DB that requires migration to an encrypted DB
|
||||
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
||||
|
||||
// Cases: Note, we need to check both portainer.db and portainer.edb
|
||||
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
|
||||
|
||||
// 1) portainer.edb + key => False
|
||||
// 2) portainer.edb + no key => ERROR Fatal!
|
||||
// 3) portainer.db + key => True (needs migration)
|
||||
// 4) portainer.db + no key => False
|
||||
// 5) NoDB (new) + key => False
|
||||
// 6) NoDB (new) + no key => False
|
||||
// 7) portainer.db & portainer.edb => ERROR Fatal!
|
||||
|
||||
// If we have a loaded encryption key, always set encrypted
|
||||
if connection.EncryptionKey != nil {
|
||||
connection.SetEncrypted(true)
|
||||
}
|
||||
|
||||
// Check for portainer.db
|
||||
dbFile := path.Join(connection.Path, DatabaseFileName)
|
||||
_, err := os.Stat(dbFile)
|
||||
haveDbFile := err == nil
|
||||
|
||||
// Check for portainer.edb
|
||||
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
|
||||
_, err = os.Stat(edbFile)
|
||||
haveEdbFile := err == nil
|
||||
|
||||
if haveDbFile && haveEdbFile {
|
||||
// 7 - encrypted and unencrypted db?
|
||||
return false, ErrHaveEncryptedAndUnencrypted
|
||||
}
|
||||
|
||||
if haveDbFile && connection.EncryptionKey != nil {
|
||||
// 3 - needs migration
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if haveEdbFile && connection.EncryptionKey == nil {
|
||||
// 2 - encrypted db, but no key?
|
||||
return false, ErrHaveEncryptedWithNoKey
|
||||
}
|
||||
|
||||
// 1, 4, 5, 6
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (connection *DbConnection) Open() error {
|
||||
|
||||
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
|
||||
|
||||
// Now we open the db
|
||||
databasePath := connection.GetDatabaseFilePath()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(databasePath), &gorm.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(5)
|
||||
sqlDB.SetMaxOpenConns(10)
|
||||
connection.DB = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (connection *DbConnection) Close() error {
|
||||
sqlDB, err := connection.DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connection.DB = nil
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
func (connection *DbConnection) getEncryptionKey() []byte {
|
||||
if !connection.isEncrypted {
|
||||
return nil
|
||||
}
|
||||
|
||||
return connection.EncryptionKey
|
||||
}
|
||||
|
||||
func (connection *DbConnection) Init() error {
|
||||
connection.DB.AutoMigrate(&models.Version{})
|
||||
connection.DB.AutoMigrate(&portainer.Settings{})
|
||||
return nil
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
package apikeyrepository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "api_key"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "api_key"
|
||||
)
|
||||
|
||||
// Service represents a service for managing api-key data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.APIKey, portainer.APIKeyID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.APIKey, portainer.APIKeyID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -39,67 +30,93 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
|
||||
var result = make([]portainer.APIKey, 0)
|
||||
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.APIKey{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
record, ok := obj.(*portainer.APIKey)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||
return nil, fmt.Errorf("failed to convert to APIKey object: %s", obj)
|
||||
}
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.APIKey{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// record, ok := obj.(*portainer.APIKey)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
|
||||
// }
|
||||
|
||||
if record.UserID == userID {
|
||||
result = append(result, *record)
|
||||
}
|
||||
// if record.UserID == userID {
|
||||
// result = append(result, *record)
|
||||
// }
|
||||
|
||||
return &portainer.APIKey{}, nil
|
||||
})
|
||||
// return &portainer.APIKey{}, nil
|
||||
// })
|
||||
|
||||
return result, err
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetAPIKeyByDigest returns the API key for the associated digest.
|
||||
// Note: there is a 1-to-1 mapping of api-key and digest
|
||||
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
|
||||
var k *portainer.APIKey
|
||||
stop := fmt.Errorf("ok")
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.APIKey{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
key, ok := obj.(*portainer.APIKey)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||
return nil, fmt.Errorf("failed to convert to APIKey object: %s", obj)
|
||||
}
|
||||
if bytes.Equal(key.Digest, digest) {
|
||||
k = key
|
||||
return nil, stop
|
||||
}
|
||||
// var k *portainer.APIKey
|
||||
// stop := fmt.Errorf("ok")
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.APIKey{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// key, ok := obj.(*portainer.APIKey)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
|
||||
// }
|
||||
// if bytes.Equal(key.Digest, digest) {
|
||||
// k = key
|
||||
// return nil, stop
|
||||
// }
|
||||
|
||||
return &portainer.APIKey{}, nil
|
||||
})
|
||||
// return &portainer.APIKey{}, nil
|
||||
// })
|
||||
|
||||
if errors.Is(err, stop) {
|
||||
return k, nil
|
||||
}
|
||||
// if err == stop {
|
||||
// return k, nil
|
||||
// }
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
// if err == nil {
|
||||
// return nil, errors.ErrObjectNotFound
|
||||
// }
|
||||
|
||||
return nil, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create creates a new APIKey object.
|
||||
func (service *Service) Create(record *portainer.APIKey) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
record.ID = portainer.APIKeyID(id)
|
||||
// CreateAPIKey creates a new APIKey object.
|
||||
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// record.ID = portainer.APIKeyID(id)
|
||||
|
||||
return int(record.ID), record
|
||||
},
|
||||
)
|
||||
// return int(record.ID), record
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAPIKey retrieves an existing APIKey object by api key ID.
|
||||
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
||||
var key portainer.APIKey
|
||||
// identifier := service.connection.ConvertToKey(int(keyID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &key)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
|
||||
// identifier := service.connection.ConvertToKey(int(key.ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package dataservices
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type BaseCRUD[T any, I constraints.Integer] interface {
|
||||
Create(element *T) error
|
||||
Read(ID I) (*T, error)
|
||||
ReadAll() ([]T, error)
|
||||
Update(ID I, element *T) error
|
||||
Delete(ID I) error
|
||||
}
|
||||
|
||||
type BaseDataService[T any, I constraints.Integer] struct {
|
||||
Bucket string
|
||||
Connection portainer.Connection
|
||||
}
|
||||
|
||||
func (s *BaseDataService[T, I]) BucketName() string {
|
||||
return s.Bucket
|
||||
}
|
||||
|
||||
func (service *BaseDataService[T, I]) Tx(tx portainer.Transaction) BaseDataServiceTx[T, I] {
|
||||
return BaseDataServiceTx[T, I]{
|
||||
Bucket: service.Bucket,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
func (service BaseDataService[T, I]) Read(ID I) (*T, error) {
|
||||
var element *T
|
||||
|
||||
return element, service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
element, err = service.Tx(tx).Read(ID)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
|
||||
var collection = make([]T, 0)
|
||||
|
||||
return collection, service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
collection, err = service.Tx(tx).ReadAll()
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (service BaseDataService[T, I]) Update(ID I, element *T) error {
|
||||
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Update(ID, element)
|
||||
})
|
||||
}
|
||||
|
||||
func (service BaseDataService[T, I]) Delete(ID I) error {
|
||||
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Delete(ID)
|
||||
})
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package dataservices
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type BaseDataServiceTx[T any, I constraints.Integer] struct {
|
||||
Bucket string
|
||||
Connection portainer.Connection
|
||||
Tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) BucketName() string {
|
||||
return service.Bucket
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
|
||||
var element T
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.Tx.GetObject(service.Bucket, identifier, &element)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &element, nil
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
|
||||
var collection = make([]T, 0)
|
||||
|
||||
return collection, service.Tx.GetAllWithJsoniter(
|
||||
service.Bucket,
|
||||
new(T),
|
||||
AppendFn(&collection),
|
||||
)
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) Update(ID I, element *T) error {
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
return service.Tx.UpdateObject(service.Bucket, identifier, element)
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) Delete(ID I) error {
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
return service.Tx.DeleteObject(service.Bucket, identifier)
|
||||
}
|
||||
@@ -2,39 +2,92 @@ package customtemplate
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "customtemplates"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "customtemplates"
|
||||
)
|
||||
|
||||
// Service represents a service for managing custom template data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.CustomTemplate, portainer.CustomTemplateID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.CustomTemplate, portainer.CustomTemplateID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CustomTemplates return an array containing all the custom templates.
|
||||
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
|
||||
var customTemplates = make([]portainer.CustomTemplate, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.CustomTemplate{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// //var tag portainer.Tag
|
||||
// customTemplate, ok := obj.(*portainer.CustomTemplate)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
|
||||
// return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
|
||||
// }
|
||||
// customTemplates = append(customTemplates, *customTemplate)
|
||||
|
||||
// return &portainer.CustomTemplate{}, nil
|
||||
// })
|
||||
|
||||
return customTemplates, nil
|
||||
}
|
||||
|
||||
// CustomTemplate returns an custom template by ID.
|
||||
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
|
||||
var customTemplate portainer.CustomTemplate
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &customTemplate)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &customTemplate, nil
|
||||
}
|
||||
|
||||
// UpdateCustomTemplate updates an custom template.
|
||||
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, customTemplate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCustomTemplate deletes an custom template.
|
||||
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateCustomTemplate uses the existing id and saves it.
|
||||
// TODO: where does the ID come from, and is it safe?
|
||||
func (service *Service) Create(customTemplate *portainer.CustomTemplate) error {
|
||||
return service.Connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
|
||||
// return service.connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a custom template.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
// return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
@@ -35,15 +35,16 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
|
||||
var dockerhub portainer.DockerHub
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(dockerHubKey), &dockerhub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, []byte(dockerHubKey), &dockerhub)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &dockerhub, nil
|
||||
}
|
||||
|
||||
// UpdateDockerHub updates a DockerHub object.
|
||||
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
|
||||
// return service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ package edgegroup
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edgegroups"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edgegroups"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge group data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -19,42 +20,72 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.EdgeGroup, portainer.EdgeGroupID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EdgeGroup, portainer.EdgeGroupID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// EdgeGroups return an array containing all the Edge groups.
|
||||
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
||||
var groups = make([]portainer.EdgeGroup, 0)
|
||||
|
||||
// err := service.connection.GetAllWithJsoniter(
|
||||
// BucketName,
|
||||
// &portainer.EdgeGroup{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// group, ok := obj.(*portainer.EdgeGroup)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
|
||||
// }
|
||||
// groups = append(groups, *group)
|
||||
|
||||
// return &portainer.EdgeGroup{}, nil
|
||||
// })
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||
id := service.Connection.ConvertToKey(int(ID))
|
||||
edgeGroup := &portainer.EdgeGroup{}
|
||||
// EdgeGroup returns an Edge group by ID.
|
||||
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||
var group portainer.EdgeGroup
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
return service.Connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
|
||||
updateFunc(edgeGroup)
|
||||
})
|
||||
// err := service.connection.GetObject(BucketName, identifier, &group)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// UpdateEdgeGroup updates an Edge group.
|
||||
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, group)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEdgeGroup deletes an Edge group.
|
||||
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
|
||||
func (service *Service) Create(group *portainer.EdgeGroup) error {
|
||||
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Create(group)
|
||||
})
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// group.ID = portainer.EdgeGroupID(id)
|
||||
// return int(group.ID), group
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package edgegroup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||
}
|
||||
|
||||
// UpdateEdgeGroupFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
group.ID = portainer.EdgeGroupID(id)
|
||||
return int(group.ID), group
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2,69 +2,97 @@ package edgejob
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edgejobs"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edgejobs"
|
||||
)
|
||||
|
||||
// Service represents a service for managing edge jobs data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.EdgeJob, portainer.EdgeJobID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.EdgeJob, portainer.EdgeJobID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EdgeJob, portainer.EdgeJobID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// EdgeJobs returns a list of Edge jobs
|
||||
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
|
||||
var edgeJobs = make([]portainer.EdgeJob, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.EdgeJob{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// job, ok := obj.(*portainer.EdgeJob)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
|
||||
// }
|
||||
|
||||
// edgeJobs = append(edgeJobs, *job)
|
||||
|
||||
// return &portainer.EdgeJob{}, nil
|
||||
// })
|
||||
|
||||
return edgeJobs, nil
|
||||
}
|
||||
|
||||
// EdgeJob returns an Edge job by ID
|
||||
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
|
||||
var edgeJob portainer.EdgeJob
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &edgeJob)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &edgeJob, nil
|
||||
}
|
||||
|
||||
// Create creates a new EdgeJob
|
||||
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
|
||||
return service.CreateWithID(portainer.EdgeJobID(service.GetNextIdentifier()), edgeJob)
|
||||
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
// edgeJob.ID = ID
|
||||
|
||||
// return service.connection.CreateObjectWithId(
|
||||
// BucketName,
|
||||
// int(edgeJob.ID),
|
||||
// edgeJob,
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateWithID creates a new EdgeJob
|
||||
func (service *Service) CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
edgeJob.ID = ID
|
||||
|
||||
return service.Connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeJob.ID),
|
||||
edgeJob,
|
||||
)
|
||||
// UpdateEdgeJob updates an Edge job by ID
|
||||
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, edgeJob)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||
id := service.Connection.ConvertToKey(int(ID))
|
||||
edgeJob := &portainer.EdgeJob{}
|
||||
|
||||
return service.Connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
|
||||
updateFunc(edgeJob)
|
||||
})
|
||||
// DeleteEdgeJob deletes an Edge job
|
||||
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
// return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package edgejob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.EdgeJob, portainer.EdgeJobID]
|
||||
}
|
||||
|
||||
// Create creates a new EdgeJob
|
||||
func (service ServiceTx) Create(edgeJob *portainer.EdgeJob) error {
|
||||
return service.CreateWithID(portainer.EdgeJobID(service.GetNextIdentifier()), edgeJob)
|
||||
}
|
||||
|
||||
// CreateWithID creates a new EdgeJob
|
||||
func (service ServiceTx) CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
edgeJob.ID = ID
|
||||
|
||||
return service.Tx.CreateObjectWithId(BucketName, int(edgeJob.ID), edgeJob)
|
||||
}
|
||||
|
||||
// UpdateEdgeJobFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.Tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edge_stack"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_stack"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge stack data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
idxVersion map[portainer.EdgeStackID]int
|
||||
mu sync.RWMutex
|
||||
cacheInvalidationFn func(portainer.EdgeStackID)
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -23,154 +19,82 @@ func (service *Service) BucketName() string {
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
s := &Service{
|
||||
connection: connection,
|
||||
idxVersion: make(map[portainer.EdgeStackID]int),
|
||||
cacheInvalidationFn: cacheInvalidationFn,
|
||||
}
|
||||
|
||||
if s.cacheInvalidationFn == nil {
|
||||
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
|
||||
}
|
||||
|
||||
es, err := s.EdgeStacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range es {
|
||||
s.idxVersion[e.ID] = e.Version
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EdgeStacks returns an array containing all edge stacks
|
||||
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
var stacks = make([]portainer.EdgeStack, 0)
|
||||
|
||||
return stacks, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.EdgeStack{},
|
||||
dataservices.AppendFn(&stacks),
|
||||
)
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.EdgeStack{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// //var tag portainer.Tag
|
||||
// stack, ok := obj.(*portainer.EdgeStack)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
|
||||
// }
|
||||
|
||||
// stacks = append(stacks, *stack)
|
||||
|
||||
// return &portainer.EdgeStack{}, nil
|
||||
// })
|
||||
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// EdgeStack returns an Edge stack by ID.
|
||||
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
|
||||
var stack portainer.EdgeStack
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, identifier, &stack)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||
service.mu.RLock()
|
||||
v, ok := service.idxVersion[ID]
|
||||
service.mu.RUnlock()
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// CreateEdgeStack saves an Edge stack object to db.
|
||||
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
edgeStack.ID = id
|
||||
|
||||
err := service.connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeStack.ID),
|
||||
edgeStack,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.mu.Lock()
|
||||
service.idxVersion[id] = edgeStack.Version
|
||||
service.cacheInvalidationFn(id)
|
||||
service.mu.Unlock()
|
||||
// edgeStack.ID = id
|
||||
|
||||
// return service.connection.CreateObjectWithId(
|
||||
// BucketName,
|
||||
// int(edgeStack.ID),
|
||||
// edgeStack,
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: Use UpdateEdgeStackFunc instead.
|
||||
// UpdateEdgeStack updates an Edge stack.
|
||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
id := service.connection.ConvertToKey(int(ID))
|
||||
edgeStack := &portainer.EdgeStack{}
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
|
||||
updateFunc(edgeStack)
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(ID)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEdgeStackFuncTx is a helper function used to call UpdateEdgeStackFunc inside a transaction.
|
||||
func (service *Service) UpdateEdgeStackFuncTx(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
return service.Tx(tx).UpdateEdgeStackFunc(ID, updateFunc)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(service.idxVersion, ID)
|
||||
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.connection.GetNextIdentifier(BucketName)
|
||||
// return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EdgeStacks returns an array containing all edge stacks
|
||||
func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
var stacks = make([]portainer.EdgeStack, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EdgeStack{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
stack, ok := obj.(*portainer.EdgeStack)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||
return nil, fmt.Errorf("failed to convert to EdgeStack object: %s", obj)
|
||||
}
|
||||
|
||||
stacks = append(stacks, *stack)
|
||||
|
||||
return &portainer.EdgeStack{}, nil
|
||||
})
|
||||
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
// EdgeStack returns an Edge stack by ID.
|
||||
func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
|
||||
var stack portainer.EdgeStack
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||
func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||
service.service.mu.RLock()
|
||||
v, ok := service.service.idxVersion[ID]
|
||||
service.service.mu.RUnlock()
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// CreateEdgeStack saves an Edge stack object to db.
|
||||
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
edgeStack.ID = id
|
||||
|
||||
err := service.tx.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeStack.ID),
|
||||
edgeStack,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.idxVersion[id] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(id)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStack updates an Edge stack.
|
||||
func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
service.service.mu.Lock()
|
||||
defer service.service.mu.Unlock()
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.idxVersion[ID] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: use UpdateEdgeStack inside a transaction instead.
|
||||
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
edgeStack, err := service.EdgeStack(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFunc(edgeStack)
|
||||
|
||||
return service.UpdateEdgeStack(ID, edgeStack)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
service.service.mu.Lock()
|
||||
defer service.service.mu.Unlock()
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(service.service.idxVersion, ID)
|
||||
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
185
api/dataservices/edgeupdateschedule/edgeupdateschedule.go
Normal file
185
api/dataservices/edgeupdateschedule/edgeupdateschedule.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package edgeupdateschedule
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_update_schedule"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge Update Schedule data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
|
||||
mu sync.Mutex
|
||||
idxActiveSchedules map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
service := &Service{
|
||||
connection: connection,
|
||||
}
|
||||
|
||||
service.idxActiveSchedules = map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation{}
|
||||
|
||||
schedules, err := service.List()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "Unable to list schedules")
|
||||
}
|
||||
|
||||
for _, schedule := range schedules {
|
||||
service.setRelation(&schedule)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *Service) ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
return service.idxActiveSchedules[environmentID]
|
||||
}
|
||||
|
||||
func (service *Service) ActiveSchedules(environmentsIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
schedules := []edgetypes.EndpointUpdateScheduleRelation{}
|
||||
|
||||
for _, environmentID := range environmentsIDs {
|
||||
if s, ok := service.idxActiveSchedules[environmentID]; ok {
|
||||
schedules = append(schedules, *s)
|
||||
}
|
||||
}
|
||||
|
||||
return schedules
|
||||
}
|
||||
|
||||
// List return an array containing all the items in the bucket.
|
||||
func (service *Service) List() ([]edgetypes.UpdateSchedule, error) {
|
||||
var list = make([]edgetypes.UpdateSchedule, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &edgetypes.UpdateSchedule{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// item, ok := obj.(*edgetypes.UpdateSchedule)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeUpdateSchedule object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EdgeUpdateSchedule object: %s", obj)
|
||||
// }
|
||||
// list = append(list, *item)
|
||||
// return &edgetypes.UpdateSchedule{}, nil
|
||||
// })
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Item returns a item by ID.
|
||||
func (service *Service) Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
|
||||
var item edgetypes.UpdateSchedule
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &item)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new object and saves it.
|
||||
func (service *Service) Create(item *edgetypes.UpdateSchedule) error {
|
||||
// err := service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// item.ID = edgetypes.UpdateScheduleID(id)
|
||||
// return int(item.ID), item
|
||||
// },
|
||||
// )
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return service.setRelation(item)
|
||||
}
|
||||
|
||||
// Update updates an item.
|
||||
func (service *Service) Update(id edgetypes.UpdateScheduleID, item *edgetypes.UpdateSchedule) error {
|
||||
// identifier := service.connection.ConvertToKey(int(id))
|
||||
// err := service.connection.UpdateObject(BucketName, identifier, item)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
service.cleanRelation(id)
|
||||
|
||||
return service.setRelation(item)
|
||||
}
|
||||
|
||||
// Delete deletes an item.
|
||||
func (service *Service) Delete(id edgetypes.UpdateScheduleID) error {
|
||||
|
||||
service.cleanRelation(id)
|
||||
|
||||
// identifier := service.connection.ConvertToKey(int(id))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) cleanRelation(id edgetypes.UpdateScheduleID) {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
for _, schedule := range service.idxActiveSchedules {
|
||||
if schedule != nil && schedule.ScheduleID == id {
|
||||
delete(service.idxActiveSchedules, schedule.EnvironmentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) setRelation(schedule *edgetypes.UpdateSchedule) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
for environmentID, environmentStatus := range schedule.Status {
|
||||
if environmentStatus.Status != edgetypes.UpdateScheduleStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
// this should never happen
|
||||
if service.idxActiveSchedules[environmentID] != nil && service.idxActiveSchedules[environmentID].ScheduleID != schedule.ID {
|
||||
return errors.New("Multiple schedules are pending for the same environment")
|
||||
}
|
||||
|
||||
service.idxActiveSchedules[environmentID] = &edgetypes.EndpointUpdateScheduleRelation{
|
||||
EnvironmentID: environmentID,
|
||||
ScheduleID: schedule.ID,
|
||||
TargetVersion: environmentStatus.TargetVersion,
|
||||
Status: environmentStatus.Status,
|
||||
Error: environmentStatus.Error,
|
||||
Type: schedule.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "endpoints"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoints"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
mu sync.RWMutex
|
||||
idxEdgeID map[string]portainer.EndpointID
|
||||
heartbeats sync.Map
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -25,152 +20,73 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
s := &Service{
|
||||
return &Service{
|
||||
connection: connection,
|
||||
idxEdgeID: make(map[string]portainer.EndpointID),
|
||||
}
|
||||
|
||||
es, err := s.endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range es {
|
||||
if len(e.EdgeID) > 0 {
|
||||
s.idxEdgeID[e.EdgeID] = e.ID
|
||||
}
|
||||
|
||||
s.heartbeats.Store(e.ID, e.LastCheckInDate)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint *portainer.Endpoint
|
||||
var err error
|
||||
var endpoint portainer.Endpoint
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
endpoint, err = service.Tx(tx).Endpoint(ID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, identifier, &endpoint)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
|
||||
|
||||
return endpoint, nil
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an environment(endpoint).
|
||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
|
||||
})
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an environment(endpoint).
|
||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).DeleteEndpoint(ID)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints []portainer.Endpoint
|
||||
var err error
|
||||
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
endpoints, err = service.Tx(tx).Endpoints()
|
||||
return err
|
||||
})
|
||||
|
||||
return endpoints, err
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
endpoints, err := service.endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for i, e := range endpoints {
|
||||
t, _ := service.Heartbeat(e.ID)
|
||||
endpoints[i].LastCheckInDate = t
|
||||
}
|
||||
// err := service.connection.GetAllWithJsoniter(
|
||||
// BucketName,
|
||||
// &portainer.Endpoint{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// endpoint, ok := obj.(*portainer.Endpoint)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
|
||||
// return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
|
||||
// }
|
||||
|
||||
// endpoints = append(endpoints, *endpoint)
|
||||
|
||||
// return &portainer.Endpoint{}, nil
|
||||
// })
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
|
||||
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||
service.mu.RLock()
|
||||
endpointID, ok := service.idxEdgeID[edgeID]
|
||||
service.mu.RUnlock()
|
||||
|
||||
return endpointID, ok
|
||||
}
|
||||
|
||||
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||
if t, ok := service.heartbeats.Load(endpointID); ok {
|
||||
return t.(int64), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||
service.heartbeats.Store(endpointID, time.Now().Unix())
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Create(endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
return endpoints, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
|
||||
for t := range e.TeamAccessPolicies {
|
||||
if t == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
)
|
||||
// return service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
var identifier int
|
||||
|
||||
service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
identifier = service.Tx(tx).GetNextIdentifier()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return identifier
|
||||
// return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint portainer.Endpoint
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint.LastCheckInDate, _ = service.service.Heartbeat(ID)
|
||||
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an environment(endpoint).
|
||||
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
if len(endpoint.EdgeID) > 0 {
|
||||
service.service.idxEdgeID[endpoint.EdgeID] = ID
|
||||
}
|
||||
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
cache.Del(endpoint.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an environment(endpoint).
|
||||
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
for edgeID, endpointID := range service.service.idxEdgeID {
|
||||
if endpointID == ID {
|
||||
delete(service.service.idxEdgeID, edgeID)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
service.service.heartbeats.Delete(ID)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
cache.Del(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
return endpoints, service.tx.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
dataservices.AppendFn(&endpoints),
|
||||
)
|
||||
}
|
||||
|
||||
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service ServiceTx) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||
log.Error().Str("func", "Heartbeat").Msg("cannot be called inside a transaction")
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||
log.Error().Str("func", "UpdateHeartbeat").Msg("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
if len(endpoint.EdgeID) > 0 {
|
||||
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
|
||||
}
|
||||
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
return endpoints, service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
|
||||
for t := range e.TeamAccessPolicies {
|
||||
if t == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package endpointgroup
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,41 +11,82 @@ const (
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.EndpointGroup, portainer.EndpointGroupID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EndpointGroup, portainer.EndpointGroupID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// EndpointGroup returns an environment(endpoint) group by ID.
|
||||
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &endpointGroup)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &endpointGroup, nil
|
||||
}
|
||||
|
||||
// UpdateEndpointGroup updates an environment(endpoint) group.
|
||||
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, endpointGroup)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointGroup deletes an environment(endpoint) group.
|
||||
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EndpointGroups return an array containing all the environment(endpoint) groups.
|
||||
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||
var endpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.EndpointGroup{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// endpointGroup, ok := obj.(*portainer.EndpointGroup)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EndpointGroup object: %s", obj)
|
||||
// }
|
||||
|
||||
// endpointGroups = append(endpointGroups, *endpointGroup)
|
||||
|
||||
// return &portainer.EndpointGroup{}, nil
|
||||
// })
|
||||
|
||||
return endpointGroups, nil
|
||||
}
|
||||
|
||||
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||
return int(endpointGroup.ID), endpointGroup
|
||||
},
|
||||
)
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||
// return int(endpointGroup.ID), endpointGroup
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package endpointgroup
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||
}
|
||||
|
||||
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||
return int(endpointGroup.ID), endpointGroup
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2,178 +2,85 @@ package endpointrelation
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "endpoint_relations"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoint_relations"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) relation data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func (service *Service) RegisterUpdateStackFunction(
|
||||
updateFunc func(portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
) {
|
||||
service.updateStackFn = updateFunc
|
||||
service.updateStackFnTx = updateFuncTx
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointRelations returns an array of all EndpointRelations
|
||||
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||
var all = make([]portainer.EndpointRelation, 0)
|
||||
|
||||
return all, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.EndpointRelation{},
|
||||
dataservices.AppendFn(&all),
|
||||
)
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.EndpointRelation{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// r, ok := obj.(*portainer.EndpointRelation)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
|
||||
// return nil, fmt.Errorf("Failed to convert to EndpointRelation object: %s", obj)
|
||||
// }
|
||||
|
||||
// all = append(all, *r)
|
||||
|
||||
// return &portainer.EndpointRelation{}, nil
|
||||
// })
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// EndpointRelation returns a Environment(Endpoint) relation object by EndpointID
|
||||
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
// identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &endpointRelation, nil
|
||||
}
|
||||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
return err
|
||||
// return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
// identifier := service.connection.ConvertToKey(int(EndpointID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.connection.DeleteObject(BucketName, identifier)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(EndpointID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
if previousRelationState != nil {
|
||||
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the updated relation state
|
||||
// = stack has been removed for this relation
|
||||
// or this relation has been deleted
|
||||
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updatedRelationState != nil {
|
||||
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the previous relation state
|
||||
// = stack has been added for this relation
|
||||
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for each stack referenced by the updated relation
|
||||
// list how many time this stack is referenced in all relations
|
||||
// in order to update the stack deployments count
|
||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||
if refStackEnabled {
|
||||
numDeployments := 0
|
||||
for _, r := range relations {
|
||||
for sId, enabled := range r.EdgeStacks {
|
||||
if enabled && sId == refStackId {
|
||||
numDeployments += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package endpointrelation
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EndpointRelations returns an array of all EndpointRelations
|
||||
func (service ServiceTx) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||
var all = make([]portainer.EndpointRelation, 0)
|
||||
|
||||
return all, service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EndpointRelation{},
|
||||
dataservices.AppendFn(&all),
|
||||
)
|
||||
}
|
||||
|
||||
// EndpointRelation returns an Environment(Endpoint) relation object by EndpointID
|
||||
func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpointRelation, nil
|
||||
}
|
||||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||
func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.tx.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
if previousRelationState != nil {
|
||||
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the updated relation state
|
||||
// = stack has been removed for this relation
|
||||
// or this relation has been deleted
|
||||
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updatedRelationState != nil {
|
||||
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the previous relation state
|
||||
// = stack has been added for this relation
|
||||
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for each stack referenced by the updated relation
|
||||
// list how many time this stack is referenced in all relations
|
||||
// in order to update the stack deployments count
|
||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||
if refStackEnabled {
|
||||
numDeployments := 0
|
||||
for _, r := range relations {
|
||||
for sId, enabled := range r.EdgeStacks {
|
||||
if enabled && sId == refStackId {
|
||||
numDeployments += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrObjectNotFound = errors.New("object not found inside the database")
|
||||
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
ErrDBImportFailed = errors.New("importing backup failed")
|
||||
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
|
||||
// TODO: i'm pretty sure this needs wrapping at several levels
|
||||
ErrObjectNotFound = errors.New("object not found inside the database")
|
||||
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
ErrDBImportFailed = errors.New("importing backup failed")
|
||||
)
|
||||
|
||||
@@ -2,11 +2,12 @@ package extension
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "extension"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "extension"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -19,10 +20,10 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
@@ -32,12 +33,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
// Extension returns a extension by ID
|
||||
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
|
||||
var extension portainer.Extension
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, identifier, &extension)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &extension, nil
|
||||
}
|
||||
@@ -46,21 +47,33 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
|
||||
func (service *Service) Extensions() ([]portainer.Extension, error) {
|
||||
var extensions = make([]portainer.Extension, 0)
|
||||
|
||||
return extensions, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Extension{},
|
||||
dataservices.AppendFn(&extensions),
|
||||
)
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.Extension{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// extension, ok := obj.(*portainer.Extension)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Extension object")
|
||||
// return nil, fmt.Errorf("Failed to convert to Extension object: %s", obj)
|
||||
// }
|
||||
|
||||
// extensions = append(extensions, *extension)
|
||||
|
||||
// return &portainer.Extension{}, nil
|
||||
// })
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
// Persist persists a extension inside the database.
|
||||
func (service *Service) Persist(extension *portainer.Extension) error {
|
||||
return service.connection.CreateObjectWithId(BucketName, int(extension.ID), extension)
|
||||
// return service.connection.CreateObjectWithId(BucketName, int(extension.ID), extension)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExtension deletes a Extension.
|
||||
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,42 +2,94 @@ package fdoprofile
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "fdo_profiles"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "fdo_profiles"
|
||||
)
|
||||
|
||||
// Service represents a service for managingFDO Profiles data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FDOProfiles return an array containing all the FDO Profiles.
|
||||
func (service *Service) FDOProfiles() ([]portainer.FDOProfile, error) {
|
||||
var fdoProfiles = make([]portainer.FDOProfile, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.FDOProfile{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// fdoProfile, ok := obj.(*portainer.FDOProfile)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to FDOProfile object")
|
||||
|
||||
// return nil, fmt.Errorf("Failed to convert to FDOProfile object: %s", obj)
|
||||
// }
|
||||
// fdoProfiles = append(fdoProfiles, *fdoProfile)
|
||||
// return &portainer.FDOProfile{}, nil
|
||||
// })
|
||||
|
||||
return fdoProfiles, nil
|
||||
}
|
||||
|
||||
// FDOProfile returns an FDO Profile by ID.
|
||||
func (service *Service) FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error) {
|
||||
var FDOProfile portainer.FDOProfile
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &FDOProfile, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new FDO Profile and saves it.
|
||||
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
|
||||
return service.Connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(FDOProfile.ID),
|
||||
FDOProfile,
|
||||
)
|
||||
// return service.connection.CreateObjectWithId(
|
||||
// BucketName,
|
||||
// int(FDOProfile.ID),
|
||||
// FDOProfile,
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates an FDO Profile.
|
||||
func (service *Service) Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes an FDO Profile.
|
||||
func (service *Service) Delete(ID portainer.FDOProfileID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a FDO Profile.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
// return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -2,52 +2,102 @@ package helmuserrepository
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "helm_user_repository"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "helm_user_repository"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HelmUserRepository returns an array of all HelmUserRepository
|
||||
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
|
||||
var repos = make([]portainer.HelmUserRepository, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.HelmUserRepository{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// r, ok := obj.(*portainer.HelmUserRepository)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
|
||||
// return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
|
||||
// }
|
||||
|
||||
// repos = append(repos, *r)
|
||||
|
||||
// return &portainer.HelmUserRepository{}, nil
|
||||
// })
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
|
||||
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
|
||||
var result = make([]portainer.HelmUserRepository, 0)
|
||||
|
||||
return result, service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.HelmUserRepository{},
|
||||
dataservices.FilterFn(&result, func(e portainer.HelmUserRepository) bool {
|
||||
return e.UserID == userID
|
||||
}),
|
||||
)
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.HelmUserRepository{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// record, ok := obj.(*portainer.HelmUserRepository)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
|
||||
// return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
|
||||
// }
|
||||
|
||||
// if record.UserID == userID {
|
||||
// result = append(result, *record)
|
||||
// }
|
||||
|
||||
// return &portainer.HelmUserRepository{}, nil
|
||||
// })
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CreateHelmUserRepository creates a new HelmUserRepository object.
|
||||
func (service *Service) Create(record *portainer.HelmUserRepository) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
record.ID = portainer.HelmUserRepositoryID(id)
|
||||
return int(record.ID), record
|
||||
},
|
||||
)
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// record.ID = portainer.HelmUserRepositoryID(id)
|
||||
// return int(record.ID), record
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateHelmUserRepostory updates an registry.
|
||||
func (service *Service) UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, registry *portainer.HelmUserRepository) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, registry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteHelmUserRepository deletes an registry.
|
||||
func (service *Service) DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package dataservices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
perrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ErrStop signals the stop of computation when filtering results
|
||||
var ErrStop = errors.New("stop")
|
||||
|
||||
func IsErrObjectNotFound(e error) bool {
|
||||
return errors.Is(e, perrors.ErrObjectNotFound)
|
||||
}
|
||||
|
||||
// AppendFn appends elements to the given collection slice
|
||||
func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error) {
|
||||
return func(obj interface{}) (interface{}, error) {
|
||||
element, ok := obj.(*T)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||
}
|
||||
|
||||
*collection = append(*collection, *element)
|
||||
|
||||
return new(T), nil
|
||||
}
|
||||
}
|
||||
|
||||
// FilterFn appends elements to the given collection when the predicate is true
|
||||
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
|
||||
return func(obj interface{}) (interface{}, error) {
|
||||
element, ok := obj.(*T)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||
}
|
||||
|
||||
if predicate(*element) {
|
||||
*collection = append(*collection, *element)
|
||||
}
|
||||
|
||||
return new(T), nil
|
||||
}
|
||||
}
|
||||
|
||||
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
|
||||
// success
|
||||
func FirstFn[T any](element *T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
|
||||
return func(obj interface{}) (interface{}, error) {
|
||||
e, ok := obj.(*T)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||
}
|
||||
|
||||
if predicate(*e) {
|
||||
*element = *e
|
||||
return new(T), ErrStop
|
||||
}
|
||||
|
||||
return new(T), nil
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
package dataservices
|
||||
|
||||
// "github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
)
|
||||
|
||||
type (
|
||||
DataStoreTx interface {
|
||||
// DataStore defines the interface to manage the data
|
||||
DataStore interface {
|
||||
Open() (newStore bool, err error)
|
||||
Init() error
|
||||
Close() error
|
||||
|
||||
CheckCurrentEdition() error
|
||||
IsErrObjectNotFound(err error) bool
|
||||
CustomTemplate() CustomTemplateService
|
||||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
EdgeStack() EdgeStackService
|
||||
EdgeUpdateSchedule() EdgeUpdateScheduleService
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
@@ -37,72 +47,72 @@ type (
|
||||
Webhook() WebhookService
|
||||
}
|
||||
|
||||
DataStore interface {
|
||||
Open() (newStore bool, err error)
|
||||
Init() error
|
||||
Close() error
|
||||
UpdateTx(func(DataStoreTx) error) error
|
||||
ViewTx(func(DataStoreTx) error) error
|
||||
MigrateData() error
|
||||
Rollback(force bool) error
|
||||
CheckCurrentEdition() error
|
||||
BackupTo(w io.Writer) error
|
||||
Export(filename string) (err error)
|
||||
|
||||
DataStoreTx
|
||||
}
|
||||
|
||||
// CustomTemplateService represents a service to manage custom templates
|
||||
CustomTemplateService interface {
|
||||
BaseCRUD[portainer.CustomTemplate, portainer.CustomTemplateID]
|
||||
GetNextIdentifier() int
|
||||
CustomTemplates() ([]portainer.CustomTemplate, error)
|
||||
CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error)
|
||||
Create(customTemplate *portainer.CustomTemplate) error
|
||||
UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error
|
||||
DeleteCustomTemplate(ID portainer.CustomTemplateID) error
|
||||
}
|
||||
|
||||
// EdgeGroupService represents a service to manage Edge groups
|
||||
EdgeGroupService interface {
|
||||
BaseCRUD[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||
UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(group *portainer.EdgeGroup)) error
|
||||
EdgeGroups() ([]portainer.EdgeGroup, error)
|
||||
EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error)
|
||||
Create(group *portainer.EdgeGroup) error
|
||||
UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error
|
||||
DeleteEdgeGroup(ID portainer.EdgeGroupID) error
|
||||
}
|
||||
|
||||
// EdgeJobService represents a service to manage Edge jobs
|
||||
EdgeJobService interface {
|
||||
BaseCRUD[portainer.EdgeJob, portainer.EdgeJobID]
|
||||
CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error
|
||||
EdgeJobs() ([]portainer.EdgeJob, error)
|
||||
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
|
||||
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
DeleteEdgeJob(ID portainer.EdgeJobID) error
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
EdgeUpdateScheduleService interface {
|
||||
ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation
|
||||
ActiveSchedules(environmentIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation
|
||||
List() ([]edgetypes.UpdateSchedule, error)
|
||||
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
|
||||
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Delete(ID edgetypes.UpdateScheduleID) error
|
||||
}
|
||||
|
||||
// EdgeStackService represents a service to manage Edge stacks
|
||||
EdgeStackService interface {
|
||||
EdgeStacks() ([]portainer.EdgeStack, error)
|
||||
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
|
||||
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
|
||||
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
DeleteEdgeStack(ID portainer.EdgeStackID) error
|
||||
GetNextIdentifier() int
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// EndpointService represents a service for managing environment(endpoint) data
|
||||
EndpointService interface {
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
||||
EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error)
|
||||
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
||||
UpdateHeartbeat(endpointID portainer.EndpointID)
|
||||
Endpoints() ([]portainer.Endpoint, error)
|
||||
Create(endpoint *portainer.Endpoint) error
|
||||
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
||||
DeleteEndpoint(ID portainer.EndpointID) error
|
||||
GetNextIdentifier() int
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// EndpointGroupService represents a service for managing environment(endpoint) group data
|
||||
EndpointGroupService interface {
|
||||
BaseCRUD[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||
EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error)
|
||||
EndpointGroups() ([]portainer.EndpointGroup, error)
|
||||
Create(group *portainer.EndpointGroup) error
|
||||
UpdateEndpointGroup(ID portainer.EndpointGroupID, group *portainer.EndpointGroup) error
|
||||
DeleteEndpointGroup(ID portainer.EndpointGroupID) error
|
||||
}
|
||||
|
||||
// EndpointRelationService represents a service for managing environment(endpoint) relations data
|
||||
@@ -112,19 +122,25 @@ type (
|
||||
Create(endpointRelation *portainer.EndpointRelation) error
|
||||
UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error
|
||||
DeleteEndpointRelation(EndpointID portainer.EndpointID) error
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// FDOProfileService represents a service to manage FDO Profiles
|
||||
FDOProfileService interface {
|
||||
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
|
||||
FDOProfiles() ([]portainer.FDOProfile, error)
|
||||
FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error)
|
||||
Create(FDOProfile *portainer.FDOProfile) error
|
||||
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
|
||||
Delete(ID portainer.FDOProfileID) error
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
||||
HelmUserRepositoryService interface {
|
||||
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||
HelmUserRepositories() ([]portainer.HelmUserRepository, error)
|
||||
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
|
||||
Create(record *portainer.HelmUserRepository) error
|
||||
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error
|
||||
DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens
|
||||
@@ -138,23 +154,37 @@ type (
|
||||
|
||||
// RegistryService represents a service for managing registry data
|
||||
RegistryService interface {
|
||||
BaseCRUD[portainer.Registry, portainer.RegistryID]
|
||||
Registry(ID portainer.RegistryID) (*portainer.Registry, error)
|
||||
Registries() ([]portainer.Registry, error)
|
||||
Create(registry *portainer.Registry) error
|
||||
UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error
|
||||
DeleteRegistry(ID portainer.RegistryID) error
|
||||
}
|
||||
|
||||
// ResourceControlService represents a service for managing resource control data
|
||||
ResourceControlService interface {
|
||||
BaseCRUD[portainer.ResourceControl, portainer.ResourceControlID]
|
||||
ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error)
|
||||
ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error)
|
||||
ResourceControls() ([]portainer.ResourceControl, error)
|
||||
Create(rc *portainer.ResourceControl) error
|
||||
UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error
|
||||
DeleteResourceControl(ID portainer.ResourceControlID) error
|
||||
}
|
||||
|
||||
// RoleService represents a service for managing user roles
|
||||
RoleService interface {
|
||||
BaseCRUD[portainer.Role, portainer.RoleID]
|
||||
Role(ID portainer.RoleID) (*portainer.Role, error)
|
||||
Roles() ([]portainer.Role, error)
|
||||
Create(role *portainer.Role) error
|
||||
UpdateRole(ID portainer.RoleID, role *portainer.Role) error
|
||||
}
|
||||
|
||||
// APIKeyRepositoryService
|
||||
APIKeyRepository interface {
|
||||
BaseCRUD[portainer.APIKey, portainer.APIKeyID]
|
||||
CreateAPIKey(key *portainer.APIKey) error
|
||||
GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error)
|
||||
UpdateAPIKey(key *portainer.APIKey) error
|
||||
DeleteAPIKey(ID portainer.APIKeyID) error
|
||||
GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error)
|
||||
GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error)
|
||||
}
|
||||
@@ -163,25 +193,32 @@ type (
|
||||
SettingsService interface {
|
||||
Settings() (*portainer.Settings, error)
|
||||
UpdateSettings(settings *portainer.Settings) error
|
||||
BucketName() string
|
||||
IsFeatureFlagEnabled(feature portainer.Feature) bool
|
||||
}
|
||||
|
||||
SnapshotService interface {
|
||||
BaseCRUD[portainer.Snapshot, portainer.EndpointID]
|
||||
Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error)
|
||||
Snapshots() ([]portainer.Snapshot, error)
|
||||
UpdateSnapshot(snapshot *portainer.Snapshot) error
|
||||
DeleteSnapshot(endpointID portainer.EndpointID) error
|
||||
Create(snapshot *portainer.Snapshot) error
|
||||
}
|
||||
|
||||
// SSLSettingsService represents a service for managing application settings
|
||||
SSLSettingsService interface {
|
||||
Settings() (*portainer.SSLSettings, error)
|
||||
UpdateSettings(settings *portainer.SSLSettings) error
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// StackService represents a service for managing stack data
|
||||
StackService interface {
|
||||
BaseCRUD[portainer.Stack, portainer.StackID]
|
||||
Stack(ID portainer.StackID) (*portainer.Stack, error)
|
||||
StackByName(name string) (*portainer.Stack, error)
|
||||
StacksByName(name string) ([]portainer.Stack, error)
|
||||
Stacks() ([]portainer.Stack, error)
|
||||
Create(stack *portainer.Stack) error
|
||||
UpdateStack(ID portainer.StackID, stack *portainer.Stack) error
|
||||
DeleteStack(ID portainer.StackID) error
|
||||
GetNextIdentifier() int
|
||||
StackByWebhookID(ID string) (*portainer.Stack, error)
|
||||
RefreshableStacks() ([]portainer.Stack, error)
|
||||
@@ -189,53 +226,74 @@ type (
|
||||
|
||||
// TagService represents a service for managing tag data
|
||||
TagService interface {
|
||||
BaseCRUD[portainer.Tag, portainer.TagID]
|
||||
UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error
|
||||
Tags() ([]portainer.Tag, error)
|
||||
Tag(ID portainer.TagID) (*portainer.Tag, error)
|
||||
Create(tag *portainer.Tag) error
|
||||
UpdateTag(ID portainer.TagID, tag *portainer.Tag) error
|
||||
DeleteTag(ID portainer.TagID) error
|
||||
}
|
||||
|
||||
// TeamService represents a service for managing user data
|
||||
TeamService interface {
|
||||
BaseCRUD[portainer.Team, portainer.TeamID]
|
||||
Team(ID portainer.TeamID) (*portainer.Team, error)
|
||||
TeamByName(name string) (*portainer.Team, error)
|
||||
Teams() ([]portainer.Team, error)
|
||||
Create(team *portainer.Team) error
|
||||
UpdateTeam(ID portainer.TeamID, team *portainer.Team) error
|
||||
DeleteTeam(ID portainer.TeamID) error
|
||||
}
|
||||
|
||||
// TeamMembershipService represents a service for managing team membership data
|
||||
TeamMembershipService interface {
|
||||
BaseCRUD[portainer.TeamMembership, portainer.TeamMembershipID]
|
||||
TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error)
|
||||
TeamMemberships() ([]portainer.TeamMembership, error)
|
||||
TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error)
|
||||
TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error)
|
||||
Create(membership *portainer.TeamMembership) error
|
||||
UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error
|
||||
DeleteTeamMembership(ID portainer.TeamMembershipID) error
|
||||
DeleteTeamMembershipByUserID(userID portainer.UserID) error
|
||||
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
|
||||
DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error
|
||||
}
|
||||
|
||||
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||
TunnelServerService interface {
|
||||
Info() (*portainer.TunnelServerInfo, error)
|
||||
UpdateInfo(info *portainer.TunnelServerInfo) error
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// UserService represents a service for managing user data
|
||||
UserService interface {
|
||||
BaseCRUD[portainer.User, portainer.UserID]
|
||||
User(ID portainer.UserID) (*portainer.User, error)
|
||||
UserByUsername(username string) (*portainer.User, error)
|
||||
Users() ([]portainer.User, error)
|
||||
UsersByRole(role portainer.UserRole) ([]portainer.User, error)
|
||||
Create(user *portainer.User) error
|
||||
UpdateUser(ID portainer.UserID, user *portainer.User) error
|
||||
DeleteUser(ID portainer.UserID) error
|
||||
}
|
||||
|
||||
// VersionService represents a service for managing version data
|
||||
VersionService interface {
|
||||
InstanceID() (string, error)
|
||||
UpdateInstanceID(ID string) error
|
||||
DBVersion() (int, error)
|
||||
Edition() (portainer.SoftwareEdition, error)
|
||||
Version() (*models.Version, error)
|
||||
UpdateVersion(*models.Version) error
|
||||
InstanceID() (string, error)
|
||||
StoreDBVersion(version int) error
|
||||
StoreInstanceID(ID string) error
|
||||
}
|
||||
|
||||
// WebhookService represents a service for managing webhook data.
|
||||
WebhookService interface {
|
||||
BaseCRUD[portainer.Webhook, portainer.WebhookID]
|
||||
Webhooks() ([]portainer.Webhook, error)
|
||||
Webhook(ID portainer.WebhookID) (*portainer.Webhook, error)
|
||||
Create(portainer *portainer.Webhook) error
|
||||
UpdateWebhook(ID portainer.WebhookID, webhook *portainer.Webhook) error
|
||||
WebhookByResourceID(resourceID string) (*portainer.Webhook, error)
|
||||
WebhookByToken(token string) (*portainer.Webhook, error)
|
||||
DeleteWebhook(ID portainer.WebhookID) error
|
||||
}
|
||||
)
|
||||
|
||||
func IsErrObjectNotFound(e error) bool {
|
||||
return e == errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
@@ -2,49 +2,91 @@ package registry
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "registries"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "registries"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Registry, portainer.RegistryID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Registry, portainer.RegistryID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Registry, portainer.RegistryID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// Registry returns an registry by ID.
|
||||
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, ®istry)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// Create creates a new registry.
|
||||
func (service *Service) Create(registry *portainer.Registry) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
registry.ID = portainer.RegistryID(id)
|
||||
return int(registry.ID), registry
|
||||
},
|
||||
)
|
||||
// Registries returns an array containing all the registries.
|
||||
func (service *Service) Registries() ([]portainer.Registry, error) {
|
||||
var registries = make([]portainer.Registry, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.Registry{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// registry, ok := obj.(*portainer.Registry)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
|
||||
// return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
|
||||
// }
|
||||
|
||||
// registries = append(registries, *registry)
|
||||
|
||||
// return &portainer.Registry{}, nil
|
||||
// })
|
||||
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// CreateRegistry creates a new registry.
|
||||
func (service *Service) Create(registry *portainer.Registry) error {
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// registry.ID = portainer.RegistryID(id)
|
||||
// return int(registry.ID), registry
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRegistry updates an registry.
|
||||
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, registry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes an registry.
|
||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.Registry, portainer.RegistryID]
|
||||
}
|
||||
|
||||
// Create creates a new registry.
|
||||
func (service ServiceTx) Create(registry *portainer.Registry) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
registry.ID = portainer.RegistryID(id)
|
||||
return int(registry.ID), registry
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,92 +1,129 @@
|
||||
package resourcecontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "resource_control"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "resource_control"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.ResourceControl, portainer.ResourceControlID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.ResourceControl, portainer.ResourceControlID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.ResourceControl, portainer.ResourceControlID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// ResourceControl returns a ResourceControl object by ID
|
||||
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
|
||||
var resourceControl portainer.ResourceControl
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &resourceControl)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||
// if no ResourceControl was found.
|
||||
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
stop := fmt.Errorf("ok")
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.ResourceControl{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
rc, ok := obj.(*portainer.ResourceControl)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
return nil, fmt.Errorf("failed to convert to ResourceControl object: %s", obj)
|
||||
}
|
||||
// var resourceControl *portainer.ResourceControl
|
||||
// stop := fmt.Errorf("ok")
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.ResourceControl{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// rc, ok := obj.(*portainer.ResourceControl)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
// return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
||||
// }
|
||||
|
||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
// if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
// resourceControl = rc
|
||||
// return nil, stop
|
||||
// }
|
||||
|
||||
for _, subResourceID := range rc.SubResourceIDs {
|
||||
if subResourceID == resourceID {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
}
|
||||
// for _, subResourceID := range rc.SubResourceIDs {
|
||||
// if subResourceID == resourceID {
|
||||
// resourceControl = rc
|
||||
// return nil, stop
|
||||
// }
|
||||
// }
|
||||
|
||||
return &portainer.ResourceControl{}, nil
|
||||
})
|
||||
if errors.Is(err, stop) {
|
||||
return resourceControl, nil
|
||||
}
|
||||
// return &portainer.ResourceControl{}, nil
|
||||
// })
|
||||
// if err == stop {
|
||||
// return resourceControl, nil
|
||||
// }
|
||||
|
||||
return nil, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ResourceControls returns all the ResourceControl objects
|
||||
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
|
||||
var rcs = make([]portainer.ResourceControl, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.ResourceControl{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// rc, ok := obj.(*portainer.ResourceControl)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
// return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
||||
// }
|
||||
|
||||
// rcs = append(rcs, *rc)
|
||||
|
||||
// return &portainer.ResourceControl{}, nil
|
||||
// })
|
||||
|
||||
return rcs, nil
|
||||
}
|
||||
|
||||
// CreateResourceControl creates a new ResourceControl object
|
||||
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
resourceControl.ID = portainer.ResourceControlID(id)
|
||||
return int(resourceControl.ID), resourceControl
|
||||
},
|
||||
)
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// resourceControl.ID = portainer.ResourceControlID(id)
|
||||
// return int(resourceControl.ID), resourceControl
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateResourceControl saves a ResourceControl object.
|
||||
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, resourceControl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteResourceControl deletes a ResourceControl object by ID
|
||||
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package resourcecontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.ResourceControl, portainer.ResourceControlID]
|
||||
}
|
||||
|
||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||
// if no ResourceControl was found.
|
||||
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
stop := fmt.Errorf("ok")
|
||||
err := service.Tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.ResourceControl{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
rc, ok := obj.(*portainer.ResourceControl)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
return nil, fmt.Errorf("failed to convert to ResourceControl object: %s", obj)
|
||||
}
|
||||
|
||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
|
||||
for _, subResourceID := range rc.SubResourceIDs {
|
||||
if subResourceID == resourceID {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
}
|
||||
|
||||
return &portainer.ResourceControl{}, nil
|
||||
})
|
||||
if errors.Is(err, stop) {
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// CreateResourceControl creates a new ResourceControl object
|
||||
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
resourceControl.ID = portainer.ResourceControlID(id)
|
||||
return int(resourceControl.ID), resourceControl
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2,49 +2,84 @@ package role
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "roles"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "roles"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Role, portainer.RoleID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Role, portainer.RoleID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Role, portainer.RoleID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// Role returns a Role by ID
|
||||
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
||||
var set portainer.Role
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
// err := service.connection.GetObject(BucketName, identifier, &set)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &set, nil
|
||||
}
|
||||
|
||||
// Roles return an array containing all the sets.
|
||||
func (service *Service) Roles() ([]portainer.Role, error) {
|
||||
var sets = make([]portainer.Role, 0)
|
||||
|
||||
// err := service.connection.GetAll(
|
||||
// BucketName,
|
||||
// &portainer.Role{},
|
||||
// func(obj interface{}) (interface{}, error) {
|
||||
// set, ok := obj.(*portainer.Role)
|
||||
// if !ok {
|
||||
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
|
||||
// return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
|
||||
// }
|
||||
|
||||
// sets = append(sets, *set)
|
||||
|
||||
// return &portainer.Role{}, nil
|
||||
// })
|
||||
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
// CreateRole creates a new Role.
|
||||
func (service *Service) Create(role *portainer.Role) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
role.ID = portainer.RoleID(id)
|
||||
return int(role.ID), role
|
||||
},
|
||||
)
|
||||
// return service.connection.CreateObject(
|
||||
// BucketName,
|
||||
// func(id uint64) (int, interface{}) {
|
||||
// role.ID = portainer.RoleID(id)
|
||||
// return int(role.ID), role
|
||||
// },
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRole updates a role.
|
||||
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.Role, portainer.RoleID]
|
||||
}
|
||||
|
||||
// CreateRole creates a new Role.
|
||||
func (service ServiceTx) Create(role *portainer.Role) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
role.ID = portainer.RoleID(id)
|
||||
return int(role.ID), role
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package schedule
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "schedules"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "schedules"
|
||||
)
|
||||
|
||||
// Service represents a service for managing schedule data.
|
||||
type Service struct {
|
||||
@@ -19,10 +20,10 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := connection.SetServiceName(BucketName)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
@@ -32,37 +33,33 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
// Schedule returns a schedule by ID.
|
||||
func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule, error) {
|
||||
var schedule portainer.Schedule
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &schedule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err := service.connection.GetObject(BucketName, identifier, &schedule)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
return &schedule, nil
|
||||
}
|
||||
|
||||
// UpdateSchedule updates a schedule.
|
||||
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, schedule)
|
||||
// identifier := service.connection.ConvertToKey(int(ID))
|
||||
// return service.connection.UpdateObject(BucketName, identifier, schedule)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSchedule deletes a schedule.
|
||||
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Schedules return a array containing all the schedules.
|
||||
func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
||||
var schedules = make([]portainer.Schedule, 0)
|
||||
|
||||
return schedules, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Schedule{},
|
||||
dataservices.AppendFn(&schedules),
|
||||
)
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
// SchedulesByJobType return a array containing all the schedules
|
||||
@@ -70,21 +67,15 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
||||
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
|
||||
var schedules = make([]portainer.Schedule, 0)
|
||||
|
||||
return schedules, service.connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Schedule{},
|
||||
dataservices.FilterFn(&schedules, func(e portainer.Schedule) bool {
|
||||
return e.JobType == jobType
|
||||
}),
|
||||
)
|
||||
return schedules, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new schedule and saves it.
|
||||
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
|
||||
return service.connection.CreateObjectWithId(BucketName, int(schedule.ID), schedule)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a schedule.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -21,36 +21,23 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
var settings portainer.Settings
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a Settings object.
|
||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service ServiceTx) Settings() (*portainer.Settings, error) {
|
||||
var settings portainer.Settings
|
||||
|
||||
err := service.tx.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a Settings object.
|
||||
func (service ServiceTx) UpdateSettings(settings *portainer.Settings) error {
|
||||
return service.tx.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package snapshot
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -10,33 +9,40 @@ const (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Snapshot, portainer.EndpointID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Snapshot, portainer.EndpointID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Snapshot, portainer.EndpointID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
|
||||
var snapshot portainer.Snapshot
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
func (service *Service) Snapshots() ([]portainer.Snapshot, error) {
|
||||
var snapshots = make([]portainer.Snapshot, 0)
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
func (service *Service) UpdateSnapshot(snapshot *portainer.Snapshot) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) DeleteSnapshot(endpointID portainer.EndpointID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) Create(snapshot *portainer.Snapshot) error {
|
||||
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.Snapshot, portainer.EndpointID]
|
||||
}
|
||||
|
||||
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
|
||||
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
@@ -21,11 +21,6 @@ func (service *Service) BucketName() string {
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
@@ -35,15 +30,10 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
func (service *Service) Settings() (*portainer.SSLSettings, error) {
|
||||
var settings portainer.SSLSettings
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(key), &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a SSLSettings object.
|
||||
func (service *Service) UpdateSettings(settings *portainer.SSLSettings) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(key), settings)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,123 +1,84 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "stacks"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "stacks"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Stack, portainer.StackID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Stack, portainer.StackID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: service.BaseDataService.Tx(tx),
|
||||
}
|
||||
// Stack returns a stack object by ID.
|
||||
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
|
||||
var stack portainer.Stack
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// StackByName returns a stack object by name.
|
||||
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
||||
var s portainer.Stack
|
||||
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||
return e.Name == name
|
||||
}),
|
||||
)
|
||||
|
||||
if errors.Is(err, dataservices.ErrStop) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks with same name
|
||||
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
return stacks, service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||
return e.Name == name
|
||||
}),
|
||||
)
|
||||
// Stacks returns an array containing all the stacks.
|
||||
func (service *Service) Stacks() ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a stack.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
return 0
|
||||
}
|
||||
|
||||
// CreateStack creates a new stack.
|
||||
func (service *Service) Create(stack *portainer.Stack) error {
|
||||
return service.Connection.CreateObjectWithId(BucketName, int(stack.ID), stack)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStack updates a stack.
|
||||
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStack deletes a stack.
|
||||
func (service *Service) DeleteStack(ID portainer.StackID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StackByWebhookID returns a pointer to a stack object by webhook ID.
|
||||
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
|
||||
func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
||||
var s portainer.Stack
|
||||
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||
return e.AutoUpdate != nil && strings.EqualFold(e.AutoUpdate.Webhook, id)
|
||||
}),
|
||||
)
|
||||
|
||||
if errors.Is(err, dataservices.ErrStop) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
// RefreshableStacks returns stacks that are configured for a periodic update
|
||||
func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
stacks := make([]portainer.Stack, 0)
|
||||
|
||||
return stacks, service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||
return e.AutoUpdate != nil && e.AutoUpdate.Interval != ""
|
||||
}),
|
||||
)
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ func TestService_StackByWebhookID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
b := stackBuilder{t: t, store: store}
|
||||
b.createNewStack(newGuidString(t))
|
||||
@@ -58,7 +59,7 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: 2,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: []portainer.Pair{{Name: "Name1", Value: "Value1"}},
|
||||
Env: []portainer.Pair{{"Name1", "Value1"}},
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ProjectPath: "/tmp/project",
|
||||
@@ -67,13 +68,13 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
|
||||
|
||||
if webhookID == "" {
|
||||
if b.count%2 == 0 {
|
||||
stack.AutoUpdate = &portainer.AutoUpdateSettings{
|
||||
stack.AutoUpdate = &portainer.StackAutoUpdate{
|
||||
Interval: "",
|
||||
Webhook: "",
|
||||
}
|
||||
} // else keep AutoUpdate nil
|
||||
} else {
|
||||
stack.AutoUpdate = &portainer.AutoUpdateSettings{Webhook: webhookID}
|
||||
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
|
||||
}
|
||||
|
||||
err := b.store.StackService.Create(&stack)
|
||||
@@ -86,11 +87,12 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
|
||||
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.AutoUpdateSettings{Interval: "1m"}}
|
||||
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
|
||||
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
|
||||
|
||||
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
|
||||
err := store.Stack().Create(stack)
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.Stack, portainer.StackID]
|
||||
}
|
||||
|
||||
// StackByName returns a stack object by name.
|
||||
func (service ServiceTx) StackByName(name string) (*portainer.Stack, error) {
|
||||
var s portainer.Stack
|
||||
|
||||
err := service.Tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||
return e.Name == name
|
||||
}),
|
||||
)
|
||||
|
||||
if errors.Is(err, dataservices.ErrStop) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks with same name
|
||||
func (service ServiceTx) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
|
||||
return stacks, service.Tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||
return e.Name == name
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a stack.
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.Tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
|
||||
// CreateStack creates a new stack.
|
||||
func (service ServiceTx) Create(stack *portainer.Stack) error {
|
||||
return service.Tx.CreateObjectWithId(BucketName, int(stack.ID), stack)
|
||||
}
|
||||
|
||||
// StackByWebhookID returns a pointer to a stack object by webhook ID.
|
||||
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
|
||||
func (service ServiceTx) StackByWebhookID(id string) (*portainer.Stack, error) {
|
||||
var s portainer.Stack
|
||||
|
||||
err := service.Tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||
return e.AutoUpdate != nil && strings.EqualFold(e.AutoUpdate.Webhook, id)
|
||||
}),
|
||||
)
|
||||
|
||||
if errors.Is(err, dataservices.ErrStop) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
}
|
||||
|
||||
// RefreshableStacks returns stacks that are configured for a periodic update
|
||||
func (service ServiceTx) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
stacks := make([]portainer.Stack, 0)
|
||||
|
||||
return stacks, service.Tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Stack{},
|
||||
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||
return e.AutoUpdate != nil && e.AutoUpdate.Interval != ""
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package tag
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,51 +11,43 @@ const (
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Tag, portainer.TagID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Tag, portainer.TagID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Tag, portainer.TagID]{
|
||||
Bucket: BucketName,
|
||||
Connection: service.Connection,
|
||||
Tx: tx,
|
||||
},
|
||||
}
|
||||
// Tags return an array containing all the tags.
|
||||
func (service *Service) Tags() ([]portainer.Tag, error) {
|
||||
var tags = make([]portainer.Tag, 0)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Tag returns a tag by ID.
|
||||
func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
|
||||
var tag portainer.Tag
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// CreateTag creates a new tag.
|
||||
func (service *Service) Create(tag *portainer.Tag) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
tag.ID = portainer.TagID(id)
|
||||
return int(tag.ID), tag
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTagFunc updates a tag inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
|
||||
id := service.Connection.ConvertToKey(int(ID))
|
||||
tag := &portainer.Tag{}
|
||||
|
||||
return service.Connection.UpdateObjectFunc(BucketName, id, tag, func() {
|
||||
updateFunc(tag)
|
||||
})
|
||||
// UpdateTag updates a tag.
|
||||
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag.
|
||||
func (service *Service) DeleteTag(ID portainer.TagID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
dataservices.BaseDataServiceTx[portainer.Tag, portainer.TagID]
|
||||
}
|
||||
|
||||
// CreateTag creates a new tag.
|
||||
func (service ServiceTx) Create(tag *portainer.Tag) error {
|
||||
return service.Tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
tag.ID = portainer.TagID(id)
|
||||
return int(tag.ID), tag
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateTagFunc is a no-op inside a transaction
|
||||
func (service ServiceTx) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
@@ -1,67 +1,58 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "teams"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "teams"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.Team, portainer.TeamID]
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.Team, portainer.TeamID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Team returns a Team by ID
|
||||
func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
|
||||
var team portainer.Team
|
||||
return &team, nil
|
||||
}
|
||||
|
||||
// TeamByName returns a team by name.
|
||||
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
||||
var t portainer.Team
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := service.Connection.GetAll(
|
||||
BucketName,
|
||||
&portainer.Team{},
|
||||
dataservices.FirstFn(&t, func(e portainer.Team) bool {
|
||||
return strings.EqualFold(e.Name, name)
|
||||
}),
|
||||
)
|
||||
// Teams return an array containing all the teams.
|
||||
func (service *Service) Teams() ([]portainer.Team, error) {
|
||||
var teams = make([]portainer.Team, 0)
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, dataservices.ErrStop) {
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
// UpdateTeam saves a Team.
|
||||
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTeam creates a new Team.
|
||||
func (service *Service) Create(team *portainer.Team) error {
|
||||
return service.Connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
team.ID = portainer.TeamID(id)
|
||||
return int(team.ID), team
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTeam deletes a Team.
|
||||
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func Test_teamByName(t *testing.T) {
|
||||
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
_, err := store.Team().TeamByName("name")
|
||||
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
|
||||
@@ -18,7 +19,8 @@ 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 := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
t: t,
|
||||
@@ -33,7 +35,8 @@ 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 := datastore.MustNewTestStore(t, true, true)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
teamBuilder := teamBuilder{
|
||||
t: t,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user