Compare commits
70 Commits
fix/dev-bu
...
fix/EE-305
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d3ea4335f | ||
|
|
9e5c056051 | ||
|
|
c8ebd563ba | ||
|
|
62a586d35b | ||
|
|
82809e32b1 | ||
|
|
7bedb7b0d2 | ||
|
|
ee3271829f | ||
|
|
e1218283bf | ||
|
|
3654109332 | ||
|
|
bf9dc8c2d0 | ||
|
|
67f8e8f3c2 | ||
|
|
56d6dfe02e | ||
|
|
861a9a5bbb | ||
|
|
1b470845b8 | ||
|
|
3c26aa8f34 | ||
|
|
de953da5a4 | ||
|
|
5356d1feeb | ||
|
|
7a8a20e0cc | ||
|
|
a7474188b9 | ||
|
|
6fe56f89c6 | ||
|
|
a98f480974 | ||
|
|
8ccac7c98f | ||
|
|
e0ce3671e8 | ||
|
|
62128d1069 | ||
|
|
a65ffe519a | ||
|
|
5ac1ea3df8 | ||
|
|
bf56bdb8f6 | ||
|
|
b00aa68c2b | ||
|
|
8c5edd2c97 | ||
|
|
c650868fe9 | ||
|
|
30a2bb0495 | ||
|
|
1a451823d9 | ||
|
|
feab2a757e | ||
|
|
17839aa473 | ||
|
|
fc1aec3bb8 | ||
|
|
d64e7eacfc | ||
|
|
7f805ac5be | ||
|
|
308a78db21 | ||
|
|
814fc9dfc0 | ||
|
|
3635df89dc | ||
|
|
30248eabb4 | ||
|
|
3636ac5c26 | ||
|
|
f6e8b25cf3 | ||
|
|
124e0bf9b9 | ||
|
|
45def82156 | ||
|
|
76bdf6f220 | ||
|
|
e142be399d | ||
|
|
13ba72ee07 | ||
|
|
f17a608dc7 | ||
|
|
6ee5cc6a56 | ||
|
|
44582732bb | ||
|
|
ea03024fbc | ||
|
|
795e6a5b3c | ||
|
|
2b17cb9104 | ||
|
|
347f66b1f1 | ||
|
|
40c387f4f4 | ||
|
|
15cbdb8af9 | ||
|
|
621a01ba3b | ||
|
|
37f382d286 | ||
|
|
77b49ae9c5 | ||
|
|
29648f517b | ||
|
|
8f42af49e8 | ||
|
|
0ab7987684 | ||
|
|
31d956dbcb | ||
|
|
2cc80e5e5d | ||
|
|
fb6e26a302 | ||
|
|
9cca299833 | ||
|
|
4c86be725d | ||
|
|
0669ad77d3 | ||
|
|
2bfc956f58 |
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/
|
||||
148
.github/workflows/nightly-security-scan.yml
vendored
148
.github/workflows/nightly-security-scan.yml
vendored
@@ -1,22 +1,23 @@
|
||||
name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
- cron: '0 20 * * *'
|
||||
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'
|
||||
if: >-
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -24,46 +25,49 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: develop scan report export to html
|
||||
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")
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=js_result::${result}"
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "js_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${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/fix/EE-3059/security-scan-debug'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.4'
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: Download go modules
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
@@ -71,125 +75,93 @@ jobs:
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: develop scan report export to html
|
||||
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")
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=go_result::${result}"
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "go_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${result}"
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
name: Image Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.19.4
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.4'
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: build
|
||||
run: make 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
|
||||
- name: scan vulnerabilities by Trivy
|
||||
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 portainerci/portainer:develop
|
||||
|
||||
- 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-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: develop scan report export to html
|
||||
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")
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the trivy result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
||||
echo "::set-output name=image_result::${result}"
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${result}"
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result
|
||||
name: Analyse Scan Results
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
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
|
||||
- name: display the results of js, Go, and image scan
|
||||
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 Slack message
|
||||
if: >-
|
||||
- name: send message to Slack
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.18.0
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
|
||||
113
.github/workflows/pr-security.yml
vendored
113
.github/workflows/pr-security.yml
vendored
@@ -12,10 +12,11 @@ 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 &&
|
||||
@@ -23,9 +24,10 @@ jobs:
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -33,13 +35,13 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
- name: upload scan result as pull-request artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-feat-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -51,24 +53,24 @@ jobs:
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
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")
|
||||
$(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: Upload js result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-compare-to-develop-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=js_diff_result::${result}"
|
||||
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
|
||||
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
name: Server Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -76,16 +78,18 @@ jobs:
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.4'
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: Download go modules
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
@@ -93,13 +97,13 @@ jobs:
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
- name: upload scan result as pull-request artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-feature-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -111,24 +115,24 @@ jobs:
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
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")
|
||||
$(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: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-compare-to-develop-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=go_diff_result::${result}"
|
||||
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
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
name: Image Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -136,53 +140,50 @@ jobs:
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.19.4
|
||||
- name: install Go 1.19.5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.4'
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: install Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
- name: install packages and build binary
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: build
|
||||
run: make build
|
||||
- name: set up docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
- name: build and compress image
|
||||
uses: docker/build-push-action@v4
|
||||
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: Run Trivy vulnerability scanner
|
||||
- name: scan vulnerabilities by Trivy
|
||||
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 }}
|
||||
|
||||
- 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
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -194,40 +195,40 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
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")
|
||||
$(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: Upload image result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the image diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=image_diff_result::${result}"
|
||||
result=$(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
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result compared to develop
|
||||
name: Analyse Scan Result Against develop Branch
|
||||
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 }}
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -8,12 +8,12 @@ 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
|
||||
run: yarn test:client
|
||||
# test-server:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
|
||||
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: '14'
|
||||
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
|
||||
122
Makefile
122
Makefile
@@ -1,122 +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)
|
||||
|
||||
TAG=latest
|
||||
SWAG_VERSION=v1.8.11
|
||||
|
||||
# build target, can be one of "production", "testing", "development"
|
||||
ENV=development
|
||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help build-storybook build-client devops download-binaries tidy clean client-deps
|
||||
|
||||
##@ Building
|
||||
|
||||
init-dist:
|
||||
@mkdir -p dist
|
||||
|
||||
build-storybook:
|
||||
yarn storybook:build
|
||||
|
||||
build: build-server build-client ## Build the server and client
|
||||
|
||||
build-client: init-dist client-deps ## 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 ## Build the Portainer image
|
||||
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
||||
|
||||
devops: clean init-dist download-binaries build-client ## Build the server binary for CI
|
||||
echo "Building the devops binary..."
|
||||
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
##@ Dependencies
|
||||
|
||||
download-binaries: ## Download dependant binaries
|
||||
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||
|
||||
tidy: ## Tidy up the go.mod file
|
||||
cd api && go mod tidy
|
||||
|
||||
client-deps: ## Install client dependencies
|
||||
yarn
|
||||
|
||||
##@ Cleanup
|
||||
|
||||
clean: ## Remove all build and download artifacts
|
||||
@echo "Clearing the dist directory..."
|
||||
@rm -rf dist/*
|
||||
|
||||
##@ Testing
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test
|
||||
|
||||
test-server: ## Run server tests
|
||||
cd api && go test -v ./...
|
||||
|
||||
test: test-client test-server ## Run all tests
|
||||
|
||||
##@ Dev
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
|
||||
dev-server: build-image ## Run the server in development mode
|
||||
@./dev/run_container.sh
|
||||
|
||||
|
||||
##@ Format
|
||||
|
||||
format-client: ## Format client code
|
||||
yarn format
|
||||
|
||||
format-server: ## Format server code
|
||||
cd api && go fmt ./...
|
||||
|
||||
format: format-client format-server ## Format all code
|
||||
|
||||
##@ Lint
|
||||
|
||||
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
|
||||
|
||||
dev-extension: build-server build-client ## Run the extension in development mode
|
||||
make local -f build/docker-extension/Makefile
|
||||
|
||||
##@ Docs
|
||||
|
||||
docs-deps: ## Install docs dependencies
|
||||
go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
|
||||
docs-build: docs-deps ## Build docs
|
||||
cd api && swag init -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
|
||||
|
||||
docs-validate: docs-build ## Validate docs
|
||||
yarn swagger2openapi --warnOnly api/docs/swagger.yaml -o api/docs/openapi.yaml
|
||||
yarn swagger-cli validate api/docs/openapi.yaml
|
||||
|
||||
docs-clean: ## Clean docs
|
||||
rm -rf api/docs
|
||||
|
||||
docs-validate-clean: docs-validate docs-clean ## Validate and clean docs
|
||||
|
||||
##@ Helpers
|
||||
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\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)
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -42,7 +43,9 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
|
||||
@@ -72,6 +72,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||
}
|
||||
|
||||
@@ -80,7 +81,6 @@ 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,31 +111,38 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
if endpointURL != "" {
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
if endpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval != "" {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
if snapshotInterval == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@ func Confirm(message string) (bool, error) {
|
||||
fmt.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
|
||||
answer = strings.ReplaceAll(answer, "\n", "")
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||
}
|
||||
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
@@ -115,9 +114,6 @@ 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
|
||||
|
||||
@@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
||||
var object string
|
||||
err := conn.UnmarshalObject(test.object, &object)
|
||||
is.NoError(err)
|
||||
is.Equal(test.expected, string(object))
|
||||
is.Equal(test.expected, object)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, i
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(int(id)), data)
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
|
||||
// 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) {
|
||||
switch storeType {
|
||||
case "boltdb":
|
||||
if storeType == "boltdb" {
|
||||
return &boltdb.DbConnection{
|
||||
Path: storePath,
|
||||
EncryptionKey: encryptionKey,
|
||||
|
||||
@@ -159,6 +159,11 @@ func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc
|
||||
})
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -101,9 +100,16 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStackFunc is a no-op inside a transaction.
|
||||
// Deprecated: use UpdateEdgeStack inside a transaction instead.
|
||||
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
edgeStack, err := service.EdgeStack(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFunc(edgeStack)
|
||||
|
||||
return service.UpdateEdgeStack(ID, edgeStack)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
|
||||
@@ -14,16 +14,21 @@ const 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
|
||||
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
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func (service *Service) RegisterUpdateStackFunction(updateFunc func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error) {
|
||||
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.
|
||||
|
||||
@@ -151,7 +151,7 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
|
||||
}
|
||||
}
|
||||
|
||||
service.service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "registries"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "registries"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -34,7 +32,14 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Registry returns an registry by ID.
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Registry returns a registry by ID.
|
||||
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
@@ -69,7 +74,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
|
||||
return registries, err
|
||||
}
|
||||
|
||||
// CreateRegistry creates a new registry.
|
||||
// Create creates a new registry.
|
||||
func (service *Service) Create(registry *portainer.Registry) error {
|
||||
return service.connection.CreateObject(
|
||||
BucketName,
|
||||
@@ -80,13 +85,13 @@ func (service *Service) Create(registry *portainer.Registry) error {
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRegistry updates an registry.
|
||||
// UpdateRegistry updates a 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)
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes an registry.
|
||||
// DeleteRegistry deletes a registry.
|
||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
|
||||
75
api/dataservices/registry/tx.go
Normal file
75
api/dataservices/registry/tx.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package registry
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Registry returns a registry by ID.
|
||||
func (service ServiceTx) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// Registries returns an array containing all the registries.
|
||||
func (service ServiceTx) Registries() ([]portainer.Registry, error) {
|
||||
var registries = make([]portainer.Registry, 0)
|
||||
|
||||
err := service.tx.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, err
|
||||
}
|
||||
|
||||
// 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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRegistry updates a registry.
|
||||
func (service ServiceTx) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, registry)
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes a registry.
|
||||
func (service ServiceTx) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
@@ -48,13 +48,16 @@ func (store *Store) MigrateData() error {
|
||||
|
||||
err = store.FailSafeMigrate(migrator, version)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Msg("migration failed, restoring database to previous version")
|
||||
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore database")
|
||||
}
|
||||
|
||||
log.Info().Msg("database restored to previous version")
|
||||
return errors.Wrap(err, "failed to migrate database")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -17,11 +18,7 @@ type PostInitMigrator struct {
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
func NewPostInitMigrator(
|
||||
kubeFactory *cli.ClientFactory,
|
||||
dockerFactory *docker.ClientFactory,
|
||||
dataStore dataservices.DataStore,
|
||||
) *PostInitMigrator {
|
||||
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *docker.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
|
||||
return &PostInitMigrator{
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
@@ -44,9 +41,10 @@ func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
// Early exit if we do not need to migrate!
|
||||
if endpoints[i].PostInitMigrations.MigrateIngresses == false {
|
||||
if !endpoints[i].PostInitMigrations.MigrateIngresses {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,10 +65,11 @@ func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
|
||||
log.Err(err).Msg("failure getting endpoints")
|
||||
return
|
||||
}
|
||||
|
||||
for i := range environments {
|
||||
if environments[i].Type == portainer.DockerEnvironment {
|
||||
// // Early exit if we do not need to migrate!
|
||||
if environments[i].PostInitMigrations.MigrateGPUs == false {
|
||||
if !environments[i].PostInitMigrations.MigrateGPUs {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -102,11 +101,13 @@ func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
|
||||
log.Err(err).Msg("failed to inspect container")
|
||||
return
|
||||
}
|
||||
|
||||
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||
for _, deviceRequest := range deviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" {
|
||||
environments[i].EnableGPUManagement = true
|
||||
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||
|
||||
break containersLoop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (store *Store) initServices() error {
|
||||
return err
|
||||
}
|
||||
store.EdgeStackService = edgeStackService
|
||||
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc)
|
||||
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx)
|
||||
|
||||
edgeGroupService, err := edgegroup.NewService(store.connection)
|
||||
if err != nil {
|
||||
|
||||
@@ -42,11 +42,15 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
|
||||
|
||||
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
|
||||
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
||||
func (tx *StoreTx) Registry() dataservices.RegistryService { return nil }
|
||||
func (tx *StoreTx) ResourceControl() dataservices.ResourceControlService { return nil }
|
||||
func (tx *StoreTx) Role() dataservices.RoleService { return nil }
|
||||
func (tx *StoreTx) APIKeyRepository() dataservices.APIKeyRepository { return nil }
|
||||
func (tx *StoreTx) Settings() dataservices.SettingsService { return nil }
|
||||
|
||||
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *StoreTx) ResourceControl() dataservices.ResourceControlService { return nil }
|
||||
func (tx *StoreTx) Role() dataservices.RoleService { return nil }
|
||||
func (tx *StoreTx) APIKeyRepository() dataservices.APIKeyRepository { return nil }
|
||||
func (tx *StoreTx) Settings() dataservices.SettingsService { return nil }
|
||||
|
||||
func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
|
||||
return tx.store.SnapshotService.Tx(tx.tx)
|
||||
|
||||
@@ -945,6 +945,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -38,17 +38,19 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
|
||||
// The underlying http client timeout may be specified, a default value is used otherwise.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
return nil, errUnsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
case portainer.AgentOnDockerEnvironment:
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
|
||||
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
case portainer.EdgeAgentOnDockerEnvironment:
|
||||
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
return createLocalClient(endpoint)
|
||||
}
|
||||
|
||||
return createTCPClient(endpoint, timeout)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||
@@ -75,6 +76,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,7 +86,9 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "logout")
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
@@ -101,6 +105,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
||||
} else {
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth")
|
||||
}
|
||||
|
||||
if !pullImage {
|
||||
args = append(args, "--resolve-image=never")
|
||||
}
|
||||
@@ -112,6 +117,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
|
||||
for _, envvar := range stack.Env {
|
||||
env = append(env, envvar.Name+"="+envvar.Value)
|
||||
}
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
|
||||
}
|
||||
|
||||
@@ -121,7 +127,9 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
@@ -198,6 +206,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
||||
if config["HttpHeaders"] == nil {
|
||||
config["HttpHeaders"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
headersObject := config["HttpHeaders"].(map[string]interface{})
|
||||
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
|
||||
headersObject["X-PortainerAgent-Signature"] = signature
|
||||
@@ -230,5 +239,6 @@ func configureFilePaths(args []string, filePaths []string) []string {
|
||||
for _, path := range filePaths {
|
||||
args = append(args, "--compose-file", path)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -96,10 +96,12 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to parse url")
|
||||
}
|
||||
|
||||
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to build download url")
|
||||
}
|
||||
|
||||
zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip")
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to create temp file")
|
||||
@@ -136,6 +138,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
|
||||
}
|
||||
|
||||
return zipFile.Name(), nil
|
||||
}
|
||||
|
||||
@@ -144,6 +147,7 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return rootItem.CommitId, nil
|
||||
}
|
||||
|
||||
@@ -193,6 +197,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
|
||||
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
|
||||
return nil, errors.Errorf("failed to get latest commitID in the repository")
|
||||
}
|
||||
|
||||
return &items.Value[0], nil
|
||||
}
|
||||
|
||||
@@ -211,7 +216,7 @@ func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
|
||||
}
|
||||
|
||||
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
||||
const expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
||||
|
||||
func parseSshUrl(rawUrl string) (*azureOptions, error) {
|
||||
path := strings.Split(rawUrl, "/")
|
||||
@@ -349,6 +354,7 @@ func (a *azureClient) buildTreeUrl(config *azureOptions, rootObjectHash string)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
// projectId={projectId}&recursive=true&fileName={fileName}&$format={$format}&api-version=6.0
|
||||
q.Set("recursive", "true")
|
||||
@@ -367,9 +373,11 @@ func formatReferenceName(name string) string {
|
||||
if strings.HasPrefix(name, branchPrefix) {
|
||||
return strings.TrimPrefix(name, branchPrefix)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, tagPrefix) {
|
||||
return strings.TrimPrefix(name, tagPrefix)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -377,9 +385,11 @@ func getVersionType(name string) string {
|
||||
if strings.HasPrefix(name, branchPrefix) {
|
||||
return "branch"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, tagPrefix) {
|
||||
return "tag"
|
||||
}
|
||||
|
||||
return "commit"
|
||||
}
|
||||
|
||||
@@ -502,5 +512,6 @@ func checkAzureStatusCode(err error, code int) error {
|
||||
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
)
|
||||
const privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
|
||||
func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
@@ -107,7 +106,7 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
|
||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
@@ -269,7 +268,7 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
|
||||
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
@@ -224,7 +224,7 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||
repositoryUrl := privateGitRepoURL
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
|
||||
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
|
||||
@@ -95,10 +95,12 @@ func getCommitHistoryLength(t *testing.T, err error, dir string) int {
|
||||
if err != nil {
|
||||
t.Fatalf("can't open a git repo at %s with error %v", dir, err)
|
||||
}
|
||||
|
||||
iter, err := repo.Log(&git.LogOptions{All: true})
|
||||
if err != nil {
|
||||
t.Fatalf("can't get a commit history iterator with error %v", err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
err = iter.ForEach(func(_ *object.Commit) error {
|
||||
count++
|
||||
@@ -107,6 +109,7 @@ func getCommitHistoryLength(t *testing.T, err error, dir string) int {
|
||||
if err != nil {
|
||||
t.Fatalf("can't iterate over the commit history with error %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
REPOSITORY_CACHE_SIZE = 4
|
||||
REPOSITORY_CACHE_TTL = 5 * time.Minute
|
||||
const (
|
||||
repositoryCacheSize = 4
|
||||
repositoryCacheTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
// baseOption provides a minimum group of information to operate a git repository, like git-remote
|
||||
@@ -60,7 +60,7 @@ type Service struct {
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(ctx context.Context) *Service {
|
||||
return newService(ctx, REPOSITORY_CACHE_SIZE, REPOSITORY_CACHE_TTL)
|
||||
return newService(ctx, repositoryCacheSize, repositoryCacheTTL)
|
||||
}
|
||||
|
||||
func newService(ctx context.Context, cacheSize int, cacheTTL time.Duration) *Service {
|
||||
|
||||
47
api/go.mod
47
api/go.mod
@@ -4,43 +4,43 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/Microsoft/go-winio v0.5.1
|
||||
github.com/Microsoft/go-winio v0.5.2
|
||||
github.com/VictoriaMetrics/fastcache v1.12.0
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/docker/cli v20.10.12+incompatible
|
||||
github.com/docker/docker v20.10.16+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/libhttp v0.0.0-20230206214615-dabd58de9f44
|
||||
github.com/portainer/portainer/pkg/featureflags v0.0.0-20230209201943-d73622ed9cd4
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230410231754-96474a42eacb
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.29.0
|
||||
@@ -48,8 +48,8 @@ require (
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.26.1
|
||||
@@ -59,13 +59,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 // indirect
|
||||
github.com/aws/smithy-go v1.13.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
@@ -76,7 +77,7 @@ require (
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
@@ -123,10 +124,10 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
|
||||
109
api/go.sum
109
api/go.sum
@@ -33,14 +33,16 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
|
||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE=
|
||||
github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
@@ -57,24 +59,29 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.1 h1:GzvOVAdTbWxhEMRK4FfiblkGverOkAT0UodDxC1jHQM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.1/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2 h1:2faRNX8JgZVy7dDxERkaGBqb/xo5Rgmc8JMPL5j1o58=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.2/go.mod h1:8kRH9fthlxHEeNJ3g1N3NTSUMBba+KtTM8hp6SvUWn8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.1/go.mod h1:MYiG3oeEcmrdBOV7JOIWhionzyRZJWCnByS5FmvhAoU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 h1:LZwqhOyqQ2w64PZk04V0Om9AEExtW8WMkCRoE1h9/94=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1/go.mod h1:22SEiBSQm5AyKEjoPcG1hzpeTI+m9CXfE6yt1h49wBE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 h1:ObMfGNk0xjOWduPxsrRWVwZZia3e9fOcO6zlKCkt38s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1/go.mod h1:1xvCD+I5BcDuQUc+psZr7LI1a9pclAWZs3S3Gce5+lg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1 h1:onTF83DG9dsRv6UzuhYb7phiktjwQ++s/n+ZtNlTQnM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1/go.mod h1:9RH1zeu1Ls3x2EQew/eCDuq2AlC0M8RzYfYy5+5gSLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.1/go.mod h1:fEaHB2bi+wVZw4uKMHEXTL9LwtT4EL//DOhTeflqIVo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.1/go.mod h1:/73aFBwUl60wKBKhdth2pEOkut5ZNjVHGF9hjXz0bM0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.10.1/go.mod h1:+BmlPeQ1Y+PuIho93MMKDby12PoUnt1SZXQdEHCzSlw=
|
||||
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1 h1:02c72fDJr87N8RAC2s3Qu0YuvMRZKNZJ9F+lAehCazk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.2 h1:F/v1w0XcFDZjL0bCdi9XWJenoPKjGbzljBhDKcryzEQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.2/go.mod h1:eAT5aj/WJ2UDIA0IVNFc2byQLeD89SDEi4cjzH/MKoQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19 h1:oRHDrwCTVT8ZXi4sr9Ld+EXk7N/KGssOr2ygNeojEhw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0 h1:AAZJJAENsQ4yYbnfvqPZT8Nc1YlEd5CZ4usymlC2b4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.14.0/go.mod h1:a3WUi3JjM3MFtIYenSYPJ7UZPXsw7U7vzebnynxucks=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.4/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU=
|
||||
github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -93,10 +100,10 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/docker/cli v20.10.9+incompatible h1:OJ7YkwQA+k2Oi51lmCojpjiygKpi76P7bg91b2eJxYU=
|
||||
github.com/docker/cli v20.10.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/docker/cli v20.10.12+incompatible h1:lZlz0uzG+GH+c0plStMUdF/qk3ppmgnswpR5EbqzVGA=
|
||||
github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v20.10.16+incompatible h1:2Db6ZR/+FUR3hqPMwnogOPHFn405crbpxvWzKovETOQ=
|
||||
@@ -131,8 +138,8 @@ github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf70
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
@@ -145,8 +152,8 @@ github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM=
|
||||
github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -167,8 +174,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
|
||||
github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig=
|
||||
github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
|
||||
@@ -216,6 +223,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -237,8 +245,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
@@ -259,8 +267,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
|
||||
@@ -332,8 +340,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -350,8 +358,8 @@ github.com/portainer/libhttp v0.0.0-20230206214615-dabd58de9f44 h1:4LYprPd3TsYjH
|
||||
github.com/portainer/libhttp v0.0.0-20230206214615-dabd58de9f44/go.mod h1:H49JLiywwLt2rrJVroafEWy8fIs0i7mThAThK40sbb8=
|
||||
github.com/portainer/portainer/pkg/featureflags v0.0.0-20230209201943-d73622ed9cd4 h1:gnXwaF0GnFUIlynRq994WFOtqOULTKZks4aSWuonlhA=
|
||||
github.com/portainer/portainer/pkg/featureflags v0.0.0-20230209201943-d73622ed9cd4/go.mod h1:T37rFZMg+PhRhT9n/z9cLSj9khJSdwHj3/Ac5PZQgKI=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724 h1:FZrRVMpxXdUV+p5VSCAy9Uz7RzAeEJr2ytlctvMrsHY=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724/go.mod h1:WUdwNVH9GMffP4qf4U2ea2qCYfti2V7S+IhGpO8Sxv0=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230410231754-96474a42eacb h1:Mye2NvFDr5amKu/TkLpAkpAosBTSbhUOPHnNSIEzM8Q=
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20230410231754-96474a42eacb/go.mod h1:cFRD6PvOwpd2pf/O1r/IMKl+ZB12pWfo/Evleh3aCfM=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73 h1:7bPOnwucE0nor0so1BQJxQKCL5t+vCWO4nAz/S0lci0=
|
||||
github.com/portainer/portainer/third_party/digest v0.0.0-20221201002639-8fd0efa34f73/go.mod h1:E2w/A6qsKuG2VyiUubPdXpDyPykWfQqxuCs0YNS0MhM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -419,6 +427,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
@@ -487,15 +496,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -505,8 +514,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -551,13 +560,13 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -565,8 +574,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -113,7 +113,9 @@ func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -132,7 +134,9 @@ func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -151,7 +155,9 @@ func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -182,7 +188,9 @@ func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -201,7 +209,9 @@ func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -225,7 +235,9 @@ func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
||||
@@ -33,10 +33,13 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return "", errorResponse
|
||||
|
||||
@@ -128,6 +128,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("unexpected status code %s", response.Status)
|
||||
@@ -137,6 +138,8 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certificate)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
@@ -132,6 +134,8 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
@@ -141,10 +145,12 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
errorResponse := parseError(responseBody)
|
||||
if errorResponse != nil {
|
||||
return nil, errorResponse
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unexpected status code %s", response.Status)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
@@ -89,6 +90,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ func backup(t *testing.T, h *Handler, password string) []byte {
|
||||
|
||||
response := w.Result()
|
||||
archive, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
return archive
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package customtemplates
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -135,6 +135,7 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
// Platform validation is only for docker related stack (docker standalone and docker swarm)
|
||||
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
@@ -215,6 +216,8 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
// IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file
|
||||
IsComposeFormat bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -234,14 +237,11 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
if payload.Type == portainer.KubernetesStack {
|
||||
return errors.New("Creating a Kubernetes custom template from git is not supported")
|
||||
}
|
||||
|
||||
if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
// Platform validation is only for docker related stack (docker standalone and docker swarm)
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack && payload.Type != portainer.KubernetesStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
@@ -260,35 +260,44 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
|
||||
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
|
||||
customTemplate := &portainer.CustomTemplate{
|
||||
ID: portainer.CustomTemplateID(customTemplateID),
|
||||
Title: payload.Title,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Description: payload.Description,
|
||||
Note: payload.Note,
|
||||
Platform: payload.Platform,
|
||||
Type: payload.Type,
|
||||
Logo: payload.Logo,
|
||||
Variables: payload.Variables,
|
||||
ID: portainer.CustomTemplateID(customTemplateID),
|
||||
Title: payload.Title,
|
||||
Description: payload.Description,
|
||||
Note: payload.Note,
|
||||
Platform: payload.Platform,
|
||||
Type: payload.Type,
|
||||
Logo: payload.Logo,
|
||||
Variables: payload.Variables,
|
||||
IsComposeFormat: payload.IsComposeFormat,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
|
||||
getProjectPath := func() string {
|
||||
return handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
|
||||
}
|
||||
projectPath := getProjectPath()
|
||||
customTemplate.ProjectPath = projectPath
|
||||
|
||||
repositoryUsername := payload.RepositoryUsername
|
||||
repositoryPassword := payload.RepositoryPassword
|
||||
if !payload.RepositoryAuthentication {
|
||||
repositoryUsername = ""
|
||||
repositoryPassword = ""
|
||||
gitConfig := &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.ComposeFilePathInRepository,
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword, payload.TLSSkipVerify)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
return nil, fmt.Errorf("invalid git credential")
|
||||
if payload.RepositoryAuthentication {
|
||||
gitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(*gitConfig, handler.GitService, getProjectPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitConfig.ConfigHash = commitHash
|
||||
customTemplate.GitConfig = gitConfig
|
||||
|
||||
isValidProject := true
|
||||
defer func() {
|
||||
if !isValidProject {
|
||||
@@ -298,7 +307,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
}
|
||||
}()
|
||||
|
||||
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
|
||||
entryPath := filesystem.JoinPaths(projectPath, gitConfig.ConfigFilePath)
|
||||
|
||||
exists, err := handler.FileService.FileExists(entryPath)
|
||||
if err != nil || !exists {
|
||||
@@ -310,6 +319,9 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if payload.Type == portainer.KubernetesStack {
|
||||
return nil, errors.New("Invalid Manifest file, ensure that the Manifest file path is correct")
|
||||
}
|
||||
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
|
||||
}
|
||||
|
||||
@@ -369,6 +381,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||
|
||||
platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true)
|
||||
templatePlatform := portainer.CustomTemplatePlatform(platform)
|
||||
// Platform validation is only for docker related stack (docker standalone and docker swarm)
|
||||
if templateType != portainer.KubernetesStack && templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
|
||||
@@ -40,7 +40,11 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
|
||||
return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.EntryPoint)
|
||||
entryPath := customTemplate.EntryPoint
|
||||
if customTemplate.GitConfig != nil {
|
||||
entryPath = customTemplate.GitConfig.ConfigFilePath
|
||||
}
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, entryPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve custom template file from disk", err)
|
||||
}
|
||||
|
||||
124
api/http/handler/customtemplates/customtemplate_git_fetch.go
Normal file
124
api/http/handler/customtemplates/customtemplate_git_fetch.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// @id CustomTemplateGitFetch
|
||||
// @summary Fetch the latest config file content based on custom template's git repository configuration
|
||||
// @description Retrieve details about a template created from git repository method.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Template identifier"
|
||||
// @success 200 {object} fileResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Custom template not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /custom_templates/{id}/git_fetch [put]
|
||||
func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid Custom template identifier route variable", err)
|
||||
}
|
||||
|
||||
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a custom template with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if customTemplate.GitConfig == nil {
|
||||
return httperror.BadRequest("Git configuration does not exist in this custom template", err)
|
||||
}
|
||||
|
||||
// If multiple users are trying to fetch the same custom template simultaneously, a lock needs to be added
|
||||
mu, ok := handler.gitFetchMutexs[portainer.TemplateID(customTemplateID)]
|
||||
if !ok {
|
||||
mu = &sync.Mutex{}
|
||||
handler.gitFetchMutexs[portainer.TemplateID(customTemplateID)] = mu
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// back up the current custom template folder
|
||||
backupPath, err := backupCustomTemplate(customTemplate.ProjectPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to backup the custom template folder", err)
|
||||
}
|
||||
|
||||
// remove backup custom template folder
|
||||
defer cleanUpBackupCustomTemplate(backupPath)
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(*customTemplate.GitConfig, handler.GitService, func() string {
|
||||
return customTemplate.ProjectPath
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to download git repository")
|
||||
rbErr := rollbackCustomTemplate(backupPath, customTemplate.ProjectPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to rollback the custom template folder", rbErr)
|
||||
}
|
||||
return httperror.InternalServerError("Failed to download git repository", err)
|
||||
}
|
||||
|
||||
if customTemplate.GitConfig.ConfigHash != commitHash {
|
||||
customTemplate.GitConfig.ConfigHash = commitHash
|
||||
|
||||
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist custom template changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.GitConfig.ConfigFilePath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve custom template file from disk", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, &fileResponse{FileContent: string(fileContent)})
|
||||
}
|
||||
|
||||
func backupCustomTemplate(projectPath string) (string, error) {
|
||||
stat, err := os.Stat(projectPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backupPath := fmt.Sprintf("%s-backup", projectPath)
|
||||
err = os.Rename(projectPath, backupPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = os.Mkdir(projectPath, stat.Mode())
|
||||
if err != nil {
|
||||
return backupPath, err
|
||||
}
|
||||
return backupPath, nil
|
||||
}
|
||||
|
||||
func rollbackCustomTemplate(backupPath, projectPath string) error {
|
||||
err := os.RemoveAll(projectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(backupPath, projectPath)
|
||||
}
|
||||
|
||||
func cleanUpBackupCustomTemplate(backupPath string) error {
|
||||
return os.RemoveAll(backupPath)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package customtemplates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testFileContent string = "abcdefg"
|
||||
|
||||
type TestGitService struct {
|
||||
portainer.GitService
|
||||
targetFilePath string
|
||||
}
|
||||
|
||||
func (g *TestGitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string, tlsSkipVerify bool) error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return createTestFile(g.targetFilePath)
|
||||
}
|
||||
|
||||
func (g *TestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type TestFileService struct {
|
||||
portainer.FileService
|
||||
}
|
||||
|
||||
func (f *TestFileService) GetFileContent(projectPath, configFilePath string) ([]byte, error) {
|
||||
return os.ReadFile(filepath.Join(projectPath, configFilePath))
|
||||
}
|
||||
|
||||
func createTestFile(targetPath string) error {
|
||||
f, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(testFileContent)
|
||||
return err
|
||||
}
|
||||
|
||||
func prepareTestFolder(projectPath, filename string) error {
|
||||
err := os.MkdirAll(projectPath, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createTestFile(filepath.Join(projectPath, filename))
|
||||
}
|
||||
|
||||
func singleAPIRequest(h *Handler, jwt string, is *assert.Assertions, expect string) {
|
||||
type response struct {
|
||||
FileContent string
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBuffer([]byte("{}")))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwt))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code)
|
||||
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
is.NoError(err, "ReadAll should not return error")
|
||||
|
||||
var resp response
|
||||
err = json.Unmarshal(body, &resp)
|
||||
is.NoError(err, "response should be list json")
|
||||
is.Equal(resp.FileContent, expect)
|
||||
}
|
||||
|
||||
func Test_customTemplateGitFetch(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
// create user(s)
|
||||
user1 := &portainer.User{ID: 1, Username: "user-1", Role: portainer.StandardUserRole, PortainerAuthorizations: authorization.DefaultPortainerAuthorizations()}
|
||||
err := store.User().Create(user1)
|
||||
is.NoError(err, "error creating user 1")
|
||||
|
||||
user2 := &portainer.User{ID: 2, Username: "user-2", Role: portainer.StandardUserRole, PortainerAuthorizations: authorization.DefaultPortainerAuthorizations()}
|
||||
err = store.User().Create(user2)
|
||||
is.NoError(err, "error creating user 2")
|
||||
|
||||
dir, err := os.Getwd()
|
||||
is.NoError(err, "error to get working directory")
|
||||
|
||||
template1 := &portainer.CustomTemplate{ID: 1, Title: "custom-template-1", ProjectPath: filepath.Join(dir, "fixtures/custom_template_1"), GitConfig: &gittypes.RepoConfig{ConfigFilePath: "test-config-path.txt"}}
|
||||
err = store.CustomTemplateService.Create(template1)
|
||||
is.NoError(err, "error creating custom template 1")
|
||||
|
||||
// prepare testing folder
|
||||
err = prepareTestFolder(template1.ProjectPath, template1.GitConfig.ConfigFilePath)
|
||||
is.NoError(err, "error creating testing folder")
|
||||
|
||||
defer os.RemoveAll(filepath.Join(dir, "fixtures"))
|
||||
|
||||
// setup services
|
||||
jwtService, err := jwt.NewService("1h", store)
|
||||
is.NoError(err, "Error initiating jwt service")
|
||||
requestBouncer := security.NewRequestBouncer(store, jwtService, nil)
|
||||
|
||||
gitService := &TestGitService{
|
||||
targetFilePath: filepath.Join(template1.ProjectPath, template1.GitConfig.ConfigFilePath),
|
||||
}
|
||||
fileService := &TestFileService{}
|
||||
|
||||
h := NewHandler(requestBouncer, store, fileService, gitService)
|
||||
|
||||
// generate two standard users' tokens
|
||||
jwt1, _ := jwtService.GenerateToken(&portainer.TokenData{ID: user1.ID, Username: user1.Username, Role: user1.Role})
|
||||
jwt2, _ := jwtService.GenerateToken(&portainer.TokenData{ID: user2.ID, Username: user2.Username, Role: user2.Role})
|
||||
|
||||
t.Run("can return the expected file content by a single call from one user", func(t *testing.T) {
|
||||
singleAPIRequest(h, jwt1, is, "abcdefg")
|
||||
})
|
||||
|
||||
t.Run("can return the expected file content by multiple calls from one user", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
singleAPIRequest(h, jwt1, is, "abcdefg")
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("can return the expected file content by multiple calls from different users", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(10)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(j int) {
|
||||
if j%1 == 0 {
|
||||
singleAPIRequest(h, jwt1, is, "abcdefg")
|
||||
} else {
|
||||
singleAPIRequest(h, jwt2, is, "abcdefg")
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("can return the expected file content after a new commit is made", func(t *testing.T) {
|
||||
singleAPIRequest(h, jwt1, is, "abcdefg")
|
||||
|
||||
testFileContent = "gfedcba"
|
||||
|
||||
singleAPIRequest(h, jwt2, is, "gfedcba")
|
||||
})
|
||||
}
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
type customTemplateUpdatePayload struct {
|
||||
@@ -29,18 +32,37 @@ type customTemplateUpdatePayload struct {
|
||||
Platform portainer.CustomTemplatePlatform `example:"1" enums:"1,2"`
|
||||
// Type of created stack (1 - swarm, 2 - compose, 3 - kubernetes)
|
||||
Type portainer.StackType `example:"1" enums:"1,2,3" validate:"required"`
|
||||
// URL of a Git repository hosting the Stack file
|
||||
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
|
||||
// Reference name of a Git repository hosting the Stack file
|
||||
RepositoryReferenceName string `example:"refs/heads/master"`
|
||||
// Use basic authentication to clone the Git repository
|
||||
RepositoryAuthentication bool `example:"true"`
|
||||
// Username used in basic authentication. Required when RepositoryAuthentication is true
|
||||
// and RepositoryGitCredentialID is 0
|
||||
RepositoryUsername string `example:"myGitUsername"`
|
||||
// Password used in basic authentication. Required when RepositoryAuthentication is true
|
||||
// and RepositoryGitCredentialID is 0
|
||||
RepositoryPassword string `example:"myGitPassword"`
|
||||
// GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication
|
||||
// is true and RepositoryUsername/RepositoryPassword are not provided
|
||||
RepositoryGitCredentialID int `example:"0"`
|
||||
// Path to the Stack file inside the Git repository
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
// Content of stack file
|
||||
FileContent string `validate:"required"`
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
// IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file
|
||||
IsComposeFormat bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Title) {
|
||||
return errors.New("Invalid custom template title")
|
||||
}
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return errors.New("Invalid file content")
|
||||
if govalidator.IsNull(payload.FileContent) && govalidator.IsNull(payload.RepositoryURL) {
|
||||
return errors.New("Either file content or git repository url need to be provided")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
@@ -55,7 +77,19 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
|
||||
return validateVariablesDefinitions(payload.Variables)
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
err := validateVariablesDefinitions(payload.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id CustomTemplateUpdate
|
||||
@@ -115,12 +149,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
|
||||
}
|
||||
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
_, err = handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist updated custom template file on disk", err)
|
||||
}
|
||||
|
||||
customTemplate.Title = payload.Title
|
||||
customTemplate.Logo = payload.Logo
|
||||
customTemplate.Description = payload.Description
|
||||
@@ -128,6 +156,42 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
customTemplate.Platform = payload.Platform
|
||||
customTemplate.Type = payload.Type
|
||||
customTemplate.Variables = payload.Variables
|
||||
customTemplate.IsComposeFormat = payload.IsComposeFormat
|
||||
|
||||
if payload.RepositoryURL != "" {
|
||||
if !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return httperror.BadRequest("Invalid repository URL. Must correspond to a valid URL format", err)
|
||||
}
|
||||
|
||||
gitConfig := &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.ComposeFilePathInRepository,
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
gitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(*gitConfig, handler.GitService, func() string {
|
||||
return customTemplate.ProjectPath
|
||||
})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
gitConfig.ConfigHash = commitHash
|
||||
customTemplate.GitConfig = gitConfig
|
||||
} else {
|
||||
templateFolder := strconv.Itoa(customTemplateID)
|
||||
_, err = handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist updated custom template file on disk", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -13,15 +14,20 @@ import (
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
gitFetchMutexs map[portainer.TemplateID]*sync.Mutex
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService, gitService portainer.GitService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
DataStore: dataStore,
|
||||
FileService: fileService,
|
||||
GitService: gitService,
|
||||
gitFetchMutexs: make(map[portainer.TemplateID]*sync.Mutex),
|
||||
}
|
||||
h.Handle("/custom_templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
|
||||
@@ -35,6 +41,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/custom_templates/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/custom_templates/{id}/git_fetch",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateGitFetch))).Methods(http.MethodPut)
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
@@ -26,12 +25,15 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("invalid Edge group name")
|
||||
}
|
||||
|
||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
|
||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||
return errors.New("environment is mandatory for a static Edge group")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,7 +58,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
@@ -101,13 +102,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
return txResponse(w, edgeGroup, err)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
@@ -27,12 +27,15 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("invalid Edge group name")
|
||||
}
|
||||
|
||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
|
||||
if !payload.Dynamic && len(payload.Endpoints) == 0 {
|
||||
return errors.New("environments is mandatory for a static Edge group")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -62,128 +65,135 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
|
||||
}
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
|
||||
return httperror.BadRequest("Edge group name must be unique", errors.New("edge group name must be unique"))
|
||||
}
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = tx.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
edgeGroup.Name = payload.Name
|
||||
}
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from database", err)
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from database", err)
|
||||
}
|
||||
|
||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
|
||||
edgeGroup.Dynamic = payload.Dynamic
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
} else {
|
||||
endpointIDs := []portainer.EndpointID{}
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if payload.Name != "" {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
|
||||
}
|
||||
|
||||
if endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
|
||||
return httperror.BadRequest("Edge group name must be unique", errors.New("edge group name must be unique"))
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroup.Name = payload.Name
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
if payload.PartialMatch != nil {
|
||||
edgeGroup.PartialMatch = *payload.PartialMatch
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
|
||||
}
|
||||
|
||||
newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
|
||||
|
||||
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch Edge jobs", err)
|
||||
}
|
||||
|
||||
for _, endpointID := range endpointsToUpdate {
|
||||
err = handler.updateEndpointStacks(endpointID)
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
|
||||
return httperror.InternalServerError("Unable to retrieve environments from database", err)
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpointGroups, err := tx.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Environment from database", err)
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from database", err)
|
||||
}
|
||||
|
||||
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
continue
|
||||
}
|
||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
|
||||
var operation string
|
||||
if slices.Contains(newRelatedEndpoints, endpointID) {
|
||||
operation = "add"
|
||||
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
||||
operation = "remove"
|
||||
edgeGroup.Dynamic = payload.Dynamic
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
} else {
|
||||
continue
|
||||
endpointIDs := []portainer.EndpointID{}
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
if endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
}
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
||||
if payload.PartialMatch != nil {
|
||||
edgeGroup.PartialMatch = *payload.PartialMatch
|
||||
}
|
||||
|
||||
err = tx.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
||||
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
|
||||
|
||||
edgeJobs, err := tx.EdgeJob().EdgeJobs()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch Edge jobs", err)
|
||||
}
|
||||
|
||||
for _, endpointID := range endpointsToUpdate {
|
||||
err = handler.updateEndpointStacks(tx, endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Environment from database", err)
|
||||
}
|
||||
|
||||
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
var operation string
|
||||
if slices.Contains(newRelatedEndpoints, endpointID) {
|
||||
operation = "add"
|
||||
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
||||
operation = "remove"
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return txResponse(w, edgeGroup, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) error {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) error {
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -197,7 +207,7 @@ func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) er
|
||||
|
||||
relation.EdgeStacks = edgeStackSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpoint *portainer.Endpoint, edgeJobs []portainer.EdgeJob, operation string) error {
|
||||
|
||||
@@ -3,11 +3,13 @@ package edgegroups
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||
@@ -34,3 +36,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
func txResponse(w http.ResponseWriter, r any, err error) *httperror.HandlerError {
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, r)
|
||||
}
|
||||
|
||||
@@ -7,16 +7,26 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/maps"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
type edgeJobBasePayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
}
|
||||
|
||||
// @id EdgeJobCreate
|
||||
// @summary Create an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
@@ -48,12 +58,8 @@ func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFileContentPayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
FileContent string
|
||||
edgeJobBasePayload
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
@@ -87,32 +93,44 @@ func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *h
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
edgeJob := handler.createEdgeJobObjectFromFileContentPayload(&payload)
|
||||
var edgeJob *portainer.EdgeJob
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
edgeJob, err = handler.createEdgeJob(handler.DataStore, &payload.edgeJobBasePayload, []byte(payload.FileContent))
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeJob, err = handler.createEdgeJob(tx, &payload.edgeJobBasePayload, []byte(payload.FileContent))
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
return txResponse(w, edgeJob, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJob(tx dataservices.DataStoreTx, payload *edgeJobBasePayload, fileContent []byte) (*portainer.EdgeJob, error) {
|
||||
var err error
|
||||
|
||||
edgeJob := handler.createEdgeJobObjectFromPayload(tx, payload)
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err = edge.GetEndpointsFromEdgeGroups(payload.EdgeGroups, handler.DataStore)
|
||||
endpoints, err = edge.GetEndpointsFromEdgeGroups(payload.EdgeGroups, tx)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
return nil, httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.addAndPersistEdgeJob(edgeJob, []byte(payload.FileContent), endpoints)
|
||||
|
||||
err = handler.addAndPersistEdgeJob(tx, edgeJob, fileContent, endpoints)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to schedule Edge job", err)
|
||||
return nil, httperror.InternalServerError("Unable to schedule Edge job", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
return edgeJob, nil
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFilePayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
File []byte
|
||||
edgeJobBasePayload
|
||||
File []byte
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
@@ -166,66 +184,35 @@ func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Req
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
edgeJob := handler.createEdgeJobObjectFromFilePayload(payload)
|
||||
var edgeJob *portainer.EdgeJob
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
edgeJob, err = handler.createEdgeJob(handler.DataStore, &payload.edgeJobBasePayload, payload.File)
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeJob, err = handler.createEdgeJob(tx, &payload.edgeJobBasePayload, payload.File)
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err = edge.GetEndpointsFromEdgeGroups(payload.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = handler.addAndPersistEdgeJob(edgeJob, payload.File, endpoints)
|
||||
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to schedule Edge job", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
return txResponse(w, edgeJob, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobObjectFromFilePayload(payload *edgeJobCreateFromFilePayload) *portainer.EdgeJob {
|
||||
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
|
||||
|
||||
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
|
||||
|
||||
edgeJob := &portainer.EdgeJob{
|
||||
ID: edgeJobIdentifier,
|
||||
func (handler *Handler) createEdgeJobObjectFromPayload(tx dataservices.DataStoreTx, payload *edgeJobBasePayload) *portainer.EdgeJob {
|
||||
return &portainer.EdgeJob{
|
||||
ID: portainer.EdgeJobID(tx.EdgeJob().GetNextIdentifier()),
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
Created: time.Now().Unix(),
|
||||
Endpoints: endpoints,
|
||||
Endpoints: convertEndpointsToMetaObject(payload.Endpoints),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Version: 1,
|
||||
GroupLogsCollection: map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{},
|
||||
}
|
||||
|
||||
return edgeJob
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobObjectFromFileContentPayload(payload *edgeJobCreateFromFileContentPayload) *portainer.EdgeJob {
|
||||
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
|
||||
|
||||
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
|
||||
|
||||
edgeJob := &portainer.EdgeJob{
|
||||
ID: edgeJobIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
Created: time.Now().Unix(),
|
||||
Endpoints: endpoints,
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Version: 1,
|
||||
GroupLogsCollection: map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{},
|
||||
}
|
||||
|
||||
return edgeJob
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []byte, endpointsFromGroups []portainer.EndpointID) error {
|
||||
func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJob *portainer.EdgeJob, file []byte, endpointsFromGroups []portainer.EndpointID) error {
|
||||
edgeCronExpression := strings.Split(edgeJob.CronExpression, " ")
|
||||
if len(edgeCronExpression) == 6 {
|
||||
edgeCronExpression = edgeCronExpression[1:]
|
||||
@@ -233,7 +220,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
edgeJob.CronExpression = strings.Join(edgeCronExpression, " ")
|
||||
|
||||
for ID := range edgeJob.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -254,7 +241,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
endpointsMap = convertEndpointsToMetaObject(endpointsFromGroups)
|
||||
|
||||
for ID := range endpointsMap {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -274,7 +261,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
}
|
||||
|
||||
for endpointID := range endpointsMap {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,5 +269,5 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
}
|
||||
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
return tx.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/maps"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -31,14 +33,34 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid Edge job identifier route variable", err)
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
err = handler.deleteEdgeJob(handler.DataStore, portainer.EdgeJobID(edgeJobID))
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.deleteEdgeJob(tx, portainer.EdgeJobID(edgeJobID))
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteEdgeJob(tx dataservices.DataStoreTx, edgeJobID portainer.EdgeJobID) error {
|
||||
edgeJob, err := tx.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(edgeJobID))
|
||||
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(int(edgeJobID)))
|
||||
err = handler.FileService.RemoveDirectory(edgeJobFolder)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to remove the files associated to the Edge job on the filesystem")
|
||||
@@ -48,7 +70,7 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
var endpointsMap map[portainer.EndpointID]portainer.EdgeJobEndpointMeta
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
@@ -63,10 +85,10 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpointID, edgeJob.ID)
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeJob().DeleteEdgeJob(edgeJob.ID)
|
||||
err = tx.EdgeJob().DeleteEdgeJob(edgeJob.ID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to remove the Edge job from the database", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
)
|
||||
|
||||
// @id EdgeJobTasksClear
|
||||
@@ -37,53 +39,86 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
|
||||
return httperror.BadRequest("Invalid Task identifier route variable", err)
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
mutationFn := func(edgeJob *portainer.EdgeJob, endpointID portainer.EndpointID, endpointsFromGroups []portainer.EndpointID) {
|
||||
if slices.Contains(endpointsFromGroups, endpointID) {
|
||||
edgeJob.GroupLogsCollection[endpointID] = portainer.EdgeJobEndpointMeta{
|
||||
CollectLogs: false,
|
||||
LogsStatus: portainer.EdgeJobLogsStatusIdle,
|
||||
}
|
||||
} else {
|
||||
meta := edgeJob.Endpoints[endpointID]
|
||||
meta.CollectLogs = false
|
||||
meta.LogsStatus = portainer.EdgeJobLogsStatusIdle
|
||||
edgeJob.Endpoints[endpointID] = meta
|
||||
}
|
||||
}
|
||||
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
|
||||
updateEdgeJobFn := func(edgeJob *portainer.EdgeJob, endpointID portainer.EndpointID, endpointsFromGroups []portainer.EndpointID) error {
|
||||
return handler.DataStore.EdgeJob().UpdateEdgeJobFunc(edgeJob.ID, func(j *portainer.EdgeJob) {
|
||||
mutationFn(j, endpointID, endpointsFromGroups)
|
||||
})
|
||||
}
|
||||
|
||||
err = handler.clearEdgeJobTaskLogs(handler.DataStore, portainer.EdgeJobID(edgeJobID), portainer.EndpointID(taskID), updateEdgeJobFn)
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
updateEdgeJobFn := func(edgeJob *portainer.EdgeJob, endpointID portainer.EndpointID, endpointsFromGroups []portainer.EndpointID) error {
|
||||
mutationFn(edgeJob, endpointID, endpointsFromGroups)
|
||||
|
||||
return tx.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
return handler.clearEdgeJobTaskLogs(tx, portainer.EdgeJobID(edgeJobID), portainer.EndpointID(taskID), updateEdgeJobFn)
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) clearEdgeJobTaskLogs(tx dataservices.DataStoreTx, edgeJobID portainer.EdgeJobID, endpointID portainer.EndpointID, updateEdgeJob func(*portainer.EdgeJob, portainer.EndpointID, []portainer.EndpointID) error) error {
|
||||
edgeJob, err := tx.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID)))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clear log file from disk", err)
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(taskID)
|
||||
endpointsFromGroups, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
endpointsFromGroups, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJobFunc(edgeJob.ID, func(j *portainer.EdgeJob) {
|
||||
if slices.Contains(endpointsFromGroups, endpointID) {
|
||||
j.GroupLogsCollection[endpointID] = portainer.EdgeJobEndpointMeta{
|
||||
CollectLogs: false,
|
||||
LogsStatus: portainer.EdgeJobLogsStatusIdle,
|
||||
}
|
||||
} else {
|
||||
meta := j.Endpoints[endpointID]
|
||||
meta.CollectLogs = false
|
||||
meta.LogsStatus = portainer.EdgeJobLogsStatusIdle
|
||||
j.Endpoints[endpointID] = meta
|
||||
}
|
||||
})
|
||||
|
||||
err = updateEdgeJob(edgeJob, endpointID, endpointsFromGroups)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Edge job changes in the database", err)
|
||||
}
|
||||
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID)))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clear log file from disk", err)
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
|
||||
return response.Empty(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeJob, err := tx.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/maps"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
)
|
||||
|
||||
type taskContainer struct {
|
||||
@@ -37,20 +38,34 @@ func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid Edge job identifier route variable", err)
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
var tasks []taskContainer
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
tasks, err = listEdgeJobTasks(handler.DataStore, portainer.EdgeJobID(edgeJobID))
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
tasks, err = listEdgeJobTasks(tx, portainer.EdgeJobID(edgeJobID))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
return txResponse(w, tasks, err)
|
||||
}
|
||||
|
||||
func listEdgeJobTasks(tx dataservices.DataStoreTx, edgeJobID portainer.EdgeJobID) ([]taskContainer, error) {
|
||||
edgeJob, err := tx.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil, httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
return nil, httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
tasks := make([]taskContainer, 0)
|
||||
|
||||
endpointsMap := map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{}
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
return nil, httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
|
||||
endpointsMap = convertEndpointsToMetaObject(endpoints)
|
||||
@@ -67,5 +82,5 @@ func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(w, tasks)
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/maps"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,27 +62,41 @@ func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
var edgeJob *portainer.EdgeJob
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
edgeJob, err = handler.updateEdgeJob(handler.DataStore, portainer.EdgeJobID(edgeJobID), payload)
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeJob, err = handler.updateEdgeJob(tx, portainer.EdgeJobID(edgeJobID), payload)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = handler.updateEdgeSchedule(edgeJob, &payload)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update Edge job", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Edge job changes inside the database", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
return txResponse(w, edgeJob, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
|
||||
func (handler *Handler) updateEdgeJob(tx dataservices.DataStoreTx, edgeJobID portainer.EdgeJobID, payload edgeJobUpdatePayload) (*portainer.EdgeJob, error) {
|
||||
edgeJob, err := tx.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil, httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.updateEdgeSchedule(tx, edgeJob, &payload)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update Edge job", err)
|
||||
}
|
||||
|
||||
err = tx.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist Edge job changes inside the database", err)
|
||||
}
|
||||
|
||||
return edgeJob, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
|
||||
if payload.Name != nil {
|
||||
edgeJob.Name = *payload.Name
|
||||
}
|
||||
@@ -99,7 +115,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,7 +136,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
|
||||
if len(payload.EdgeGroups) == 0 && len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -138,7 +154,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
|
||||
if len(payload.EdgeGroups) > 0 {
|
||||
for _, edgeGroupID := range payload.EdgeGroups {
|
||||
_, err := handler.DataStore.EdgeGroup().EdgeGroup(edgeGroupID)
|
||||
_, err := tx.EdgeGroup().EdgeGroup(edgeGroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -148,7 +164,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
}
|
||||
|
||||
endpointsFromGroupsToAdd, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToAdd, handler.DataStore)
|
||||
endpointsFromGroupsToAdd, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToAdd, tx)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -165,7 +181,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
}
|
||||
}
|
||||
|
||||
endpointsFromGroupsToRemove, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToRemove, handler.DataStore)
|
||||
endpointsFromGroupsToRemove, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToRemove, tx)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -212,7 +228,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *
|
||||
maps.Copy(endpointsFromGroupsToAddMap, edgeJob.Endpoints)
|
||||
|
||||
for endpointID := range endpointsFromGroupsToAddMap {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package edgejobs
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle Edge job operations.
|
||||
@@ -44,6 +46,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksCollect)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksClear)))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -56,3 +59,15 @@ func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portaine
|
||||
|
||||
return endpointsMap
|
||||
}
|
||||
|
||||
func txResponse(w http.ResponseWriter, r any, err error) *httperror.HandlerError {
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, r)
|
||||
}
|
||||
|
||||
@@ -81,16 +81,14 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", nil)
|
||||
// EE-5910
|
||||
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||
// EE-5910
|
||||
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
|
||||
@@ -30,7 +30,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
{
|
||||
portainer.Endpoint{},
|
||||
portainer.EndpointRelation{},
|
||||
http.StatusNotFound,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
@@ -43,7 +43,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: -1,
|
||||
},
|
||||
http.StatusNotFound,
|
||||
http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
|
||||
@@ -38,7 +38,6 @@ type endpointCreatePayload struct {
|
||||
AzureAuthenticationKey string
|
||||
TagIDs []portainer.TagID
|
||||
EdgeCheckinInterval int
|
||||
IsEdgeDevice bool
|
||||
}
|
||||
|
||||
type endpointCreationEnum int
|
||||
@@ -381,7 +380,6 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
EdgeKey: edgeKey,
|
||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
UserTrusted: true,
|
||||
}
|
||||
|
||||
@@ -435,7 +433,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
err := handler.snapshotAndPersistEndpoint(endpoint)
|
||||
@@ -501,7 +498,6 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -128,7 +129,6 @@ func getDockerHubToken(httpClient *client.HTTPClient, registry *portainer.Regist
|
||||
}
|
||||
|
||||
func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhubStatusResponse, error) {
|
||||
|
||||
requestURL := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
||||
@@ -142,7 +142,9 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed fetching dockerhub limits")
|
||||
|
||||
89
api/http/handler/gitops/git_repo_file_preview.go
Normal file
89
api/http/handler/gitops/git_repo_file_preview.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package gitops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
)
|
||||
|
||||
type fileResponse struct {
|
||||
FileContent string
|
||||
}
|
||||
|
||||
type repositoryFilePreviewPayload struct {
|
||||
Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"`
|
||||
Reference string `json:"reference" example:"refs/heads/master"`
|
||||
Username string `json:"username" example:"myGitUsername"`
|
||||
Password string `json:"password" example:"myGitPassword"`
|
||||
// Path to file whose content will be read
|
||||
TargetFile string `json:"targetFile" example:"docker-compose.yml"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Repository) || !govalidator.IsURL(payload.Repository) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Reference) {
|
||||
payload.Reference = "refs/heads/main"
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.TargetFile) {
|
||||
return errors.New("Invalid target filename.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id GitOperationRepoFilePreview
|
||||
// @summary preview the content of target file in the git repository
|
||||
// @description Retrieve the compose file content based on git repository configuration
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags gitops
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param body body repositoryFilePreviewPayload true "Template details"
|
||||
// @success 200 {object} fileResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /gitops/repo/file/preview [post]
|
||||
func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload repositoryFilePreviewPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
projectPath, err := handler.fileService.GetTemporaryPath()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to create temporary folder", err)
|
||||
}
|
||||
|
||||
err = handler.gitService.CloneRepository(projectPath, payload.Repository, payload.Reference, payload.Username, payload.Password, payload.TLSSkipVerify)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
return httperror.BadRequest("Invalid git credential", err)
|
||||
}
|
||||
|
||||
newErr := fmt.Errorf("unable to clone git repository: %w", err)
|
||||
return httperror.InternalServerError(newErr.Error(), newErr)
|
||||
}
|
||||
|
||||
defer handler.fileService.RemoveDirectory(projectPath)
|
||||
|
||||
fileContent, err := handler.fileService.GetFileContent(projectPath, payload.TargetFile)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve custom template file from disk", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, &fileResponse{FileContent: string(fileContent)})
|
||||
}
|
||||
33
api/http/handler/gitops/handler.go
Normal file
33
api/http/handler/gitops/handler.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package gitops
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle git repo operation
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
dataStore dataservices.DataStore
|
||||
gitService portainer.GitService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gitService portainer.GitService, fileService portainer.FileService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
dataStore: dataStore,
|
||||
gitService: gitService,
|
||||
fileService: fileService,
|
||||
}
|
||||
|
||||
h.Handle("/gitops/repo/file/preview",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.gitOperationRepoFilePreview))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
@@ -56,6 +57,7 @@ type Handler struct {
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointHelmHandler *helm.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
GitOperationHandler *gitops.Handler
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
@@ -82,7 +84,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.18.2
|
||||
// @version 2.19.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -121,6 +123,8 @@ type Handler struct {
|
||||
// @tag.description Manage Docker environments(endpoints)
|
||||
// @tag.name endpoint_groups
|
||||
// @tag.description Manage environment(endpoint) groups
|
||||
// @tag.name gitops
|
||||
// @tag.description Operate git repository
|
||||
// @tag.name kubernetes
|
||||
// @tag.description Manage Kubernetes cluster
|
||||
// @tag.name motd
|
||||
@@ -203,6 +207,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/gitops"):
|
||||
http.StripPrefix("/api", h.GitOperationHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ldap"):
|
||||
http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
|
||||
@@ -48,15 +48,16 @@ func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "editor":
|
||||
if method == "editor" {
|
||||
return handler.createFDOProfileFromFileContent(w, r)
|
||||
}
|
||||
|
||||
return httperror.BadRequest("Invalid method. Value must be one of: editor", errors.New("invalid method"))
|
||||
}
|
||||
|
||||
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload createProfileFromFileContentPayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
@@ -66,6 +67,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), Err: errors.New("a profile already exists with this name")}
|
||||
}
|
||||
@@ -80,6 +82,7 @@ func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
||||
}
|
||||
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
|
||||
@@ -23,14 +23,17 @@ type kubernetesStringDeploymentPayload struct {
|
||||
ComposeFormat bool
|
||||
Namespace string
|
||||
StackFileContent string
|
||||
// Whether the stack is from a app template
|
||||
FromAppTemplate bool `example:"false"`
|
||||
}
|
||||
|
||||
func createStackPayloadFromK8sFileContentPayload(name, namespace, fileContent string, composeFormat bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromK8sFileContentPayload(name, namespace, fileContent string, composeFormat, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
StackName: name,
|
||||
Namespace: namespace,
|
||||
StackFileContent: fileContent,
|
||||
ComposeFormat: composeFormat,
|
||||
FromAppTemplate: fromAppTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +149,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
|
||||
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: stackutils.ErrStackAlreadyExists}
|
||||
}
|
||||
|
||||
stackPayload := createStackPayloadFromK8sFileContentPayload(payload.StackName, payload.Namespace, payload.StackFileContent, payload.ComposeFormat)
|
||||
stackPayload := createStackPayloadFromK8sFileContentPayload(payload.StackName, payload.Namespace, payload.StackFileContent, payload.ComposeFormat, payload.FromAppTemplate)
|
||||
|
||||
k8sStackBuilder := stackbuilders.CreateK8sStackFileContentBuilder(handler.DataStore,
|
||||
handler.FileService,
|
||||
|
||||
@@ -150,7 +150,6 @@ func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
ComposeFile: composeFile,
|
||||
AdditionalFiles: additionalFiles,
|
||||
|
||||
@@ -25,7 +25,6 @@ type stackGitUpdatePayload struct {
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -139,7 +138,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
//update retrieved stack data based on the payload
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
stack.Env = payload.Env
|
||||
stack.UpdatedBy = user.Username
|
||||
|
||||
@@ -31,7 +31,6 @@ type kubernetesGitStackUpdatePayload struct {
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -63,7 +62,6 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
}
|
||||
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
|
||||
@@ -10,10 +10,9 @@ import (
|
||||
)
|
||||
|
||||
type systemInfoResponse struct {
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
EdgeDevices int `json:"edgeDevices"`
|
||||
Agents int `json:"agents"`
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
Agents int `json:"agents"`
|
||||
}
|
||||
|
||||
// @id systemInfo
|
||||
@@ -34,7 +33,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||
|
||||
agents := 0
|
||||
edgeAgents := 0
|
||||
edgeDevices := 0
|
||||
|
||||
for _, environment := range environments {
|
||||
if endpointutils.IsAgentEndpoint(&environment) {
|
||||
@@ -45,9 +43,6 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||
edgeAgents++
|
||||
}
|
||||
|
||||
if environment.IsEdgeDevice {
|
||||
edgeDevices++
|
||||
}
|
||||
}
|
||||
|
||||
platform, err := plf.DetermineContainerPlatform()
|
||||
@@ -56,9 +51,8 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
return response.JSON(w, &systemInfoResponse{
|
||||
EdgeAgents: edgeAgents,
|
||||
EdgeDevices: edgeDevices,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
EdgeAgents: edgeAgents,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
)
|
||||
|
||||
type systemUpgradePayload struct {
|
||||
@@ -28,13 +30,19 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
|
||||
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
|
||||
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
|
||||
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
|
||||
}
|
||||
|
||||
// @id systemUpgrade
|
||||
// @summary Upgrade Portainer to BE
|
||||
// @description Upgrade Portainer to BE
|
||||
// @description **Access policy**: administrator
|
||||
// @tags system
|
||||
// @produce json
|
||||
// @success 200 {object} status "Success"
|
||||
// @success 204 {object} status "Success"
|
||||
// @router /system/upgrade [post]
|
||||
func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload, err := request.GetPayload[systemUpgradePayload](r)
|
||||
@@ -42,10 +50,40 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
err = handler.upgradeService.Upgrade(payload.License)
|
||||
environment, err := handler.guessLocalEndpoint()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to guess local endpoint", err)
|
||||
}
|
||||
|
||||
err = handler.upgradeService.Upgrade(environment, payload.License)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to upgrade Portainer", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) guessLocalEndpoint() (*portainer.Endpoint, error) {
|
||||
platform, err := platform.DetermineContainerPlatform()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to determine container platform")
|
||||
}
|
||||
|
||||
endpointType, ok := platformToEndpointType[platform]
|
||||
if !ok {
|
||||
return nil, errors.New("failed to determine endpoint type")
|
||||
}
|
||||
|
||||
endpoints, err := handler.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to retrieve endpoints")
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == endpointType {
|
||||
return &endpoint, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("failed to find local endpoint")
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package tags
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle tag operations.
|
||||
@@ -29,3 +31,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func txResponse(w http.ResponseWriter, r any, err error) *httperror.HandlerError {
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, r)
|
||||
}
|
||||
|
||||
@@ -7,19 +7,20 @@ import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
)
|
||||
|
||||
type tagCreatePayload struct {
|
||||
// Name
|
||||
Name string `validate:"required" example:"org/acme"`
|
||||
}
|
||||
|
||||
func (payload *tagCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return errors.New("Invalid tag name")
|
||||
return errors.New("invalid tag name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -44,14 +45,28 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
var tag *portainer.Tag
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
tag, err = createTag(handler.DataStore, payload)
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
tag, err = createTag(tx, payload)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
return txResponse(w, tag, err)
|
||||
}
|
||||
|
||||
func createTag(tx dataservices.DataStoreTx, payload tagCreatePayload) (*portainer.Tag, error) {
|
||||
tags, err := tx.Tag().Tags()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve tags from the database", err)
|
||||
return nil, httperror.InternalServerError("Unable to retrieve tags from the database", err)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Name == payload.Name {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "This name is already associated to a tag", Err: errors.New("A tag already exists with this name")}
|
||||
return nil, &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "This name is already associated to a tag", Err: errors.New("a tag already exists with this name")}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +76,10 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
Endpoints: map[portainer.EndpointID]bool{},
|
||||
}
|
||||
|
||||
err = handler.DataStore.Tag().Create(tag)
|
||||
err = tx.Tag().Create(tag)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the tag inside the database", err)
|
||||
return nil, httperror.InternalServerError("Unable to persist the tag inside the database", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, tag)
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
)
|
||||
|
||||
// @id TagDelete
|
||||
@@ -29,89 +31,119 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid tag identifier route variable", err)
|
||||
}
|
||||
tagID := portainer.TagID(id)
|
||||
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
err = deleteTag(handler.DataStore, portainer.TagID(id))
|
||||
} else {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return deleteTag(tx, portainer.TagID(id))
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(*httperror.HandlerError); ok {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error {
|
||||
tag, err := tx.Tag().Tag(tagID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a tag with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a tag with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
for endpointID := range tag.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
endpoint.TagIDs = removeElement(endpoint.TagIDs, tagID)
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
err = tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
}
|
||||
|
||||
for endpointGroupID := range tag.EndpointGroups {
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpointGroupID)
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpointGroupID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment group from the database", err)
|
||||
}
|
||||
|
||||
endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagID)
|
||||
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
err = tx.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment group", err)
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge groups from the database", err)
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge stacks from the database", err)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) {
|
||||
err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks)
|
||||
err = updateEndpointRelations(tx, endpoint, edgeGroups, edgeStacks)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment relations in the database", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
err = handler.DataStore.EdgeGroup().UpdateEdgeGroupFunc(edgeGroup.ID, func(g *portainer.EdgeGroup) {
|
||||
g.TagIDs = removeElement(g.TagIDs, tagID)
|
||||
})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update edge group", err)
|
||||
if featureflags.IsEnabled(portainer.FeatureNoTx) {
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
err = tx.EdgeGroup().UpdateEdgeGroupFunc(edgeGroup.ID, func(g *portainer.EdgeGroup) {
|
||||
g.TagIDs = removeElement(g.TagIDs, tagID)
|
||||
})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update edge group", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
edgeGroup.TagIDs = removeElement(edgeGroup.TagIDs, tagID)
|
||||
|
||||
err = tx.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, &edgeGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update edge group", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Tag().DeleteTag(tagID)
|
||||
err = tx.Tag().DeleteTag(tagID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to remove the tag from the database", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||
endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||
endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -123,7 +155,7 @@ func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edg
|
||||
}
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
|
||||
func removeElement(slice []portainer.TagID, elem portainer.TagID) []portainer.TagID {
|
||||
|
||||
@@ -63,6 +63,7 @@ func (handler *Handler) ifRequestedTemplateExists(payload *filePayload) *httperr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Invalid template", errors.New("requested template does not exist"))
|
||||
}
|
||||
|
||||
@@ -82,6 +83,7 @@ func (handler *Handler) ifRequestedTemplateExists(payload *filePayload) *httperr
|
||||
// @router /templates/file [post]
|
||||
func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload filePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
@@ -112,11 +114,9 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
}
|
||||
|
||||
func (handler *Handler) cleanUp(projectPath string) error {
|
||||
func (handler *Handler) cleanUp(projectPath string) {
|
||||
err := handler.FileService.RemoveDirectory(projectPath)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("HTTP error: unable to cleanup stack creation")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,48 +14,57 @@ func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer
|
||||
_, in, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
_, err = writer.Write(in)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
||||
out := make([]byte, readerBufferSize)
|
||||
|
||||
for {
|
||||
out := make([]byte, readerBufferSize)
|
||||
_, err := reader.Read(out)
|
||||
n, err := reader.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
processedOutput := validString(string(out[:]))
|
||||
processedOutput := validString(string(out[:n]))
|
||||
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validString(s string) string {
|
||||
if !utf8.ValidString(s) {
|
||||
v := make([]rune, 0, len(s))
|
||||
for i, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
v = append(v, r)
|
||||
}
|
||||
s = string(v)
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
return s
|
||||
|
||||
v := make([]rune, 0, len(s))
|
||||
|
||||
for i, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
v = append(v, r)
|
||||
}
|
||||
|
||||
return string(v)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func (transport *Transport) applyPortainerContainers(resources []interface{}) ([
|
||||
responseObject, _ = transport.applyPortainerContainer(responseObject)
|
||||
decoratedResourceData = append(decoratedResourceData, responseObject)
|
||||
}
|
||||
|
||||
return decoratedResourceData, nil
|
||||
}
|
||||
|
||||
@@ -34,8 +35,10 @@ func (transport *Transport) applyPortainerContainer(resourceObject map[string]in
|
||||
if !ok {
|
||||
return resourceObject, nil
|
||||
}
|
||||
|
||||
if len(resourceId) >= 12 && resourceId[0:12] == portainerContainerId {
|
||||
resourceObject["IsPortainer"] = true
|
||||
}
|
||||
|
||||
return resourceObject, nil
|
||||
}
|
||||
|
||||
@@ -150,8 +150,7 @@ func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenMa
|
||||
func decorateAgentRequest(r *http.Request, dataStore dataservices.DataStore) error {
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
||||
if strings.HasPrefix(requestPath, "/dockerhub") {
|
||||
return decorateAgentDockerHubRequest(r, dataStore)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
@@ -143,10 +144,7 @@ func (server *Server) Start() error {
|
||||
var roleHandler = roles.NewHandler(requestBouncer)
|
||||
roleHandler.DataStore = server.DataStore
|
||||
|
||||
var customTemplatesHandler = customtemplates.NewHandler(requestBouncer)
|
||||
customTemplatesHandler.DataStore = server.DataStore
|
||||
customTemplatesHandler.FileService = server.FileService
|
||||
customTemplatesHandler.GitService = server.GitService
|
||||
var customTemplatesHandler = customtemplates.NewHandler(requestBouncer, server.DataStore, server.FileService, server.GitService)
|
||||
|
||||
var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
|
||||
edgeGroupsHandler.DataStore = server.DataStore
|
||||
@@ -196,6 +194,8 @@ func (server *Server) Start() error {
|
||||
|
||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
|
||||
|
||||
var gitOperationHandler = gitops.NewHandler(requestBouncer, server.DataStore, server.GitService, server.FileService)
|
||||
|
||||
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
|
||||
|
||||
var ldapHandler = ldap.NewHandler(requestBouncer)
|
||||
@@ -297,6 +297,7 @@ func (server *Server) Start() error {
|
||||
EndpointHelmHandler: endpointHelmHandler,
|
||||
EndpointEdgeHandler: endpointEdgeHandler,
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
GitOperationHandler: gitOperationHandler,
|
||||
FileHandler: fileHandler,
|
||||
LDAPHandler: ldapHandler,
|
||||
HelmTemplatesHandler: helmTemplatesHandler,
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cbroglie/mustache"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/platform"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,19 +26,23 @@ const (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Upgrade(licenseKey string) error
|
||||
Upgrade(environment *portainer.Endpoint, licenseKey string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
composeDeployer libstack.Deployer
|
||||
isUpdating bool
|
||||
platform platform.ContainerPlatform
|
||||
assetsPath string
|
||||
composeDeployer libstack.Deployer
|
||||
kubernetesClientFactory *cli.ClientFactory
|
||||
|
||||
isUpdating bool
|
||||
platform platform.ContainerPlatform
|
||||
|
||||
assetsPath string
|
||||
}
|
||||
|
||||
func NewService(
|
||||
assetsPath string,
|
||||
composeDeployer libstack.Deployer,
|
||||
kubernetesClientFactory *cli.ClientFactory,
|
||||
) (Service, error) {
|
||||
platform, err := platform.DetermineContainerPlatform()
|
||||
if err != nil {
|
||||
@@ -56,13 +50,14 @@ func NewService(
|
||||
}
|
||||
|
||||
return &service{
|
||||
assetsPath: assetsPath,
|
||||
composeDeployer: composeDeployer,
|
||||
platform: platform,
|
||||
assetsPath: assetsPath,
|
||||
composeDeployer: composeDeployer,
|
||||
kubernetesClientFactory: kubernetesClientFactory,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *service) Upgrade(licenseKey string) error {
|
||||
func (service *service) Upgrade(environment *portainer.Endpoint, licenseKey string) error {
|
||||
service.isUpdating = true
|
||||
|
||||
switch service.platform {
|
||||
@@ -70,113 +65,9 @@ func (service *service) Upgrade(licenseKey string) error {
|
||||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "standalone")
|
||||
case platform.PlatformDockerSwarm:
|
||||
return service.upgradeDocker(licenseKey, portainer.APIVersion, "swarm")
|
||||
// case platform.PlatformKubernetes:
|
||||
// case platform.PlatformPodman:
|
||||
// case platform.PlatformNomad:
|
||||
// default:
|
||||
case platform.PlatformKubernetes:
|
||||
return service.upgradeKubernetes(environment, licenseKey, portainer.APIVersion)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported platform %s", service.platform)
|
||||
}
|
||||
|
||||
func (service *service) upgradeDocker(licenseKey, version, envType string) error {
|
||||
ctx := context.TODO()
|
||||
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
|
||||
|
||||
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||
if portainerImagePrefix == "" {
|
||||
portainerImagePrefix = "portainer/portainer-ee"
|
||||
}
|
||||
|
||||
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||
|
||||
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
||||
|
||||
if err := service.checkImage(ctx, image, skipPullImage != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
||||
"image": image,
|
||||
"skip_pull_image": skipPullImage,
|
||||
"updater_image": os.Getenv(updaterImageEnvVar),
|
||||
"license": licenseKey,
|
||||
"envType": envType,
|
||||
})
|
||||
|
||||
log.Debug().
|
||||
Str("composeFile", composeFile).
|
||||
Msg("Compose file for upgrade")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to render upgrade template")
|
||||
}
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
timeId := time.Now().Unix()
|
||||
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId))
|
||||
|
||||
r := bytes.NewReader([]byte(composeFile))
|
||||
|
||||
err = filesystem.CreateFile(filePath, r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create upgrade compose file")
|
||||
}
|
||||
|
||||
projectName := fmt.Sprintf(
|
||||
"portainer-upgrade-%d-%s",
|
||||
timeId,
|
||||
strings.Replace(version, ".", "-", -1))
|
||||
|
||||
err = service.composeDeployer.Deploy(
|
||||
ctx,
|
||||
[]string{filePath},
|
||||
libstack.DeployOptions{
|
||||
ForceRecreate: true,
|
||||
AbortOnContainerExit: true,
|
||||
Options: libstack.Options{
|
||||
ProjectName: projectName,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// optimally, server was restarted by the updater, so we should not reach this point
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to deploy upgrade stack")
|
||||
}
|
||||
|
||||
return errors.New("upgrade failed: server should have been restarted by the updater")
|
||||
}
|
||||
|
||||
func (service *service) checkImage(ctx context.Context, image string, skipPullImage bool) error {
|
||||
cli, err := docker.CreateClientFromEnv()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create docker client")
|
||||
}
|
||||
|
||||
if skipPullImage {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("reference", image)
|
||||
images, err := cli.ImageList(ctx, types.ImageListOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list images")
|
||||
}
|
||||
|
||||
if len(images) == 0 {
|
||||
return errors.Errorf("image %s not found locally", image)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
// check if available on registry
|
||||
_, err := cli.DistributionInspect(ctx, image, "")
|
||||
if err != nil {
|
||||
return errors.Errorf("image %s not found on registry", image)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
122
api/internal/upgrade/upgrade_docker.go
Normal file
122
api/internal/upgrade/upgrade_docker.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
||||
"github.com/cbroglie/mustache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (service *service) upgradeDocker(licenseKey, version, envType string) error {
|
||||
ctx := context.TODO()
|
||||
templateName := filesystem.JoinPaths(service.assetsPath, "mustache-templates", mustacheUpgradeDockerTemplateFile)
|
||||
|
||||
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||
if portainerImagePrefix == "" {
|
||||
portainerImagePrefix = "portainer/portainer-ee"
|
||||
}
|
||||
|
||||
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||
|
||||
skipPullImage := os.Getenv(skipPullImageEnvVar)
|
||||
|
||||
if err := service.checkImageForDocker(ctx, image, skipPullImage != ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
composeFile, err := mustache.RenderFile(templateName, map[string]string{
|
||||
"image": image,
|
||||
"skip_pull_image": skipPullImage,
|
||||
"updater_image": os.Getenv(updaterImageEnvVar),
|
||||
"license": licenseKey,
|
||||
"envType": envType,
|
||||
})
|
||||
|
||||
log.Debug().
|
||||
Str("composeFile", composeFile).
|
||||
Msg("Compose file for upgrade")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to render upgrade template")
|
||||
}
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
timeId := time.Now().Unix()
|
||||
filePath := filesystem.JoinPaths(tmpDir, fmt.Sprintf("upgrade-%d.yml", timeId))
|
||||
|
||||
r := bytes.NewReader([]byte(composeFile))
|
||||
|
||||
err = filesystem.CreateFile(filePath, r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create upgrade compose file")
|
||||
}
|
||||
|
||||
projectName := fmt.Sprintf(
|
||||
"portainer-upgrade-%d-%s",
|
||||
timeId,
|
||||
strings.ReplaceAll(version, ".", "-"))
|
||||
|
||||
err = service.composeDeployer.Deploy(
|
||||
ctx,
|
||||
[]string{filePath},
|
||||
libstack.DeployOptions{
|
||||
ForceRecreate: true,
|
||||
AbortOnContainerExit: true,
|
||||
Options: libstack.Options{
|
||||
ProjectName: projectName,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// optimally, server was restarted by the updater, so we should not reach this point
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to deploy upgrade stack")
|
||||
}
|
||||
|
||||
return errors.New("upgrade failed: server should have been restarted by the updater")
|
||||
}
|
||||
|
||||
func (service *service) checkImageForDocker(ctx context.Context, image string, skipPullImage bool) error {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create docker client")
|
||||
}
|
||||
|
||||
if skipPullImage {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("reference", image)
|
||||
images, err := cli.ImageList(ctx, types.ImageListOptions{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list images")
|
||||
}
|
||||
|
||||
if len(images) == 0 {
|
||||
return errors.Errorf("image %s not found locally", image)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
// check if available on registry
|
||||
_, err := cli.DistributionInspect(ctx, image, "")
|
||||
if err != nil {
|
||||
return errors.Errorf("image %s not found on registry", image)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
201
api/internal/upgrade/upgrade_kubernetes.go
Normal file
201
api/internal/upgrade/upgrade_kubernetes.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/rs/zerolog/log"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
func ptr[T any](i T) *T { return &i }
|
||||
|
||||
func (service *service) upgradeKubernetes(environment *portainer.Endpoint, licenseKey, version string) error {
|
||||
ctx := context.TODO()
|
||||
|
||||
kubeCLI, err := service.kubernetesClientFactory.CreateClient(environment)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get kubernetes client")
|
||||
}
|
||||
|
||||
namespace := "portainer"
|
||||
taskName := fmt.Sprintf("portainer-upgrade-%d", time.Now().Unix())
|
||||
|
||||
jobsCli := kubeCLI.BatchV1().Jobs(namespace)
|
||||
|
||||
updaterImage := os.Getenv(updaterImageEnvVar)
|
||||
if updaterImage == "" {
|
||||
updaterImage = "portainer/portainer-updater:latest"
|
||||
}
|
||||
|
||||
portainerImagePrefix := os.Getenv(portainerImagePrefixEnvVar)
|
||||
if portainerImagePrefix == "" {
|
||||
portainerImagePrefix = "portainer/portainer-ee"
|
||||
}
|
||||
|
||||
image := fmt.Sprintf("%s:%s", portainerImagePrefix, version)
|
||||
|
||||
if err := service.checkImageForKubernetes(ctx, kubeCLI, namespace, image); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job, err := jobsCli.Create(ctx, &batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: taskName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
|
||||
Spec: batchv1.JobSpec{
|
||||
TTLSecondsAfterFinished: ptr[int32](5 * 60), // cleanup after 5 minutes
|
||||
BackoffLimit: ptr[int32](0),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
|
||||
RestartPolicy: "Never",
|
||||
ServiceAccountName: "portainer-sa-clusteradmin",
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: taskName,
|
||||
Image: updaterImage,
|
||||
Args: []string{
|
||||
"--pretty-log",
|
||||
"--log-level", "DEBUG",
|
||||
"portainer",
|
||||
"--env-type", "kubernetes",
|
||||
"--image", image,
|
||||
"--license", licenseKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to create upgrade job")
|
||||
}
|
||||
|
||||
watcher, err := jobsCli.Watch(ctx, metav1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + taskName,
|
||||
TimeoutSeconds: ptr[int64](60),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to watch upgrade job")
|
||||
}
|
||||
|
||||
for event := range watcher.ResultChan() {
|
||||
job, ok := event.Object.(*batchv1.Job)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range job.Status.Conditions {
|
||||
if c.Type == batchv1.JobComplete {
|
||||
log.Debug().
|
||||
Str("job", job.Name).
|
||||
Msg("Upgrade job completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Type == batchv1.JobFailed {
|
||||
return fmt.Errorf("upgrade failed: %s", c.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("job", job.Name).
|
||||
Msg("Upgrade job created")
|
||||
|
||||
return errors.New("upgrade failed: server should have been restarted by the updater")
|
||||
|
||||
}
|
||||
|
||||
func (service *service) checkImageForKubernetes(ctx context.Context, kubeCLI *kubernetes.Clientset, namespace, image string) error {
|
||||
podsCli := kubeCLI.CoreV1().Pods(namespace)
|
||||
|
||||
log.Debug().
|
||||
Str("image", image).
|
||||
Msg("Checking image")
|
||||
|
||||
podName := fmt.Sprintf("portainer-image-check-%d", time.Now().Unix())
|
||||
_, err := podsCli.Create(ctx, &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: podName,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
RestartPolicy: "Never",
|
||||
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: fmt.Sprint(podName, "-container"),
|
||||
Image: image,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to create image check pod")
|
||||
return errors.WithMessage(err, "failed to create image check pod")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
log.Debug().
|
||||
Str("pod", podName).
|
||||
Msg("Deleting image check pod")
|
||||
|
||||
if err := podsCli.Delete(ctx, podName, metav1.DeleteOptions{}); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to delete image check pod")
|
||||
}
|
||||
}()
|
||||
|
||||
i := 0
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
log.Debug().
|
||||
Str("image", image).
|
||||
Int("try", i).
|
||||
Msg("Checking image")
|
||||
|
||||
i++
|
||||
|
||||
pod, err := podsCli.Get(ctx, podName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get image check pod")
|
||||
}
|
||||
|
||||
for _, containerStatus := range pod.Status.ContainerStatuses {
|
||||
if containerStatus.Ready {
|
||||
log.Debug().
|
||||
Str("image", image).
|
||||
Str("pod", podName).
|
||||
Msg("Image check container ready, assuming image is available")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if containerStatus.State.Waiting != nil {
|
||||
if containerStatus.State.Waiting.Reason == "ErrImagePull" || containerStatus.State.Waiting.Reason == "ImagePullBackOff" {
|
||||
log.Debug().
|
||||
Str("image", image).
|
||||
Str("pod", podName).
|
||||
Str("reason", containerStatus.State.Waiting.Reason).
|
||||
Str("message", containerStatus.State.Waiting.Message).
|
||||
Str("container", containerStatus.Name).
|
||||
Msg("Image check container failed because of missing image")
|
||||
return fmt.Errorf("image %s not found", image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace
|
||||
func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error {
|
||||
kcl.lock.Lock()
|
||||
defer kcl.lock.Unlock()
|
||||
kcl.mu.Lock()
|
||||
defer kcl.mu.Unlock()
|
||||
|
||||
policies, err := kcl.GetNamespaceAccessPolicies()
|
||||
if err != nil {
|
||||
@@ -42,6 +42,7 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -40,7 +39,6 @@ func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConf
|
||||
k := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
config := &ktypes.ConfigMap{
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -29,16 +28,17 @@ type (
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
instanceID string
|
||||
endpointClients cmap.ConcurrentMap
|
||||
endpointClients map[string]*KubeClient
|
||||
endpointProxyClients *cache.Cache
|
||||
AddrHTTPS string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// KubeClient represent a service used to execute Kubernetes operations
|
||||
KubeClient struct {
|
||||
cli kubernetes.Interface
|
||||
instanceID string
|
||||
lock *sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
|
||||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
instanceID: instanceID,
|
||||
endpointClients: cmap.New(),
|
||||
endpointClients: make(map[string]*KubeClient),
|
||||
endpointProxyClients: cache.New(timeout, timeout),
|
||||
AddrHTTPS: addrHTTPS,
|
||||
}, nil
|
||||
@@ -69,50 +69,58 @@ func (factory *ClientFactory) GetInstanceID() (instanceID string) {
|
||||
|
||||
// Remove the cached kube client so a new one can be created
|
||||
func (factory *ClientFactory) RemoveKubeClient(endpointID portainer.EndpointID) {
|
||||
factory.endpointClients.Remove(strconv.Itoa(int(endpointID)))
|
||||
factory.mu.Lock()
|
||||
delete(factory.endpointClients, strconv.Itoa(int(endpointID)))
|
||||
factory.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetKubeClient checks if an existing client is already registered for the environment(endpoint) and returns it if one is found.
|
||||
// If no client is registered, it will create a new client, register it, and returns it.
|
||||
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
||||
func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||
factory.mu.Lock()
|
||||
defer factory.mu.Unlock()
|
||||
|
||||
key := strconv.Itoa(int(endpoint.ID))
|
||||
client, ok := factory.endpointClients.Get(key)
|
||||
client, ok := factory.endpointClients[key]
|
||||
if !ok {
|
||||
client, err := factory.createCachedAdminKubeClient(endpoint)
|
||||
var err error
|
||||
|
||||
client, err = factory.createCachedAdminKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factory.endpointClients.Set(key, client)
|
||||
return client, nil
|
||||
factory.endpointClients[key] = client
|
||||
}
|
||||
|
||||
return client.(portainer.KubeClient), nil
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetProxyKubeClient retrieves a KubeClient from the cache. You should be
|
||||
// calling SetProxyKubeClient before first. It is normally, called the
|
||||
// kubernetes middleware.
|
||||
func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (portainer.KubeClient, bool) {
|
||||
func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (*KubeClient, bool) {
|
||||
client, ok := factory.endpointProxyClients.Get(endpointID + "." + token)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return client.(portainer.KubeClient), true
|
||||
|
||||
return client.(*KubeClient), true
|
||||
}
|
||||
|
||||
// SetProxyKubeClient stores a kubeclient in the cache.
|
||||
func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli portainer.KubeClient) {
|
||||
func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli *KubeClient) {
|
||||
factory.endpointProxyClients.Set(endpointID+"."+token, cli, 0)
|
||||
}
|
||||
|
||||
// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and
|
||||
// Kubernetes config.
|
||||
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (portainer.KubeClient, error) {
|
||||
func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (*KubeClient, error) {
|
||||
config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliConfig, err := config.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -126,28 +134,22 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubecli := &KubeClient{
|
||||
return &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) {
|
||||
func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (*KubeClient, error) {
|
||||
cli, err := factory.CreateClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubecli := &KubeClient{
|
||||
return &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateClient returns a pointer to a new Clientset instance
|
||||
|
||||
@@ -3,7 +3,6 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -19,7 +18,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
@@ -37,12 +35,10 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
assert.Error(t, err)
|
||||
|
||||
})
|
||||
|
||||
t.Run("if called with the same state, should skip (exit without error)", func(t *testing.T) {
|
||||
@@ -61,7 +57,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
systemNamespaceLabel: strconv.FormatBool(test.isSystem),
|
||||
}}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, test.isSystem)
|
||||
@@ -81,7 +76,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
@@ -102,7 +96,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, false)
|
||||
@@ -125,7 +118,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
systemNamespaceLabel: "true",
|
||||
}}}),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, false)
|
||||
@@ -159,7 +151,6 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(namespace, config),
|
||||
instanceID: "instance",
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
|
||||
err := kcl.ToggleSystemState(nsName, true)
|
||||
@@ -178,6 +169,5 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
actualPolicies, err := kcl.GetNamespaceAccessPolicies()
|
||||
assert.NoError(t, err, "failed to fetch policies")
|
||||
assert.Equal(t, expectedPolicies, actualPolicies)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port
|
||||
|
||||
// When the api call is internal, the baseURL should not be used.
|
||||
if hostURL == "localhost" {
|
||||
hostURL = hostURL + service.httpsBindAddr
|
||||
hostURL += service.httpsBindAddr
|
||||
baseURL = "/"
|
||||
}
|
||||
|
||||
|
||||
@@ -121,12 +121,13 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
||||
|
||||
client := &http.Client{}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -41,10 +41,12 @@ func DetermineContainerPlatform() (ContainerPlatform, error) {
|
||||
if podmanModeEnvVar == "1" {
|
||||
return PlatformPodman, nil
|
||||
}
|
||||
|
||||
serviceHostKubernetesEnvVar := os.Getenv(KubernetesServiceHost)
|
||||
if serviceHostKubernetesEnvVar != "" {
|
||||
return PlatformKubernetes, nil
|
||||
}
|
||||
|
||||
nomadJobName := os.Getenv(NomadJobName)
|
||||
if nomadJobName != "" {
|
||||
return PlatformNomad, nil
|
||||
|
||||
@@ -181,6 +181,9 @@ type (
|
||||
Type StackType `json:"Type" example:"1"`
|
||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||
Variables []CustomTemplateVariableDefinition
|
||||
GitConfig *gittypes.RepoConfig `json:"GitConfig"`
|
||||
// IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file
|
||||
IsComposeFormat bool `example:"false"`
|
||||
}
|
||||
|
||||
// CustomTemplateID represents a custom template identifier
|
||||
@@ -391,8 +394,6 @@ type (
|
||||
// Heartbeat indicates the heartbeat status of an edge environment
|
||||
Heartbeat bool `json:"Heartbeat" example:"true"`
|
||||
|
||||
// IsEdgeDevice marks if the environment was created as an EdgeDevice
|
||||
IsEdgeDevice bool
|
||||
// Whether the device has been trusted or not by the user
|
||||
UserTrusted bool
|
||||
|
||||
@@ -420,6 +421,9 @@ type (
|
||||
|
||||
// Deprecated in DBVersion == 22
|
||||
Tags []string `json:"Tags"`
|
||||
|
||||
// Deprecated v2.18
|
||||
IsEdgeDevice bool
|
||||
}
|
||||
|
||||
EnvironmentEdgeSettings struct {
|
||||
@@ -1514,7 +1518,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.18.2"
|
||||
APIVersion = "2.19.0"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1561,8 +1565,14 @@ const (
|
||||
)
|
||||
|
||||
// List of supported features
|
||||
const (
|
||||
FeatureFdo = "fdo"
|
||||
FeatureNoTx = "noTx"
|
||||
)
|
||||
|
||||
var SupportedFeatureFlags = []featureflags.Feature{
|
||||
"fdo",
|
||||
FeatureFdo,
|
||||
FeatureNoTx,
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -53,6 +53,7 @@ func (b *K8sStackFileContentBuilder) SetUniqueInfo(payload *StackPayload) FileCo
|
||||
b.stack.Namespace = payload.Namespace
|
||||
b.stack.CreatedBy = b.User.Username
|
||||
b.stack.IsComposeFormat = payload.ComposeFormat
|
||||
b.stack.FromAppTemplate = payload.FromAppTemplate
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stackbuilders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -82,7 +83,12 @@ func (b *GitMethodStackBuilder) SetGitRepository(payload *StackPayload) GitMetho
|
||||
// Set the project path on the disk
|
||||
b.stack.ProjectPath = b.fileService.GetStackProjectPath(stackFolder)
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(b.stack.ID, repoConfig, b.gitService, b.fileService)
|
||||
getProjectPath := func() string {
|
||||
stackFolder := fmt.Sprintf("%d", b.stack.ID)
|
||||
return b.fileService.GetStackProjectPath(stackFolder)
|
||||
}
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(repoConfig, b.gitService, getProjectPath)
|
||||
if err != nil {
|
||||
b.err = httperror.InternalServerError(err.Error(), err)
|
||||
return b
|
||||
|
||||
@@ -16,7 +16,7 @@ var (
|
||||
|
||||
// DownloadGitRepository downloads the target git repository on the disk
|
||||
// The first return value represents the commit hash of the downloaded git repository
|
||||
func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig, gitService portainer.GitService, fileService portainer.FileService) (string, error) {
|
||||
func DownloadGitRepository(config gittypes.RepoConfig, gitService portainer.GitService, getProjectPath func() string) (string, error) {
|
||||
username := ""
|
||||
password := ""
|
||||
if config.Authentication != nil {
|
||||
@@ -24,9 +24,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
|
||||
password = config.Authentication.Password
|
||||
}
|
||||
|
||||
stackFolder := fmt.Sprintf("%d", stackID)
|
||||
projectPath := fileService.GetStackProjectPath(stackFolder)
|
||||
|
||||
projectPath := getProjectPath()
|
||||
err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password, config.TLSSkipVerify)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
|
||||
@@ -28,6 +28,7 @@ func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
|
||||
for _, file := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
filePaths = append(filePaths, filesystem.JoinPaths(stack.ProjectPath, file))
|
||||
}
|
||||
|
||||
return filePaths
|
||||
}
|
||||
|
||||
|
||||
@@ -28,3 +28,5 @@ export const PREDEFINED_NETWORKS = ['host', 'bridge', 'ingress', 'nat', 'none'];
|
||||
export const PORTAINER_FADEOUT = 1500;
|
||||
export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$';
|
||||
export const KUBE_TEMPLATE_NAME_VALIDATION_REGEX =
|
||||
'^(([a-z0-9](?:(?:[-a-z0-9_.]){0,61}[a-z0-9])?))$'; // alphanumeric, lowercase, can contain dashes, dots and underscores, max 63 characters
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { editor, upload, git } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
|
||||
class KubeCreateCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
||||
|
||||
this.methodOptions = [editor, upload];
|
||||
this.methodOptions = [editor, upload, git];
|
||||
|
||||
this.templates = null;
|
||||
this.isTemplateVariablesEnabled = isBE;
|
||||
@@ -20,6 +21,7 @@ class KubeCreateCustomTemplateViewController {
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
isTemplateValid: true,
|
||||
templateNameRegex: KUBE_TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
@@ -31,6 +33,13 @@ class KubeCreateCustomTemplateViewController {
|
||||
Logo: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
Variables: [],
|
||||
RepositoryURL: '',
|
||||
RepositoryURLValid: false,
|
||||
RepositoryReferenceName: 'refs/heads/main',
|
||||
RepositoryAuthentication: false,
|
||||
RepositoryUsername: '',
|
||||
RepositoryPassword: '',
|
||||
ComposeFilePathInRepository: 'manifest.yml',
|
||||
};
|
||||
|
||||
this.onChangeFile = this.onChangeFile.bind(this);
|
||||
@@ -121,6 +130,8 @@ class KubeCreateCustomTemplateViewController {
|
||||
return this.createCustomTemplateFromFileContent(template);
|
||||
case 'upload':
|
||||
return this.createCustomTemplateFromFileUpload(template);
|
||||
case 'repository':
|
||||
return this.createCustomTemplateFromGitRepository(template);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +143,10 @@ class KubeCreateCustomTemplateViewController {
|
||||
return this.CustomTemplateService.createCustomTemplateFromFileUpload(template);
|
||||
}
|
||||
|
||||
createCustomTemplateFromGitRepository(template) {
|
||||
return this.CustomTemplateService.createCustomTemplateFromGitRepository(template);
|
||||
}
|
||||
|
||||
validateForm(method) {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
<custom-template-common-fields
|
||||
form-values="$ctrl.formValues"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
name-regex-error="'This field must consist of lower-case alphanumeric characters, \'.\', \'_\' or \'-\', must start and end with an alphanumeric character and must be 63 characters or less (e.g. \'my-name\', or \'abc-123\').'"
|
||||
></custom-template-common-fields>
|
||||
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title"> Build method </div>
|
||||
@@ -35,6 +39,8 @@
|
||||
<file-upload-description> You can upload a Manifest file from your computer. </file-upload-description>
|
||||
</file-upload-form>
|
||||
|
||||
<git-form deploy-method="kubernetes" ng-if="$ctrl.state.method === 'repository'" value="$ctrl.formValues" on-change="($ctrl.handleChange)"></git-form>
|
||||
|
||||
<custom-templates-variables-definition-field
|
||||
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||
value="$ctrl.formValues.Variables"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||
import { getFilePreview } from '@/react/portainer/gitops/gitops.service';
|
||||
import { KUBE_TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
|
||||
class KubeEditCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
@@ -11,11 +13,19 @@ class KubeEditCustomTemplateViewController {
|
||||
|
||||
this.isTemplateVariablesEnabled = isBE;
|
||||
|
||||
this.formValues = null;
|
||||
this.formValues = {
|
||||
Variables: [],
|
||||
TLSSkipVerify: false,
|
||||
};
|
||||
this.state = {
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
isTemplateValid: true,
|
||||
isEditorReadOnly: false,
|
||||
templateLoadFailed: false,
|
||||
templatePreviewFailed: false,
|
||||
templatePreviewError: '',
|
||||
templateNameRegex: KUBE_TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
};
|
||||
this.templates = [];
|
||||
|
||||
@@ -25,6 +35,7 @@ class KubeEditCustomTemplateViewController {
|
||||
this.onBeforeUnload = this.onBeforeUnload.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.bind(this);
|
||||
this.previewFileFromGitRepository = this.previewFileFromGitRepository.bind(this);
|
||||
}
|
||||
|
||||
getTemplate() {
|
||||
@@ -32,13 +43,25 @@ class KubeEditCustomTemplateViewController {
|
||||
try {
|
||||
const { id } = this.$state.params;
|
||||
|
||||
const [template, file] = await Promise.all([this.CustomTemplateService.customTemplate(id), this.CustomTemplateService.customTemplateFile(id)]);
|
||||
template.FileContent = file;
|
||||
const template = await this.CustomTemplateService.customTemplate(id);
|
||||
|
||||
if (template.GitConfig !== null) {
|
||||
this.state.isEditorReadOnly = true;
|
||||
}
|
||||
|
||||
try {
|
||||
template.FileContent = await this.CustomTemplateService.customTemplateFile(id, template.GitConfig !== null);
|
||||
} catch (err) {
|
||||
this.state.templateLoadFailed = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template.Variables = template.Variables || [];
|
||||
|
||||
this.formValues = template;
|
||||
this.formValues = { ...this.formValues, ...template };
|
||||
|
||||
this.parseTemplate(file);
|
||||
this.parseTemplate(template.FileContent);
|
||||
this.parseGitConfig(template.GitConfig);
|
||||
|
||||
this.oldFileContent = this.formValues.FileContent;
|
||||
|
||||
@@ -79,6 +102,62 @@ class KubeEditCustomTemplateViewController {
|
||||
}
|
||||
}
|
||||
|
||||
parseGitConfig(config) {
|
||||
if (config === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let flatConfig = {
|
||||
RepositoryURL: config.URL,
|
||||
RepositoryReferenceName: config.ReferenceName,
|
||||
ComposeFilePathInRepository: config.ConfigFilePath,
|
||||
RepositoryAuthentication: config.Authentication !== null,
|
||||
TLSSkipVerify: config.TLSSkipVerify,
|
||||
};
|
||||
|
||||
if (config.Authentication) {
|
||||
flatConfig = {
|
||||
...flatConfig,
|
||||
RepositoryUsername: config.Authentication.Username,
|
||||
RepositoryPassword: config.Authentication.Password,
|
||||
};
|
||||
}
|
||||
|
||||
this.formValues = { ...this.formValues, ...flatConfig };
|
||||
}
|
||||
|
||||
previewFileFromGitRepository() {
|
||||
this.state.templatePreviewFailed = false;
|
||||
this.state.templatePreviewError = '';
|
||||
|
||||
let creds = {};
|
||||
if (this.formValues.RepositoryAuthentication) {
|
||||
creds = {
|
||||
username: this.formValues.RepositoryUsername,
|
||||
password: this.formValues.RepositoryPassword,
|
||||
};
|
||||
}
|
||||
const payload = {
|
||||
repository: this.formValues.RepositoryURL,
|
||||
targetFile: this.formValues.ComposeFilePathInRepository,
|
||||
tlsSkipVerify: this.formValues.TLSSkipVerify,
|
||||
...creds,
|
||||
};
|
||||
|
||||
this.$async(async () => {
|
||||
try {
|
||||
this.formValues.FileContent = await getFilePreview(payload);
|
||||
this.state.isEditorDirty = true;
|
||||
|
||||
// check if the template contains mustache template symbol
|
||||
this.parseTemplate(this.formValues.FileContent);
|
||||
} catch (err) {
|
||||
this.state.templatePreviewError = err.message;
|
||||
this.state.templatePreviewFailed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
|
||||
@@ -5,7 +5,27 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="$ctrl.form">
|
||||
<custom-template-common-fields form-values="$ctrl.formValues"></custom-template-common-fields>
|
||||
<custom-template-common-fields
|
||||
form-values="$ctrl.formValues"
|
||||
name-regex="$ctrl.state.templateNameRegex"
|
||||
name-regex-error="'This field must consist of lower-case alphanumeric characters, \'.\', \'_\' or \'-\', must start and end with an alphanumeric character and must be 63 characters or less (e.g. \'my-name\', or \'abc-123\').'"
|
||||
></custom-template-common-fields>
|
||||
|
||||
<git-form value="$ctrl.formValues" on-change="($ctrl.handleChange)" ng-if="$ctrl.formValues.GitConfig"></git-form>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12"
|
||||
><button type="button" class="btn btn-sm btn-light !ml-0" ng-if="$ctrl.formValues.GitConfig" ng-click="$ctrl.previewFileFromGitRepository()">
|
||||
<pr-icon icon="'refresh-cw'" feather="true"></pr-icon>Reload custom template</button
|
||||
>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="$ctrl.state.templatePreviewFailed">
|
||||
<p class="small vertical-center text-danger mt-5">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>
|
||||
Custom template could not be loaded, {{ $ctrl.state.templatePreviewError }}.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<web-editor-form
|
||||
identifier="template-editor"
|
||||
@@ -14,6 +34,7 @@
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
read-only="$ctrl.state.isEditorReadOnly"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
@@ -31,7 +52,11 @@
|
||||
is-variables-names-from-parent="true"
|
||||
></custom-templates-variables-definition-field>
|
||||
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
|
||||
<por-access-control-form
|
||||
form-data="$ctrl.formValues.AccessControlData"
|
||||
resource-control="$ctrl.formValues.ResourceControl"
|
||||
ng-if="$ctrl.formValues.AccessControlData"
|
||||
></por-access-control-form>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -773,7 +773,7 @@
|
||||
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
|
||||
"
|
||||
>
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
||||
<label for="memory-limit" class="space-right col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
||||
Memory limit (MB)
|
||||
<portainer-tooltip
|
||||
message="'An instance of this application will reserve this amount of memory. If the instance memory usage exceeds the reservation, it might be subject to OOM.'"
|
||||
@@ -816,7 +816,7 @@
|
||||
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
|
||||
"
|
||||
>
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
||||
<label for="cpu-limit" class="space-right col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
||||
CPU limit
|
||||
<portainer-tooltip
|
||||
message="'An instance of this application will reserve this amount of CPU. If the instance CPU usage exceeds the reservation, it might be subject to CPU throttling.'"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user