Compare commits
19 Commits
fix/EE-305
...
fix/EE-523
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a33ad268 | ||
|
|
0ca56ddbb1 | ||
|
|
3a30c8ed1e | ||
|
|
151db6bfe7 | ||
|
|
106c719a34 | ||
|
|
1cfd031db1 | ||
|
|
fbc1a2d44d | ||
|
|
47478efd1e | ||
|
|
50940b7fba | ||
|
|
7468d5637b | ||
|
|
6edc210ae7 | ||
|
|
f859876cb6 | ||
|
|
5e434a82ed | ||
|
|
d9f6471a00 | ||
|
|
a7d1a20dfb | ||
|
|
17517d7521 | ||
|
|
c609f6912f | ||
|
|
346fe9e3f1 | ||
|
|
69f14e569b |
147
.github/workflows/nightly-security-scan.yml
vendored
147
.github/workflows/nightly-security-scan.yml
vendored
@@ -2,22 +2,21 @@ name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 20 * * *'
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -25,49 +24,46 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload js result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: analyse vulnerabilities
|
||||
- name: Analyse the js result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "js_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=js_result::${result}"
|
||||
|
||||
server-dependencies:
|
||||
name: Server Dependency Check
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: '1.19.4'
|
||||
|
||||
- name: download Go modules
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
@@ -75,93 +71,122 @@ jobs:
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: upload scan result as develop artifact
|
||||
- name: Upload go security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: analyse vulnerabilities
|
||||
- name: Analyse the go result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "go_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=go_result::${result}"
|
||||
|
||||
image-vulnerability:
|
||||
name: Image Vulnerability Check
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
steps:
|
||||
- name: scan vulnerabilities by Trivy
|
||||
- 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 and build
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
|
||||
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-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: analyse vulnerabilities
|
||||
- name: Analyse the trivy result
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:pr5 summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||
echo "${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
||||
echo "::set-output name=image_result::${result}"
|
||||
|
||||
result-analysis:
|
||||
name: Analyse Scan Results
|
||||
name: Analyse scan result
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/fix/EE-3059/security-scan-debug'
|
||||
github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||
steps:
|
||||
- name: display the results of js, Go, and image scan
|
||||
- name: Display the results of js, go and image
|
||||
run: |
|
||||
echo "${{ matrix.js.status }}"
|
||||
echo "${{ matrix.go.status }}"
|
||||
echo "${{ matrix.image.status }}"
|
||||
echo "${{ matrix.js.summary }}"
|
||||
echo "${{ matrix.go.summary }}"
|
||||
echo "${{ matrix.image.summary }}"
|
||||
echo ${{ matrix.js.status }}
|
||||
echo ${{ matrix.go.status }}
|
||||
echo ${{ matrix.image.status }}
|
||||
echo ${{ matrix.js.summary }}
|
||||
echo ${{ matrix.go.summary }}
|
||||
echo ${{ matrix.image.summary }}
|
||||
|
||||
- name: send message to Slack
|
||||
- name: Send Slack message
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
uses: slackapi/slack-github-action@v1.18.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
|
||||
115
.github/workflows/pr-security.yml
vendored
115
.github/workflows/pr-security.yml
vendored
@@ -12,11 +12,10 @@ on:
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
- '.github/workflows/pr-security.yml'
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client Dependency Check
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -24,10 +23,9 @@ jobs:
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -35,13 +33,13 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: upload scan result as pull-request artifact
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-feat-result
|
||||
path: snyk.json
|
||||
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -53,24 +51,24 @@ jobs:
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload js result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-compare-to-develop-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
- name: Analyse the js diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
|
||||
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=js_diff_result::${result}"
|
||||
|
||||
server-dependencies:
|
||||
name: Server Dependency Check
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -78,18 +76,16 @@ jobs:
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: '1.19.4'
|
||||
|
||||
- name: download Go modules
|
||||
- name: Download go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: scan vulnerabilities by Snyk
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
@@ -97,13 +93,13 @@ jobs:
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: upload scan result as pull-request artifact
|
||||
- name: Upload go security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-feature-result
|
||||
path: snyk.json
|
||||
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -115,24 +111,24 @@ jobs:
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload go result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-compare-to-develop-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
- name: Analyse the go diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
|
||||
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=go_diff_result::${result}"
|
||||
|
||||
image-vulnerability:
|
||||
name: Image Vulnerability Check
|
||||
name: Build docker image and Image vulnerability check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -140,50 +136,50 @@ jobs:
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
- name: checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: install Go 1.19.5
|
||||
- name: Use golang 1.19.4
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
go-version: '1.19.4'
|
||||
|
||||
- name: install Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: install packages and build binary
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn 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 compress image
|
||||
uses: docker/build-push-action@v4
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: load docker image
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: scan vulnerabilities by Trivy
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: upload image security scan result as artifact
|
||||
- name: Upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-feature-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
- name: Download artifacts from develop branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -195,24 +191,24 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
|
||||
- name: upload html file as artifact
|
||||
- name: Upload image result html file
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
- name: Analyse the image diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
|
||||
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=image_diff_result::${result}"
|
||||
|
||||
result-analysis:
|
||||
name: Analyse Scan Result Against develop Branch
|
||||
name: Analyse scan result compared to develop
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
@@ -224,7 +220,8 @@ jobs:
|
||||
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' ||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -43,9 +42,7 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
|
||||
@@ -3,10 +3,8 @@ package backup
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -45,12 +43,6 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
||||
return errors.Wrap(err, "Failed to stop db")
|
||||
}
|
||||
|
||||
// At some point, backups were created containing a subdirectory, now we need to handle both
|
||||
restorePath, err = getRestoreSourcePath(restorePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
|
||||
}
|
||||
|
||||
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||
return errors.Wrap(err, "failed to restore the system state")
|
||||
}
|
||||
@@ -67,26 +59,6 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
|
||||
return archive.ExtractTarGz(r, destinationDirPath)
|
||||
}
|
||||
|
||||
func getRestoreSourcePath(dir string) (string, error) {
|
||||
// find portainer.db or portainer.edb file. Return the parent directory
|
||||
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
|
||||
|
||||
backupDirPath := dir
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if portainerdbRegex.MatchString(d.Name()) {
|
||||
backupDirPath = filepath.Dir(path)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return backupDirPath, err
|
||||
}
|
||||
|
||||
func restoreFiles(srcDir string, destinationDir string) error {
|
||||
for _, filename := range filesToRestore {
|
||||
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||
|
||||
@@ -72,7 +72,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||
}
|
||||
|
||||
@@ -81,6 +80,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
// ValidateFlags validates the values of the flags.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
|
||||
displayDeprecationWarnings(flags)
|
||||
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
@@ -111,38 +111,31 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
if endpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
if endpointURL != "" {
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval == "" {
|
||||
return nil
|
||||
if snapshotInterval != "" {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ 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.ReplaceAll(answer, "\n", "")
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
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, kubernetesClientFactory)
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
@@ -114,6 +115,9 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -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, object)
|
||||
is.Equal(test.expected, string(object))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, i
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
return bucket.Put(tx.conn.ConvertToKey(int(id)), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
|
||||
@@ -9,7 +9,8 @@ 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) {
|
||||
if storeType == "boltdb" {
|
||||
switch storeType {
|
||||
case "boltdb":
|
||||
return &boltdb.DbConnection{
|
||||
Path: storePath,
|
||||
EncryptionKey: encryptionKey,
|
||||
|
||||
@@ -159,11 +159,6 @@ 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,6 +1,7 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -100,16 +101,9 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: use UpdateEdgeStack inside a transaction instead.
|
||||
// UpdateEdgeStackFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
edgeStack, err := service.EdgeStack(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFunc(edgeStack)
|
||||
|
||||
return service.UpdateEdgeStack(ID, edgeStack)
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
|
||||
@@ -14,21 +14,16 @@ 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
|
||||
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
connection portainer.Connection
|
||||
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func (service *Service) RegisterUpdateStackFunction(
|
||||
updateFunc func(portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
) {
|
||||
func (service *Service) RegisterUpdateStackFunction(updateFunc func(ID portainer.EdgeStackID, updateFunc func(edgeStack *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.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
service.service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "registries"
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "registries"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -32,14 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Registry returns a registry by ID.
|
||||
// Registry returns an registry by ID.
|
||||
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
@@ -74,7 +69,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
|
||||
return registries, err
|
||||
}
|
||||
|
||||
// Create creates a new registry.
|
||||
// CreateRegistry creates a new registry.
|
||||
func (service *Service) Create(registry *portainer.Registry) error {
|
||||
return service.connection.CreateObject(
|
||||
BucketName,
|
||||
@@ -85,13 +80,13 @@ func (service *Service) Create(registry *portainer.Registry) error {
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRegistry updates a registry.
|
||||
// UpdateRegistry updates an registry.
|
||||
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, registry)
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes a registry.
|
||||
// DeleteRegistry deletes an registry.
|
||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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,16 +48,13 @@ 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 err
|
||||
return errors.Wrap(err, "failed to migrate database")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -8,7 +8,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -18,7 +17,11 @@ 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,
|
||||
@@ -41,10 +44,9 @@ 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 {
|
||||
if endpoints[i].PostInitMigrations.MigrateIngresses == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,11 +67,10 @@ 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 {
|
||||
if environments[i].PostInitMigrations.MigrateGPUs == false {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,13 +102,11 @@ 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, edgeStackService.UpdateEdgeStackFuncTx)
|
||||
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc)
|
||||
|
||||
edgeGroupService, err := edgegroup.NewService(store.connection)
|
||||
if err != nil {
|
||||
|
||||
@@ -42,15 +42,11 @@ 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)
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"EdgeKey": "",
|
||||
"Extensions": [],
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"Name": "local",
|
||||
"PublicURL": "",
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"EnableGPUManagement": false,
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
"Kubernetes": {
|
||||
@@ -945,6 +944,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -38,19 +38,17 @@ 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) {
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, errUnsupportedEnvironmentType
|
||||
case portainer.AgentOnDockerEnvironment:
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
|
||||
case portainer.EdgeAgentOnDockerEnvironment:
|
||||
} else if endpoint.Type == 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
err = manager.deployer.Deploy(ctx, filePaths, libstack.DeployOptions{
|
||||
Options: libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
@@ -106,7 +106,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
err = manager.deployer.Pull(ctx, filePaths, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
|
||||
@@ -59,7 +59,6 @@ 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)
|
||||
@@ -76,7 +75,6 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,15 +84,13 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "logout")
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -105,7 +101,6 @@ 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")
|
||||
}
|
||||
@@ -117,7 +112,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -127,9 +121,7 @@ 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, "")
|
||||
}
|
||||
|
||||
@@ -206,7 +198,6 @@ 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
|
||||
@@ -239,6 +230,5 @@ func configureFilePaths(args []string, filePaths []string) []string {
|
||||
for _, path := range filePaths {
|
||||
args = append(args, "--compose-file", path)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/client"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -49,22 +51,21 @@ type azureItem struct {
|
||||
}
|
||||
|
||||
type azureClient struct {
|
||||
client *http.Client
|
||||
baseUrl string
|
||||
}
|
||||
|
||||
func NewAzureClient() *azureClient {
|
||||
httpsCli := newHttpClientForAzure()
|
||||
return &azureClient{
|
||||
client: httpsCli,
|
||||
baseUrl: "https://dev.azure.com",
|
||||
}
|
||||
}
|
||||
|
||||
func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
|
||||
func newHttpClientForAzure() *http.Client {
|
||||
tlsConfig := crypto.CreateTLSConfiguration()
|
||||
|
||||
if insecureSkipVerify {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
httpsCli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
@@ -73,6 +74,7 @@ func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
|
||||
Timeout: 300 * time.Second,
|
||||
}
|
||||
|
||||
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
||||
return httpsCli
|
||||
}
|
||||
|
||||
@@ -96,17 +98,14 @@ 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")
|
||||
}
|
||||
|
||||
defer zipFile.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
|
||||
@@ -120,14 +119,10 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
return "", errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
res, err := client.Do(req)
|
||||
res, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
@@ -138,7 +133,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -147,7 +141,6 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return rootItem.CommitId, nil
|
||||
}
|
||||
|
||||
@@ -173,10 +166,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
@@ -197,7 +187,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -216,7 +205,7 @@ func parseUrl(rawUrl string) (*azureOptions, error) {
|
||||
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
|
||||
}
|
||||
|
||||
const expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
||||
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
|
||||
|
||||
func parseSshUrl(rawUrl string) (*azureOptions, error) {
|
||||
path := strings.Split(rawUrl, "/")
|
||||
@@ -354,7 +343,6 @@ 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")
|
||||
@@ -373,11 +361,9 @@ 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
|
||||
}
|
||||
|
||||
@@ -385,11 +371,9 @@ func getVersionType(name string) string {
|
||||
if strings.HasPrefix(name, branchPrefix) {
|
||||
return "branch"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, tagPrefix) {
|
||||
return "tag"
|
||||
}
|
||||
|
||||
return "commit"
|
||||
}
|
||||
|
||||
@@ -415,10 +399,7 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
@@ -475,10 +456,7 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
@@ -512,6 +490,5 @@ func checkAzureStatusCode(err error, code int) error {
|
||||
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
var (
|
||||
privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
|
||||
)
|
||||
|
||||
func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
@@ -58,7 +59,7 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
|
||||
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false)
|
||||
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
})
|
||||
@@ -73,7 +74,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
|
||||
|
||||
dst := t.TempDir()
|
||||
|
||||
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false)
|
||||
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
}
|
||||
@@ -84,7 +85,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
|
||||
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
|
||||
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
|
||||
}
|
||||
@@ -96,7 +97,7 @@ func TestService_ListRefs_Azure(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
}
|
||||
@@ -106,10 +107,10 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -247,7 +248,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions)
|
||||
if tt.expect.shouldFail {
|
||||
assert.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
@@ -268,10 +269,10 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 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)
|
||||
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
@@ -292,6 +292,7 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
a := &azureClient{
|
||||
client: server.Client(),
|
||||
baseUrl: server.URL,
|
||||
}
|
||||
|
||||
@@ -328,6 +329,7 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
a := &azureClient{
|
||||
client: server.Client(),
|
||||
baseUrl: server.URL,
|
||||
}
|
||||
|
||||
@@ -440,7 +442,6 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args baseOption
|
||||
|
||||
@@ -20,8 +20,6 @@ type CloneOptions struct {
|
||||
ReferenceName string
|
||||
Username string
|
||||
Password string
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
|
||||
@@ -45,7 +43,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
|
||||
|
||||
cleanUp = true
|
||||
|
||||
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
|
||||
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password)
|
||||
if err != nil {
|
||||
cleanUp = false
|
||||
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
|
||||
|
||||
@@ -28,10 +28,9 @@ func NewGitClient(preserveGitDir bool) *gitClient {
|
||||
|
||||
func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) error {
|
||||
gitOptions := git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
}
|
||||
|
||||
if opt.referenceName != "" {
|
||||
@@ -61,8 +60,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
}
|
||||
|
||||
refs, err := remote.List(listOptions)
|
||||
@@ -112,8 +110,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
}
|
||||
|
||||
refs, err := rem.List(listOptions)
|
||||
@@ -135,13 +132,12 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
// listFiles list all filenames under the specific repository
|
||||
func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, error) {
|
||||
cloneOption := &git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(opt.referenceName),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
URL: opt.repositoryUrl,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(opt.referenceName),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
}
|
||||
|
||||
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken, false)
|
||||
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 0, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false)
|
||||
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func TestService_ListRefs_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 0, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
}
|
||||
@@ -60,11 +60,11 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions)
|
||||
if tt.expect.shouldFail {
|
||||
assert.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
@@ -224,10 +224,10 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||
repositoryUrl := privateGitRepoURL
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 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)
|
||||
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -240,8 +240,8 @@ func TestService_purgeCache_Github(t *testing.T) {
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
@@ -261,8 +261,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
|
||||
// 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
|
||||
service := newService(context.TODO(), 2, 40*timeout)
|
||||
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
@@ -293,12 +293,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 2, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
}
|
||||
@@ -311,26 +311,26 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 2, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{}, false)
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{})
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 2, service.repoFileCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false)
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", true)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
// The relevant file caches should be removed too
|
||||
@@ -344,12 +344,12 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
repositoryUrl := privateGitRepoURL
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
_, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{}, false)
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, service.repoFileCache.Len())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.NoDirExists(t, filepath.Join(dir, ".git"))
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) {
|
||||
repositoryURL := setup(t)
|
||||
referenceName := "refs/heads/main"
|
||||
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false)
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
|
||||
@@ -95,12 +95,10 @@ 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++
|
||||
@@ -109,7 +107,6 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,9 +10,9 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
repositoryCacheSize = 4
|
||||
repositoryCacheTTL = 5 * time.Minute
|
||||
var (
|
||||
REPOSITORY_CACHE_SIZE = 4
|
||||
REPOSITORY_CACHE_TTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
// baseOption provides a minimum group of information to operate a git repository, like git-remote
|
||||
@@ -21,7 +20,6 @@ type baseOption struct {
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
tlsSkipVerify bool
|
||||
}
|
||||
|
||||
// fetchOption allows to specify the reference name of the target repository
|
||||
@@ -60,7 +58,7 @@ type Service struct {
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(ctx context.Context) *Service {
|
||||
return newService(ctx, repositoryCacheSize, repositoryCacheTTL)
|
||||
return newService(ctx, REPOSITORY_CACHE_SIZE, REPOSITORY_CACHE_TTL)
|
||||
}
|
||||
|
||||
func newService(ctx context.Context, cacheSize int, cacheTTL time.Duration) *Service {
|
||||
@@ -121,14 +119,13 @@ func (service *Service) timerHasStopped() bool {
|
||||
|
||||
// CloneRepository clones a git repository using the specified URL in the specified
|
||||
// destination folder.
|
||||
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
|
||||
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
options := cloneOption{
|
||||
fetchOption: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
},
|
||||
@@ -147,13 +144,12 @@ func (service *Service) cloneRepository(destination string, options cloneOption)
|
||||
}
|
||||
|
||||
// LatestCommitID returns SHA1 of the latest commit of the specified reference
|
||||
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
|
||||
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
options := fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
}
|
||||
@@ -166,8 +162,8 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p
|
||||
}
|
||||
|
||||
// ListRefs will list target repository's references without cloning the repository
|
||||
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
|
||||
refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
|
||||
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
refCacheKey := generateCacheKey(repositoryURL, password)
|
||||
if service.cacheEnabled && hardRefresh {
|
||||
// Should remove the cache explicitly, so that the following normal list can show the correct result
|
||||
service.repoRefCache.Remove(refCacheKey)
|
||||
@@ -197,7 +193,6 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -224,8 +219,8 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
|
||||
|
||||
// ListFiles will list all the files of the target repository with specific extensions.
|
||||
// If extension is not provided, it will list all the files under the target repository
|
||||
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
|
||||
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify))
|
||||
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
repoKey := generateCacheKey(repositoryURL, referenceName)
|
||||
|
||||
if service.cacheEnabled && hardRefresh {
|
||||
// Should remove the cache explicitly, so that the following normal list can show the correct result
|
||||
@@ -251,7 +246,6 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package gittypes
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct")
|
||||
ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct")
|
||||
ErrIncorrectRepositoryURL = errors.New("Git repository could not be found, please ensure that the URL is correct.")
|
||||
ErrAuthenticationFailure = errors.New("Authentication failed, please ensure that the git credentials are correct.")
|
||||
)
|
||||
|
||||
// RepoConfig represents a configuration for a repo
|
||||
@@ -19,8 +19,6 @@ type RepoConfig struct {
|
||||
Authentication *GitAuthentication
|
||||
// Repository hash
|
||||
ConfigHash string `example:"bc4c183d756879ea4d173315338110b31004b8e0"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
type GitAuthentication struct {
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// UpdateGitObject updates a git object based on its config
|
||||
func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *gittypes.RepoConfig, forceUpdate bool, projectPath string) (bool, string, error) {
|
||||
func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.DataStore, objId string, gitConfig *gittypes.RepoConfig, autoUpdateConfig *portainer.AutoUpdateSettings, projectPath string) (bool, string, error) {
|
||||
if gitConfig == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -28,13 +29,13 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
|
||||
return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId)
|
||||
}
|
||||
|
||||
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password, gitConfig.TLSSkipVerify)
|
||||
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password)
|
||||
if err != nil {
|
||||
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
|
||||
}
|
||||
|
||||
hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
|
||||
|
||||
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
|
||||
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
||||
if !hashChanged && !forceUpdate {
|
||||
log.Debug().
|
||||
Str("hash", newHash).
|
||||
@@ -47,10 +48,9 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
|
||||
}
|
||||
|
||||
cloneParams := &cloneRepositoryParameters{
|
||||
url: gitConfig.URL,
|
||||
ref: gitConfig.ReferenceName,
|
||||
toDir: projectPath,
|
||||
tlsSkipVerify: gitConfig.TLSSkipVerify,
|
||||
url: gitConfig.URL,
|
||||
ref: gitConfig.ReferenceName,
|
||||
toDir: projectPath,
|
||||
}
|
||||
if gitConfig.Authentication != nil {
|
||||
cloneParams.auth = &gitAuth{
|
||||
@@ -78,8 +78,6 @@ type cloneRepositoryParameters struct {
|
||||
ref string
|
||||
toDir string
|
||||
auth *gitAuth
|
||||
// tlsSkipVerify skips SSL verification when cloning the Git repository
|
||||
tlsSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
type gitAuth struct {
|
||||
@@ -89,8 +87,8 @@ type gitAuth struct {
|
||||
|
||||
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
|
||||
if cloneParams.auth != nil {
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password, cloneParams.tlsSkipVerify)
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password)
|
||||
}
|
||||
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "", cloneParams.tlsSkipVerify)
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "")
|
||||
}
|
||||
|
||||
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.2
|
||||
github.com/Microsoft/go-winio v0.5.1
|
||||
github.com/VictoriaMetrics/fastcache v1.12.0
|
||||
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/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/cbroglie/mustache v1.4.0
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/docker/cli v20.10.12+incompatible
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.16+incompatible
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/fxamacker/cbor/v2 v2.3.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/gofrs/uuid v4.0.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.8.0
|
||||
github.com/gorilla/mux v1.7.3
|
||||
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.4.0
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
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-20230410231754-96474a42eacb
|
||||
github.com/portainer/portainer/pkg/libhelm v0.0.0-20221201012749-4fee35924724
|
||||
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-20220411215720-9780585627b5
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.26.1
|
||||
@@ -59,14 +59,13 @@ 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.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/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/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
|
||||
@@ -77,7 +76,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.5.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
@@ -124,10 +123,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.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/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/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,16 +33,14 @@ 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.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
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/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=
|
||||
@@ -59,29 +57,24 @@ 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-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/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/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=
|
||||
@@ -100,10 +93,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-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/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/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=
|
||||
@@ -138,8 +131,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.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-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-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=
|
||||
@@ -152,8 +145,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.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
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-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=
|
||||
@@ -174,8 +167,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.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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/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=
|
||||
@@ -223,7 +216,6 @@ 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=
|
||||
@@ -245,8 +237,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.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
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/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=
|
||||
@@ -267,8 +259,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.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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/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=
|
||||
@@ -340,8 +332,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 v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
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/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=
|
||||
@@ -358,8 +350,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-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/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/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=
|
||||
@@ -427,7 +419,6 @@ 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=
|
||||
@@ -496,15 +487,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.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
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/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-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
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/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=
|
||||
@@ -514,8 +505,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-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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/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=
|
||||
@@ -560,13 +551,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.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/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/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.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
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/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=
|
||||
@@ -574,8 +565,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.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/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,9 +113,7 @@ func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -134,9 +132,7 @@ func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -155,9 +151,7 @@ func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -188,9 +182,7 @@ func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
@@ -209,9 +201,7 @@ func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -235,9 +225,7 @@ func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
|
||||
@@ -33,13 +33,10 @@ 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,7 +128,6 @@ 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)
|
||||
@@ -138,8 +137,6 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certificate)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(block.Bytes), nil
|
||||
}
|
||||
|
||||
@@ -103,8 +103,6 @@ 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
|
||||
@@ -134,8 +132,6 @@ 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
|
||||
@@ -145,12 +141,10 @@ 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,7 +53,6 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
@@ -90,7 +89,6 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||
|
||||
response := w.Result()
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
|
||||
@@ -99,8 +99,6 @@ func backup(t *testing.T, h *Handler, password string) []byte {
|
||||
|
||||
response := w.Result()
|
||||
archive, _ := io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
return archive
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package customtemplates
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -17,7 +18,6 @@ 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,7 +135,6 @@ 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")
|
||||
}
|
||||
@@ -214,10 +213,6 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
// Definitions of variables in the stack file
|
||||
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 {
|
||||
@@ -237,11 +232,14 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
return errors.New("Invalid custom template platform")
|
||||
}
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack && payload.Type != portainer.KubernetesStack {
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
@@ -260,44 +258,35 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
|
||||
customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier()
|
||||
customTemplate := &portainer.CustomTemplate{
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
getProjectPath := func() string {
|
||||
return handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
|
||||
}
|
||||
projectPath := getProjectPath()
|
||||
projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID))
|
||||
customTemplate.ProjectPath = projectPath
|
||||
|
||||
gitConfig := &gittypes.RepoConfig{
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.ComposeFilePathInRepository,
|
||||
repositoryUsername := payload.RepositoryUsername
|
||||
repositoryPassword := payload.RepositoryPassword
|
||||
if !payload.RepositoryAuthentication {
|
||||
repositoryUsername = ""
|
||||
repositoryPassword = ""
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
gitConfig.Authentication = &gittypes.GitAuthentication{
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: payload.RepositoryPassword,
|
||||
}
|
||||
}
|
||||
|
||||
commitHash, err := stackutils.DownloadGitRepository(*gitConfig, handler.GitService, getProjectPath)
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
return nil, fmt.Errorf("invalid git credential")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitConfig.ConfigHash = commitHash
|
||||
customTemplate.GitConfig = gitConfig
|
||||
|
||||
isValidProject := true
|
||||
defer func() {
|
||||
if !isValidProject {
|
||||
@@ -307,7 +296,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
}
|
||||
}()
|
||||
|
||||
entryPath := filesystem.JoinPaths(projectPath, gitConfig.ConfigFilePath)
|
||||
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
|
||||
|
||||
exists, err := handler.FileService.FileExists(entryPath)
|
||||
if err != nil || !exists {
|
||||
@@ -319,9 +308,6 @@ 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")
|
||||
}
|
||||
|
||||
@@ -381,7 +367,6 @@ 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,11 +40,7 @@ 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)
|
||||
}
|
||||
|
||||
entryPath := customTemplate.EntryPoint
|
||||
if customTemplate.GitConfig != nil {
|
||||
entryPath = customTemplate.GitConfig.ConfigFilePath
|
||||
}
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, entryPath)
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.EntryPoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve custom template file from disk", err)
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
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,11 +10,8 @@ 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 {
|
||||
@@ -32,37 +29,18 @@ 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) && govalidator.IsNull(payload.RepositoryURL) {
|
||||
return errors.New("Either file content or git repository url need to be provided")
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return errors.New("Invalid file content")
|
||||
}
|
||||
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
|
||||
return errors.New("Invalid custom template platform")
|
||||
@@ -77,19 +55,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
|
||||
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
|
||||
return validateVariablesDefinitions(payload.Variables)
|
||||
}
|
||||
|
||||
// @id CustomTemplateUpdate
|
||||
@@ -149,6 +115,12 @@ 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
|
||||
@@ -156,42 +128,6 @@ 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,7 +2,6 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -14,20 +13,15 @@ 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
|
||||
gitFetchMutexs map[portainer.TemplateID]*sync.Mutex
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, fileService portainer.FileService, gitService portainer.GitService) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
DataStore: dataStore,
|
||||
FileService: fileService,
|
||||
GitService: gitService,
|
||||
gitFetchMutexs: make(map[portainer.TemplateID]*sync.Mutex),
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/custom_templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
|
||||
@@ -41,8 +35,6 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||
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,6 +6,7 @@ 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"
|
||||
@@ -25,15 +26,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -58,6 +56,7 @@ 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 {
|
||||
@@ -102,6 +101,13 @@ 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 txResponse(w, edgeGroup, err)
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
|
||||
@@ -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,15 +27,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -65,135 +62,128 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
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, 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"))
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
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 err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
|
||||
return httperror.InternalServerError("Unable to retrieve environment 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"))
|
||||
}
|
||||
if endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
}
|
||||
|
||||
edgeGroup.Name = payload.Name
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
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)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from database", err)
|
||||
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
|
||||
}
|
||||
|
||||
endpointGroups, err := tx.EndpointGroup().EndpointGroups()
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from database", err)
|
||||
return httperror.InternalServerError("Unable to get Environment from database", err)
|
||||
}
|
||||
|
||||
oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
edgeGroup.Dynamic = payload.Dynamic
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
var operation string
|
||||
if slices.Contains(newRelatedEndpoints, endpointID) {
|
||||
operation = "add"
|
||||
} else if slices.Contains(oldRelatedEndpoints, endpointID) {
|
||||
operation = "remove"
|
||||
} else {
|
||||
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
|
||||
continue
|
||||
}
|
||||
|
||||
if payload.PartialMatch != nil {
|
||||
edgeGroup.PartialMatch = *payload.PartialMatch
|
||||
}
|
||||
|
||||
err = tx.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
||||
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
|
||||
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) error {
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||
func (handler *Handler) updateEndpointStacks(endpointID portainer.EndpointID) error {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,7 +197,7 @@ func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoi
|
||||
|
||||
relation.EdgeStacks = edgeStackSet
|
||||
|
||||
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID, endpoint *portainer.Endpoint, edgeJobs []portainer.EdgeJob, operation string) error {
|
||||
|
||||
@@ -3,13 +3,11 @@ 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.
|
||||
@@ -36,15 +34,3 @@ 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,26 +7,16 @@ 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
|
||||
@@ -58,8 +48,12 @@ func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFileContentPayload struct {
|
||||
edgeJobBasePayload
|
||||
FileContent string
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
@@ -93,44 +87,32 @@ func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *h
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
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)
|
||||
edgeJob := handler.createEdgeJobObjectFromFileContentPayload(&payload)
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err = edge.GetEndpointsFromEdgeGroups(payload.EdgeGroups, tx)
|
||||
endpoints, err = edge.GetEndpointsFromEdgeGroups(payload.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.addAndPersistEdgeJob(tx, edgeJob, fileContent, endpoints)
|
||||
err = handler.addAndPersistEdgeJob(edgeJob, []byte(payload.FileContent), endpoints)
|
||||
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to schedule Edge job", err)
|
||||
return httperror.InternalServerError("Unable to schedule Edge job", err)
|
||||
}
|
||||
|
||||
return edgeJob, nil
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFilePayload struct {
|
||||
edgeJobBasePayload
|
||||
File []byte
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
File []byte
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
@@ -184,35 +166,66 @@ func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Req
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
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)
|
||||
edgeJob := handler.createEdgeJobObjectFromFilePayload(payload)
|
||||
|
||||
return err
|
||||
})
|
||||
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 txResponse(w, edgeJob, 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)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobObjectFromPayload(tx dataservices.DataStoreTx, payload *edgeJobBasePayload) *portainer.EdgeJob {
|
||||
return &portainer.EdgeJob{
|
||||
ID: portainer.EdgeJobID(tx.EdgeJob().GetNextIdentifier()),
|
||||
func (handler *Handler) createEdgeJobObjectFromFilePayload(payload *edgeJobCreateFromFilePayload) *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: convertEndpointsToMetaObject(payload.Endpoints),
|
||||
Endpoints: endpoints,
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Version: 1,
|
||||
GroupLogsCollection: map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{},
|
||||
}
|
||||
|
||||
return edgeJob
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJob *portainer.EdgeJob, file []byte, endpointsFromGroups []portainer.EndpointID) error {
|
||||
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 {
|
||||
edgeCronExpression := strings.Split(edgeJob.CronExpression, " ")
|
||||
if len(edgeCronExpression) == 6 {
|
||||
edgeCronExpression = edgeCronExpression[1:]
|
||||
@@ -220,7 +233,7 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
edgeJob.CronExpression = strings.Join(edgeCronExpression, " ")
|
||||
|
||||
for ID := range edgeJob.Endpoints {
|
||||
endpoint, err := tx.Endpoint().Endpoint(ID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,7 +254,7 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
endpointsMap = convertEndpointsToMetaObject(endpointsFromGroups)
|
||||
|
||||
for ID := range endpointsMap {
|
||||
endpoint, err := tx.Endpoint().Endpoint(ID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -261,7 +274,7 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
}
|
||||
|
||||
for endpointID := range endpointsMap {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -269,5 +282,5 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
}
|
||||
|
||||
return tx.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
return handler.DataStore.EdgeJob().Create(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -21,7 +19,7 @@ import (
|
||||
// @tags edge_jobs
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
@@ -33,34 +31,14 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid Edge job identifier route variable", 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(int(edgeJobID)))
|
||||
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(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")
|
||||
@@ -70,7 +48,7 @@ func (handler *Handler) deleteEdgeJob(tx dataservices.DataStoreTx, edgeJobID por
|
||||
|
||||
var endpointsMap map[portainer.EndpointID]portainer.EdgeJobEndpointMeta
|
||||
if len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
@@ -85,10 +63,10 @@ func (handler *Handler) deleteEdgeJob(tx dataservices.DataStoreTx, edgeJobID por
|
||||
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpointID, edgeJob.ID)
|
||||
}
|
||||
|
||||
err = tx.EdgeJob().DeleteEdgeJob(edgeJob.ID)
|
||||
err = handler.DataStore.EdgeJob().DeleteEdgeJob(edgeJob.ID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to remove the Edge job from the database", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type edgeJobFileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} edgeJobFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -21,7 +21,7 @@ type edgeJobInspectResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -8,10 +8,8 @@ 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
|
||||
@@ -21,8 +19,8 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
@@ -39,86 +37,53 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
|
||||
return httperror.BadRequest("Invalid Task identifier route variable", 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID)))
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clear log file from disk", err)
|
||||
}
|
||||
|
||||
endpointsFromGroups, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
endpointID := portainer.EndpointID(taskID)
|
||||
endpointsFromGroups, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
|
||||
err = updateEdgeJob(edgeJob, endpointID, endpointsFromGroups)
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist Edge job changes in the database", err)
|
||||
}
|
||||
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID)))
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clear log file from disk", err)
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.NotFound("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
|
||||
|
||||
return nil
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
@@ -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 tx.IsErrObjectNotFound(err) {
|
||||
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)
|
||||
|
||||
@@ -20,8 +20,8 @@ type fileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @success 200 {object} fileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -6,11 +6,10 @@ 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 {
|
||||
@@ -26,7 +25,7 @@ type taskContainer struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {array} taskContainer
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
@@ -38,34 +37,20 @@ func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid Edge job identifier route variable", 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)
|
||||
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 nil, httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
return 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, tx)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
|
||||
}
|
||||
|
||||
endpointsMap = convertEndpointsToMetaObject(endpoints)
|
||||
@@ -82,5 +67,5 @@ func listEdgeJobTasks(tx dataservices.DataStoreTx, edgeJobID portainer.EdgeJobID
|
||||
})
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
return response.JSON(w, tasks)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ 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"
|
||||
)
|
||||
@@ -31,7 +30,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -43,7 +41,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param body body edgeJobUpdatePayload true "EdgeGroup data"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
@@ -62,41 +60,27 @@ func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid request payload", 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
|
||||
})
|
||||
}
|
||||
|
||||
return txResponse(w, edgeJob, err)
|
||||
}
|
||||
|
||||
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)
|
||||
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 nil, httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
return httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.updateEdgeSchedule(tx, edgeJob, &payload)
|
||||
err = handler.updateEdgeSchedule(edgeJob, &payload)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update Edge job", err)
|
||||
return httperror.InternalServerError("Unable to update Edge job", err)
|
||||
}
|
||||
|
||||
err = tx.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist Edge job changes inside the database", err)
|
||||
return httperror.InternalServerError("Unable to persist Edge job changes inside the database", err)
|
||||
}
|
||||
|
||||
return edgeJob, nil
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
|
||||
func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
|
||||
if payload.Name != nil {
|
||||
edgeJob.Name = *payload.Name
|
||||
}
|
||||
@@ -115,7 +99,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
}
|
||||
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -136,7 +120,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
}
|
||||
|
||||
if len(payload.EdgeGroups) == 0 && len(edgeJob.EdgeGroups) > 0 {
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
|
||||
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, handler.DataStore)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -154,7 +138,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
|
||||
if len(payload.EdgeGroups) > 0 {
|
||||
for _, edgeGroupID := range payload.EdgeGroups {
|
||||
_, err := tx.EdgeGroup().EdgeGroup(edgeGroupID)
|
||||
_, err := handler.DataStore.EdgeGroup().EdgeGroup(edgeGroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,7 +148,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
}
|
||||
}
|
||||
|
||||
endpointsFromGroupsToAdd, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToAdd, tx)
|
||||
endpointsFromGroupsToAdd, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToAdd, handler.DataStore)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -181,7 +165,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
}
|
||||
}
|
||||
|
||||
endpointsFromGroupsToRemove, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToRemove, tx)
|
||||
endpointsFromGroupsToRemove, err := edge.GetEndpointsFromEdgeGroups(edgeGroupsToRemove, handler.DataStore)
|
||||
if err != nil {
|
||||
return errors.New("unable to get endpoints from edge groups")
|
||||
}
|
||||
@@ -228,7 +212,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
|
||||
maps.Copy(endpointsFromGroupsToAddMap, edgeJob.Endpoints)
|
||||
|
||||
for endpointID := range endpointsFromGroupsToAddMap {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,13 +3,11 @@ 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.
|
||||
@@ -46,7 +44,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -59,15 +56,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -201,8 +201,6 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
Registries []portainer.RegistryID
|
||||
// Uses the manifest's namespaces instead of the default one
|
||||
UseManifestNamespaces bool
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -249,7 +247,6 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryru
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.FilePathInRepository,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
@@ -348,7 +345,7 @@ func (handler *Handler) storeManifestFromGitRepository(stackFolder string, relat
|
||||
repositoryPassword = repositoryConfig.Authentication.Password
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
|
||||
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// @tags edge_stacks
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -20,7 +20,7 @@ type stackFileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} stackFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -40,7 +40,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
// @tags edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -18,12 +18,32 @@ import (
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Helper()
|
||||
@@ -78,7 +98,7 @@ func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.GitService = testhelpers.NewGitService(errors.New("Clone error"), "git-service-id")
|
||||
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
|
||||
|
||||
return handler, rawAPIKey, storeTeardown
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param body body updateEdgeStackPayload true "EdgeStack data"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
|
||||
@@ -25,8 +25,8 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
// @tags edge, endpoints
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "environment(endpoint) Id"
|
||||
// @param jobID path int true "Job Id"
|
||||
// @param id path string true "environment(endpoint) Id"
|
||||
// @param jobID path string true "Job Id"
|
||||
// @success 200
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -25,8 +25,8 @@ type configResponse struct {
|
||||
// @tags edge, endpoints, edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "environment(endpoint) Id"
|
||||
// @param stackId path int true "EdgeStack Id"
|
||||
// @param id path string true "environment(endpoint) Id"
|
||||
// @param stackId path string true "EdgeStack Id"
|
||||
// @success 200 {object} configResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -81,14 +81,16 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
|
||||
// EE-5910
|
||||
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", nil)
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
// EE-5910
|
||||
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
|
||||
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)
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
|
||||
@@ -30,7 +30,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
{
|
||||
portainer.Endpoint{},
|
||||
portainer.EndpointRelation{},
|
||||
http.StatusForbidden,
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
@@ -43,7 +43,7 @@ var endpointTestCases = []endpointTestCase{
|
||||
portainer.EndpointRelation{
|
||||
EndpointID: -1,
|
||||
},
|
||||
http.StatusForbidden,
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
portainer.Endpoint{
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
/// This feature is implemented in the agent API and not directly here.
|
||||
/// However, it's proxied. So we document it here.
|
||||
|
||||
// @summary Upload a file under a specific path on the file system of an environment (endpoint)
|
||||
// @description Use this environment(endpoint) to upload TLS files.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept multipart/form-data
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param volumeID query string false "Optional volume identifier to upload the file"
|
||||
// @param Path formData string true "The destination path to upload the file to"
|
||||
// @param file formData file true "The file to upload"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/docker/v2/browse/put [post]
|
||||
func _fileBrowseFileUploadV2() {
|
||||
// dummy function to make swag pick up the above docs for the following REST call
|
||||
// POST request on /browse/put?volumeID=:id
|
||||
}
|
||||
@@ -38,6 +38,7 @@ type endpointCreatePayload struct {
|
||||
AzureAuthenticationKey string
|
||||
TagIDs []portainer.TagID
|
||||
EdgeCheckinInterval int
|
||||
IsEdgeDevice bool
|
||||
}
|
||||
|
||||
type endpointCreationEnum int
|
||||
@@ -176,20 +177,20 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
// @accept multipart/form-data
|
||||
// @produce json
|
||||
// @param Name formData string true "Name that will be used to identify this environment(endpoint) (example: my-environment)"
|
||||
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment)" Enum(1,2,3,4,5)
|
||||
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine). Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)"
|
||||
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5)
|
||||
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)". Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)
|
||||
// @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)"
|
||||
// @param GroupID formData int false "Environment(Endpoint) group identifier. If not specified will default to 1 (unassigned)."
|
||||
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint). Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint)"
|
||||
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS"
|
||||
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS"
|
||||
// @param TLSCACertFile formData file false "TLS CA certificate file"
|
||||
// @param TLSCertFile formData file false "TLS client certificate file"
|
||||
// @param TLSKeyFile formData file false "TLS client key file"
|
||||
// @param AzureApplicationID formData string false "Azure application ID. Required if environment(endpoint) type is set to 3"
|
||||
// @param AzureTenantID formData string false "Azure tenant ID. Required if environment(endpoint) type is set to 3"
|
||||
// @param AzureAuthenticationKey formData string false "Azure authentication key. Required if environment(endpoint) type is set to 3"
|
||||
// @param TagIds formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
|
||||
// @param TagIDs formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
|
||||
// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)"
|
||||
// @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel"
|
||||
// @param Gpus formData array false "List of GPUs"
|
||||
@@ -380,6 +381,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
EdgeKey: edgeKey,
|
||||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
UserTrusted: true,
|
||||
}
|
||||
|
||||
@@ -433,6 +435,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
err := handler.snapshotAndPersistEndpoint(endpoint)
|
||||
@@ -498,6 +501,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
}
|
||||
|
||||
endpoint.Agent.Version = agentVersion
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -129,6 +128,7 @@ 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,9 +142,7 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed fetching dockerhub limits")
|
||||
|
||||
@@ -42,13 +42,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
}
|
||||
|
||||
hideFields(endpoint)
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(endpoint, settings)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
if !excludeSnapshot(r) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -104,7 +103,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
paginatedEndpoints[idx].QueryDate = time.Now().Unix()
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings)
|
||||
if !query.excludeSnapshots {
|
||||
err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx])
|
||||
if err != nil {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
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)})
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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,7 +17,6 @@ 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"
|
||||
@@ -57,7 +56,6 @@ type Handler struct {
|
||||
EndpointHandler *endpoints.Handler
|
||||
EndpointHelmHandler *helm.Handler
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
GitOperationHandler *gitops.Handler
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
@@ -84,7 +82,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.19.0
|
||||
// @version 2.18.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -99,7 +97,7 @@ type Handler struct {
|
||||
|
||||
// @securitydefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name x-api-key
|
||||
// @name Authorization
|
||||
|
||||
// @securitydefinitions.apikey jwt
|
||||
// @in header
|
||||
@@ -109,8 +107,6 @@ type Handler struct {
|
||||
// @tag.description Authenticate against Portainer HTTP API
|
||||
// @tag.name custom_templates
|
||||
// @tag.description Manage Custom Templates
|
||||
// @tag.name edge
|
||||
// @tag.description Manage Edge related environment(endpoint) settings
|
||||
// @tag.name edge_groups
|
||||
// @tag.description Manage Edge Groups
|
||||
// @tag.name edge_jobs
|
||||
@@ -119,12 +115,12 @@ type Handler struct {
|
||||
// @tag.description Manage Edge Stacks
|
||||
// @tag.name edge_templates
|
||||
// @tag.description Manage Edge Templates
|
||||
// @tag.name edge
|
||||
// @tag.description Manage Edge related environment(endpoint) settings
|
||||
// @tag.name endpoints
|
||||
// @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
|
||||
@@ -137,14 +133,8 @@ type Handler struct {
|
||||
// @tag.description Manage roles
|
||||
// @tag.name settings
|
||||
// @tag.description Manage Portainer settings
|
||||
// @tag.name ssl
|
||||
// @tag.description Manage ssl settings
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage stacks
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name system
|
||||
// @tag.description Manage Portainer system
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name tags
|
||||
// @tag.description Manage tags
|
||||
// @tag.name teams
|
||||
@@ -153,14 +143,20 @@ type Handler struct {
|
||||
// @tag.description Manage team memberships
|
||||
// @tag.name templates
|
||||
// @tag.description Manage App Templates
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage stacks
|
||||
// @tag.name ssl
|
||||
// @tag.description Manage ssl settings
|
||||
// @tag.name upload
|
||||
// @tag.description Upload files
|
||||
// @tag.name webhooks
|
||||
// @tag.description Manage webhooks
|
||||
// @tag.name websocket
|
||||
// @tag.description Create exec sessions using websockets
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name system
|
||||
// @tag.description Manage Portainer system
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -207,8 +203,6 @@ 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"):
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param release path string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string false "An optional namespace"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid environment(endpoint) id or bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param namespace query string false "specify an optional namespace"
|
||||
// @param filter query string false "specify an optional filter"
|
||||
// @param selector query string false "specify an optional selector"
|
||||
// @param namespace query string true "specify an optional namespace"
|
||||
// @param filter query string true "specify an optional filter"
|
||||
// @param selector query string true "specify an optional selector"
|
||||
// @success 200 {array} release.ReleaseElement "Success"
|
||||
// @failure 400 "Invalid environment(endpoint) identifier"
|
||||
// @failure 401 "Unauthorized"
|
||||
|
||||
@@ -25,7 +25,7 @@ type addHelmRepoUrlPayload struct {
|
||||
}
|
||||
|
||||
func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL, nil)
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoryCreate
|
||||
|
||||
@@ -48,16 +48,15 @@ func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
if method == "editor" {
|
||||
switch method {
|
||||
case "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)
|
||||
@@ -67,7 +66,6 @@ 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")}
|
||||
}
|
||||
@@ -82,7 +80,6 @@ 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()
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL, nil)
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid Helm repository URL. Must correspond to a valid URL format", err)
|
||||
}
|
||||
|
||||
@@ -162,11 +162,9 @@ type composeStackFromGitRepositoryPayload struct {
|
||||
Env []portainer.Pair
|
||||
// Whether the stack is from a app template
|
||||
FromAppTemplate bool `example:"false"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
Name: name,
|
||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||
@@ -175,7 +173,6 @@ func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoU
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
ComposeFile: composeFile,
|
||||
AdditionalFiles: additionalFiles,
|
||||
@@ -261,9 +258,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate,
|
||||
payload.Env,
|
||||
payload.FromAppTemplate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
payload.FromAppTemplate)
|
||||
|
||||
composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext,
|
||||
handler.DataStore,
|
||||
|
||||
@@ -23,17 +23,14 @@ 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, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromK8sFileContentPayload(name, namespace, fileContent string, composeFormat bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
StackName: name,
|
||||
Namespace: namespace,
|
||||
StackFileContent: fileContent,
|
||||
ComposeFormat: composeFormat,
|
||||
FromAppTemplate: fromAppTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +46,9 @@ type kubernetesGitDeploymentPayload struct {
|
||||
ManifestFile string
|
||||
AdditionalFiles []string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
StackName: name,
|
||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||
@@ -62,7 +57,6 @@ func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsern
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
Namespace: namespace,
|
||||
ComposeFormat: composeFormat,
|
||||
@@ -149,7 +143,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, payload.FromAppTemplate)
|
||||
stackPayload := createStackPayloadFromK8sFileContentPayload(payload.StackName, payload.Namespace, payload.StackFileContent, payload.ComposeFormat)
|
||||
|
||||
k8sStackBuilder := stackbuilders.CreateK8sStackFileContentBuilder(handler.DataStore,
|
||||
handler.FileService,
|
||||
@@ -209,9 +203,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
payload.Namespace,
|
||||
payload.ManifestFile,
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
payload.AutoUpdate)
|
||||
|
||||
k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore,
|
||||
handler.FileService,
|
||||
|
||||
@@ -117,8 +117,6 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
||||
// Optional auto update configuration
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -140,7 +138,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
Name: name,
|
||||
SwarmID: swarmID,
|
||||
@@ -203,9 +201,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate,
|
||||
payload.Env,
|
||||
payload.FromAppTemplate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
payload.FromAppTemplate)
|
||||
|
||||
swarmStackBuilder := stackbuilders.CreateSwarmStackGitBuilder(securityContext,
|
||||
handler.DataStore,
|
||||
|
||||
@@ -151,9 +151,6 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
password := payload.RepositoryPassword
|
||||
|
||||
// When the existing stack is using the custom username/password and the password is not updated,
|
||||
// the stack should keep using the saved username/password
|
||||
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
password = stack.GitConfig.Authentication.Password
|
||||
}
|
||||
@@ -161,7 +158,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
|
||||
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch git repository", err)
|
||||
}
|
||||
|
||||
@@ -139,25 +139,13 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
repositoryPassword := ""
|
||||
if payload.RepositoryAuthentication {
|
||||
repositoryPassword = payload.RepositoryPassword
|
||||
|
||||
// When the existing stack is using the custom username/password and the password is not updated,
|
||||
// the stack should keep using the saved username/password
|
||||
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
repositoryPassword = stack.GitConfig.Authentication.Password
|
||||
}
|
||||
repositoryUsername = payload.RepositoryUsername
|
||||
}
|
||||
|
||||
cloneOptions := git.CloneOptions{
|
||||
ProjectPath: stack.ProjectPath,
|
||||
URL: stack.GitConfig.URL,
|
||||
ReferenceName: stack.GitConfig.ReferenceName,
|
||||
Username: repositoryUsername,
|
||||
Password: repositoryPassword,
|
||||
TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
|
||||
}
|
||||
|
||||
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, cloneOptions)
|
||||
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, git.CloneOptions{ProjectPath: stack.ProjectPath, URL: stack.GitConfig.URL, ReferenceName: stack.GitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clone git repository directory", err)
|
||||
}
|
||||
@@ -169,7 +157,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
return httpErr
|
||||
}
|
||||
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, stack.GitConfig.TLSSkipVerify)
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID))
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
|
||||
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch git repository", err)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
)
|
||||
|
||||
type systemInfoResponse struct {
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
Agents int `json:"agents"`
|
||||
Platform plf.ContainerPlatform `json:"platform"`
|
||||
EdgeAgents int `json:"edgeAgents"`
|
||||
EdgeDevices int `json:"edgeDevices"`
|
||||
Agents int `json:"agents"`
|
||||
}
|
||||
|
||||
// @id systemInfo
|
||||
@@ -33,6 +34,7 @@ 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) {
|
||||
@@ -43,6 +45,9 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||
edgeAgents++
|
||||
}
|
||||
|
||||
if environment.IsEdgeDevice {
|
||||
edgeDevices++
|
||||
}
|
||||
}
|
||||
|
||||
platform, err := plf.DetermineContainerPlatform()
|
||||
@@ -51,8 +56,9 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
return response.JSON(w, &systemInfoResponse{
|
||||
EdgeAgents: edgeAgents,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
EdgeAgents: edgeAgents,
|
||||
EdgeDevices: edgeDevices,
|
||||
Agents: agents,
|
||||
Platform: platform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,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/platform"
|
||||
)
|
||||
|
||||
type systemUpgradePayload struct {
|
||||
@@ -30,19 +28,13 @@ 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 204 {object} status "Success"
|
||||
// @success 200 {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)
|
||||
@@ -50,40 +42,10 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
environment, err := handler.guessLocalEndpoint()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to guess local endpoint", err)
|
||||
}
|
||||
|
||||
err = handler.upgradeService.Upgrade(environment, payload.License)
|
||||
err = handler.upgradeService.Upgrade(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,12 +3,10 @@ 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.
|
||||
@@ -31,15 +29,3 @@ 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,20 +7,19 @@ 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
|
||||
}
|
||||
|
||||
@@ -45,28 +44,14 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
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()
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve tags from the database", err)
|
||||
return httperror.InternalServerError("Unable to retrieve tags from the database", err)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Name == payload.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")}
|
||||
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")}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +61,10 @@ func createTag(tx dataservices.DataStoreTx, payload tagCreatePayload) (*portaine
|
||||
Endpoints: map[portainer.EndpointID]bool{},
|
||||
}
|
||||
|
||||
err = tx.Tag().Create(tag)
|
||||
err = handler.DataStore.Tag().Create(tag)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist the tag inside the database", err)
|
||||
return httperror.InternalServerError("Unable to persist the tag inside the database", err)
|
||||
}
|
||||
|
||||
return tag, nil
|
||||
return response.JSON(w, tag)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ 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
|
||||
@@ -31,119 +29,89 @@ 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)
|
||||
|
||||
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) {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if handler.DataStore.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 := tx.Endpoint().Endpoint(endpointID)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
endpoint.TagIDs = removeElement(endpoint.TagIDs, tagID)
|
||||
err = tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
}
|
||||
|
||||
for endpointGroupID := range tag.EndpointGroups {
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpointGroupID)
|
||||
endpointGroup, err := handler.DataStore.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 = tx.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment group", err)
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge groups from the database", err)
|
||||
}
|
||||
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
edgeStacks, err := handler.DataStore.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 = updateEndpointRelations(tx, endpoint, edgeGroups, edgeStacks)
|
||||
err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment relations in the database", 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Tag().DeleteTag(tagID)
|
||||
err = handler.DataStore.Tag().DeleteTag(tagID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to remove the tag from the database", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||
endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||
endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -155,7 +123,7 @@ func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.End
|
||||
}
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
|
||||
func removeElement(slice []portainer.TagID, elem portainer.TagID) []portainer.TagID {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @tags teams
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "Team Id"
|
||||
// @param id path string true "Team Id"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user