Compare commits

...

16 Commits

Author SHA1 Message Date
andres-portainer
369598bc96 Bump version to v2.23.0 (#29) 2024-10-14 13:55:11 -03:00
andres-portainer
61c5269353 fix(edgejobs): decouple the Edge Jobs from the reverse tunnel service BE-10866 (#11) 2024-10-14 10:37:13 -03:00
LP B
7a35b5b0e4 refactor(ui/code-editor): accept enum type (#22)
Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
2024-10-14 13:52:51 +02:00
Yajith Dayarathna
20e9423390 chore: standalone repository workflow cleanup (#26) 2024-10-14 18:34:08 +13:00
Ali
cf230a1cbc fix(k8s-volumes): add missing json labels tag [r8s-108] (#27) 2024-10-14 13:37:59 +13:00
Ali
a06a09afcf fix(app): use standard resource request units [r8s-122] (#15) 2024-10-14 11:27:22 +13:00
Yajith Dayarathna
c88382ec1f fix(apps): persist table settings [r8s-120] (#10)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2024-10-14 11:27:04 +13:00
Ali
fd0bc652a9 fix(volumes): update external labels CE [r8s-108] (#7) 2024-10-14 10:48:13 +13:00
Ali
57e10dc911 fix(apps): group helm apps together [r8s-102] (#24) 2024-10-14 10:28:56 +13:00
Yajith Dayarathna
1110f745e1 fix(volumes): allow standard users to select volumes [r8s-109] (#9)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2024-10-12 13:01:27 +13:00
Oscar Zhou
811d03a419 chore: rm old .vscode.example folders in sub-repo [BE-11287] (#17)
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2024-10-11 16:10:16 +02:00
andres-portainer
666c031821 fix(git): optimize the git cloning process in terms of space BE-11286 (#20) 2024-10-10 18:49:50 -03:00
andres-portainer
4e457d97ad fix(linters): add back removed linters and extend them to CE BE-11294 2024-10-10 17:05:03 -03:00
andres-portainer
364e4f1b4e fix(linters): add back removed linters and extend them to CE BE-11294 2024-10-10 12:06:20 -03:00
andres-portainer
8aae557266 fix(stacks): run webhooks in background to avoid GitHub timeouts BE-11260 2024-10-09 17:28:19 -03:00
Yajith Dayarathna
2bd880ec29 required changes to enable monorepo.
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
2024-10-09 08:37:23 +13:00
127 changed files with 543 additions and 1756 deletions

View File

@@ -1,166 +0,0 @@
name: ci
on:
workflow_dispatch:
push:
branches:
- 'develop'
- 'release/*'
pull_request:
branches:
- 'develop'
- 'release/*'
- 'feat/*'
- 'fix/*'
- 'refactor/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
NODE_VERSION: 18.x
jobs:
build_images:
strategy:
matrix:
config:
- { platform: linux, arch: amd64, version: "" }
- { platform: linux, arch: arm64, version: "" }
- { platform: linux, arch: arm, version: "" }
- { platform: linux, arch: ppc64le, version: "" }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: '[preparation] checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: '[preparation] set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version-file: go.mod
- name: '[preparation] set up node.js'
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- name: '[preparation] set up qemu'
uses: docker/setup-qemu-action@v3.0.0
- name: '[preparation] set up docker context for buildx'
run: docker context create builders
- name: '[preparation] set up docker buildx'
uses: docker/setup-buildx-action@v3.0.0
with:
endpoint: builders
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: '[preparation] set the container image tag'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
fi
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $GITHUB_ENV
- name: '[execution] build linux & windows portainer binaries'
run: |
export YARN_VERSION=$(yarn --version)
export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}')
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
GIT_COMMIT_HASH_LONG=${{ github.sha }}
export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7}
NODE_ENV="testing"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
NODE_ENV="production"
fi
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
env:
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
- name: '[execution] build and push docker images'
run: |
if [ "${{ matrix.config.platform }}" == "windows" ]; then
mv dist/portainer dist/portainer.exe
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
else
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
env:
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
build_manifests:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
needs: [build_images]
steps:
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: '[preparation] set up docker context for buildx'
run: docker version && docker context create builders
- name: '[preparation] set up docker buildx'
uses: docker/setup-buildx-action@v3.0.0
with:
endpoint: builders
- name: '[execution] build and push manifests'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
fi
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64"
fi

View File

@@ -1,3 +1,4 @@
name: Label Conflicts
on:
push:
branches:

View File

@@ -1,55 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.59.1
args: --timeout=10m -c .golangci.yaml

View File

@@ -1,254 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
env:
GO_VERSION: 1.22.5
DOCKER_HUB_REPO: portainerci/portainer-ce
DOCKER_HUB_IMAGE_TAG: develop
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "js_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "go_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image-trivy: ${{ steps.set-trivy-matrix.outputs.image_trivy_result }}
image-docker-scout: ${{ steps.set-docker-scout-matrix.outputs.image_docker_scout_result }}
steps:
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
- name: upload Trivy image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: develop Trivy scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-trivy-result")
- name: upload html file as Trivy artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-trivy-result.html
- name: analyse vulnerabilities from Trivy
id: set-trivy-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
echo "image_trivy_result=${result}" >> $GITHUB_OUTPUT
- name: scan vulnerabilities by Docker Scout
uses: docker/scout-action@v1
continue-on-error: true
with:
command: cves
image: ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
sarif-file: image-docker-scout.json
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: upload Docker Scout image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-docker-scout.json
- name: develop Docker Scout scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
- name: upload html file as Docker Scout artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-docker-scout-result.html
- name: analyse vulnerabilities from Docker Scout
id: set-docker-scout-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=matrix)
echo "image_docker_scout_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Results
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image-trivy: ${{fromJson(needs.image-vulnerability.outputs.image-trivy)}}
image-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.image-docker-scout)}}
steps:
- name: display the results of js, Go, and image scan
run: |
echo "${{ matrix.js.status }}"
echo "${{ matrix.go.status }}"
echo "${{ matrix.image-trivy.status }}"
echo "${{ matrix.image-docker-scout.status }}"
echo "${{ matrix.js.summary }}"
echo "${{ matrix.go.summary }}"
echo "${{ matrix.image-trivy.summary }}"
echo "${{ matrix.image-docker-scout.summary }}"
- name: send message to Slack
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image-trivy.status == 'failure' ||
matrix.image-docker-scout.status == 'failure'
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
}
}
],
"attachments": [
{
"color": "#FF0000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image Trivy vulnerability check*: *${{ matrix.image-trivy.status }}*\n${{ matrix.image-trivy.summary }}\n"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image Docker Scout vulnerability check*: *${{ matrix.image-docker-scout.status }}*\n${{ matrix.image-docker-scout.summary }}\n"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,298 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'go.mod'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./js-snyk-feature.json
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./js-snyk-develop.json
else
echo "null" > ./js-snyk-develop.json
fi
- name: 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: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: analyse different vulnerabilities against develop branch
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
server-dependencies:
name: Server Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-feature-result
path: snyk.json
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./go-snyk-feature.json
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./go-snyk-develop.json
else
echo "null" > ./go-snyk-develop.json
fi
- name: 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: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: analyse different vulnerabilities against develop branch
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
image-vulnerability:
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }}
imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }}
steps:
- name: checkout code
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build-all
- name: set up docker buildx
uses: docker/setup-buildx-action@v2
- name: build and compress image
uses: docker/build-push-action@v4
with:
context: .
file: build/linux/Dockerfile
tags: local-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/local-portainer-image.tar
- name: load docker image
run: |
docker load --input /tmp/local-portainer-image.tar
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress local-portainer:${{ github.sha }}
- name: upload Trivy image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: download Trivy artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-trivy.json ./image-trivy-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-trivy.json ]]; then
mv ./image-trivy.json ./image-trivy-develop.json
else
echo "null" > ./image-trivy-develop.json
fi
- name: pr vs develop Trivy 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-trivy-result")
- name: upload html file as Trivy artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-trivy-result.html
- name: analyse different vulnerabilities against develop branch by Trivy
id: set-diff-trivy-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_trivy_result=${result}" >> $GITHUB_OUTPUT
- name: scan vulnerabilities by Docker Scout
uses: docker/scout-action@v1
continue-on-error: true
with:
command: cves
image: local-portainer:${{ github.sha }}
sarif-file: image-docker-scout.json
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: upload Docker Scout image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-docker-scout.json
- name: download Docker Scout artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-docker-scout.json ./image-docker-scout-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-docker-scout.json ]]; then
mv ./image-docker-scout.json ./image-docker-scout-develop.json
else
echo "null" > ./image-docker-scout-develop.json
fi
- name: pr vs develop Docker Scout scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
- name: upload html file as Docker Scout artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-docker-scout-result.html
- name: analyse different vulnerabilities against develop branch by Docker Scout
id: set-diff-docker-scout-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=matrix)
echo "image_diff_docker_scout_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Result Against develop Branch
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff-trivy: ${{fromJson(needs.image-vulnerability.outputs.imagediff-trivy)}}
imagediff-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.imagediff-docker-scout)}}
steps:
- name: check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff-trivy.status == 'failure' ||
matrix.imagediff-docker-scout.status == 'failure'
run: |
echo "${{ matrix.jsdiff.status }}"
echo "${{ matrix.godiff.status }}"
echo "${{ matrix.imagediff-trivy.status }}"
echo "${{ matrix.imagediff-docker-scout.status }}"
echo "${{ matrix.jsdiff.summary }}"
echo "${{ matrix.godiff.summary }}"
echo "${{ matrix.imagediff-trivy.summary }}"
echo "${{ matrix.imagediff-docker-scout.summary }}"
exit 1

View File

@@ -1,76 +0,0 @@
name: Test
env:
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
on:
workflow_dispatch:
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
push:
branches:
- master
- develop
- release/*
jobs:
test-client:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up node.js'
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
test-server:
strategy:
matrix:
config:
- { platform: linux, arch: amd64 }
- { platform: linux, arch: arm64 }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
- name: 'install dependencies'
run: make test-deps PLATFORM=linux ARCH=amd64
- name: 'update $PATH'
run: echo "$(pwd)/dist" >> $GITHUB_PATH
- name: 'run tests'
run: make test-server

View File

@@ -1,39 +0,0 @@
name: Validate OpenAPI specs
on:
pull_request:
branches:
- master
- develop
- 'release/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
jobs:
openapi-spec:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Download golang modules
run: cd ./api && go get -t -v -d ./...
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

View File

@@ -9,6 +9,9 @@ linters:
- gosimple
- govet
- errorlint
- copyloopvar
- intrange
- perfsprint
linters-settings:
depguard:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
cd $(dirname -- "$0") && yarn lint-staged

View File

@@ -1,19 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,
"args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"]
}
]
}

View File

@@ -1,191 +0,0 @@
{
// Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"React Named Export Component": {
"prefix": "rnec",
"body": [
"export function $TM_FILENAME_BASE() {",
" return <div>$TM_FILENAME_BASE</div>;",
"}"
],
"description": "React Named Export Component"
},
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller,",
"});",
""
]
},
"Controller": {
"scope": "javascript",
"prefix": "mycontroller",
"body": [
"class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {",
"\t/* @ngInject */",
"\tconstructor($0) {",
"\t}",
"}",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Dummy ES6+ controller"
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
"description": "Dummy ES6+ service",
"body": [
"import angular from 'angular';",
"import PortainerError from 'Portainer/error';",
"",
"class $1 {",
" /* @ngInject */",
" constructor(\\$async, $0) {",
" this.\\$async = \\$async;",
"",
" this.getAsync = this.getAsync.bind(this);",
" this.getAllAsync = this.getAllAsync.bind(this);",
" this.createAsync = this.createAsync.bind(this);",
" this.updateAsync = this.updateAsync.bind(this);",
" this.deleteAsync = this.deleteAsync.bind(this);",
" }",
"",
" /**",
" * GET",
" */",
" async getAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" async getAllAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" get() {",
" if () {",
" return this.\\$async(this.getAsync);",
" }",
" return this.\\$async(this.getAllAsync);",
" }",
"",
" /**",
" * CREATE",
" */",
" async createAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" create() {",
" return this.\\$async(this.createAsync);",
" }",
"",
" /**",
" * UPDATE",
" */",
" async updateAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" update() {",
" return this.\\$async(this.updateAsync);",
" }",
"",
" /**",
" * DELETE",
" */",
" async deleteAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" delete() {",
" return this.\\$async(this.deleteAsync);",
" }",
"}",
"",
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
},
"swagger-api-doc": {
"prefix": "swapi",
"scope": "go",
"description": "Snippet for a api doc",
"body": [
"// @id ",
"// @summary ",
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",
"// @param id path int true \"identifier\"",
"// @param body body Object true \"details\"",
"// @success 200 {object} portainer. \"Success\"",
"// @success 204 \"Success\"",
"// @failure 400 \"Invalid request\"",
"// @failure 403 \"Permission denied\"",
"// @failure 404 \" not found\"",
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

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

View File

@@ -15,7 +15,7 @@ import (
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
targzPath := filepath.Join(absolutePath, filepath.Base(absolutePath)+".tar.gz")
outFile, err := os.Create(targzPath)
if err != nil {
return "", err

View File

@@ -1,7 +1,6 @@
package archive
import (
"fmt"
"os"
"os/exec"
"path"
@@ -24,7 +23,7 @@ func listFiles(dir string) []string {
return items
}
func Test_shouldCreateArhive(t *testing.T) {
func Test_shouldCreateArchive(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
@@ -34,12 +33,11 @@ func Test_shouldCreateArhive(t *testing.T) {
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
if err := cmd.Run(); err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
@@ -56,7 +54,7 @@ func Test_shouldCreateArhive(t *testing.T) {
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
func Test_shouldCreateArchive2(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
@@ -66,12 +64,11 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
if err := ExtractTarGz(r, extractionDir); err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)

View File

@@ -3,7 +3,7 @@ package ecr
import (
"context"
"encoding/base64"
"fmt"
"errors"
"strings"
"time"
)
@@ -15,7 +15,7 @@ func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Ti
}
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
err = fmt.Errorf("AuthorizationData is empty")
err = errors.New("AuthorizationData is empty")
return
}
@@ -50,7 +50,7 @@ func (s *Service) ParseAuthorizationToken(token string) (username string, passwo
splitToken := strings.Split(token, ":")
if len(splitToken) < 2 {
err = fmt.Errorf("invalid ECR authorization token")
err = errors.New("invalid ECR authorization token")
return
}

View File

@@ -94,7 +94,7 @@ func encrypt(path string, passphrase string) (string, error) {
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
outFileName := path + ".encrypted"
out, err := os.Create(outFileName)
if err != nil {
return "", err

View File

@@ -1,82 +0,0 @@
package chisel
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
)
// EdgeJobs retrieves the edge jobs for the given environment
func (service *Service) EdgeJobs(endpointID portainer.EndpointID) []portainer.EdgeJob {
service.mu.RLock()
defer service.mu.RUnlock()
return append(
make([]portainer.EdgeJob, 0, len(service.edgeJobs[endpointID])),
service.edgeJobs[endpointID]...,
)
}
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
return
}
service.mu.Lock()
defer service.mu.Unlock()
existingJobIndex := -1
for idx, existingJob := range service.edgeJobs[endpoint.ID] {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
break
}
}
if existingJobIndex == -1 {
service.edgeJobs[endpoint.ID] = append(service.edgeJobs[endpoint.ID], *edgeJob)
} else {
service.edgeJobs[endpoint.ID][existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for endpointID := range service.edgeJobs {
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
cache.Del(endpointID)
}
service.mu.Unlock()
}
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
defer service.mu.Unlock()
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
cache.Del(endpointID)
}

View File

@@ -31,7 +31,6 @@ import (
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/snapshot"
@@ -467,10 +466,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}
if err := edge.LoadEdgeJobs(dataStore, reverseTunnelService); err != nil {
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
}
applicationStatus := initStatus(instanceID)
// channel to control when the admin user is created

View File

@@ -31,8 +31,7 @@ const (
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
err := aesEncryptGCM(input, output, passphrase)
if err != nil {
if err := aesEncryptGCM(input, output, passphrase); err != nil {
return fmt.Errorf("error encrypting file: %w", err)
}
@@ -142,7 +141,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
}
if string(header) != aesGcmHeader {
return nil, fmt.Errorf("invalid header")
return nil, errors.New("invalid header")
}
// Read salt
@@ -194,8 +193,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
return nil, err
}
_, err = buf.Write(plaintext)
if err != nil {
if _, err := buf.Write(plaintext); err != nil {
return nil, err
}

View File

@@ -21,8 +21,7 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
@@ -62,7 +61,7 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, error) {
var k *portainer.APIKey
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Connection.GetAll(
BucketName,
&portainer.APIKey{},

View File

@@ -48,7 +48,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// if no ResourceControl was found.
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Connection.GetAll(
BucketName,
&portainer.ResourceControl{},

View File

@@ -19,7 +19,7 @@ type ServiceTx struct {
// if no ResourceControl was found.
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Tx.GetAll(
BucketName,
&portainer.ResourceControl{},

View File

@@ -1,7 +1,6 @@
package datastore
import (
"fmt"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -33,7 +32,7 @@ func TestStoreCreation(t *testing.T) {
func TestBackup(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
backupFileName := store.backupFilename()
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
t.Run("Backup should create "+backupFileName, func(t *testing.T) {
v := models.Version{
Edition: int(portainer.PortainerCE),
SchemaVersion: portainer.APIVersion,

View File

@@ -602,7 +602,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
"KubectlShellImage": "portainer/kubectl-shell:2.23.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -932,6 +932,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.23.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -142,23 +142,23 @@ func (i *Image) hubLink() (string, error) {
prefix = "_"
path = strings.Replace(i.Path, "library/", "", 1)
}
return fmt.Sprintf("https://hub.docker.com/%s/%s", prefix, path), nil
return "https://hub.docker.com/" + prefix + "/" + path, nil
case "docker.bintray.io", "jfrog-docker-reg2.bintray.io":
return fmt.Sprintf("https://bintray.com/jfrog/reg2/%s", strings.ReplaceAll(i.Path, "/", "%3A")), nil
return "https://bintray.com/jfrog/reg2/" + strings.ReplaceAll(i.Path, "/", "%3A"), nil
case "docker.pkg.github.com":
return fmt.Sprintf("https://github.com/%s/packages", filepath.ToSlash(filepath.Dir(i.Path))), nil
return "https://github.com/" + filepath.ToSlash(filepath.Dir(i.Path)) + "/packages", nil
case "gcr.io":
return fmt.Sprintf("https://%s/%s", i.Domain, i.Path), nil
return "https://" + i.Domain + "/" + i.Path, nil
case "ghcr.io":
ref := strings.Split(i.Path, "/")
ghUser, ghPackage := ref[0], ref[1]
return fmt.Sprintf("https://github.com/users/%s/packages/container/package/%s", ghUser, ghPackage), nil
return "https://github.com/users/" + ghUser + "/packages/container/package/" + ghPackage, nil
case "quay.io":
return fmt.Sprintf("https://quay.io/repository/%s", i.Path), nil
return "https://quay.io/repository/" + i.Path, nil
case "registry.access.redhat.com":
return fmt.Sprintf("https://access.redhat.com/containers/#/registry.access.redhat.com/%s", i.Path), nil
return "https://access.redhat.com/containers/#/registry.access.redhat.com/" + i.Path, nil
case "registry.gitlab.com":
return fmt.Sprintf("https://gitlab.com/%s/container_registry", i.Path), nil
return "https://gitlab.com/" + i.Path + "/container_registry", nil
default:
return "", nil
}

View File

@@ -1,7 +1,6 @@
package images
import (
"fmt"
"strings"
"github.com/containers/image/v5/docker"
@@ -10,7 +9,7 @@ import (
func ParseReference(imageStr string) (types.ImageReference, error) {
if !strings.HasPrefix(imageStr, "//") {
imageStr = fmt.Sprintf("//%s", imageStr)
imageStr = "//" + imageStr
}
return docker.ParseReference(imageStr)
}

View File

@@ -2,7 +2,6 @@ package exec
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
@@ -60,8 +59,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
if err != nil {
if err := w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{}); err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}
@@ -69,8 +67,7 @@ func Test_UpAndDown(t *testing.T) {
t.Fatal("container should exist")
}
err = w.Down(ctx, stack, endpoint)
if err != nil {
if err := w.Down(ctx, stack, endpoint); err != nil {
t.Fatalf("Error calling docker-compose down: %s", err)
}
@@ -80,7 +77,7 @@ func Test_UpAndDown(t *testing.T) {
}
func containerExists(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
cmd := exec.Command("docker", "ps", "-a", "-f", "name="+containerName)
out, err := cmd.Output()
if err != nil {

View File

@@ -71,7 +71,7 @@ func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *
}
if token == "" {
return "", fmt.Errorf("can not get a valid user service account token")
return "", errors.New("can not get a valid user service account token")
}
return token, nil

View File

@@ -8,6 +8,7 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
@@ -357,7 +358,7 @@ func (service *Service) RollbackStackFile(stackIdentifier, fileName string) erro
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
composeFilePath := JoinPaths(stackStorePath, fileName)
path := service.wrapFileStore(composeFilePath)
backupPath := fmt.Sprintf("%s.bak", path)
backupPath := path + ".bak"
exists, err := service.FileExists(backupPath)
if err != nil {
@@ -381,12 +382,12 @@ func (service *Service) RollbackStackFile(stackIdentifier, fileName string) erro
func (service *Service) RollbackStackFileByVersion(stackIdentifier string, version int, fileName string) error {
versionStr := ""
if version != 0 {
versionStr = fmt.Sprintf("v%d", version)
versionStr = "v" + strconv.Itoa(version)
}
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier, versionStr)
composeFilePath := JoinPaths(stackStorePath, fileName)
path := service.wrapFileStore(composeFilePath)
backupPath := fmt.Sprintf("%s.bak", path)
backupPath := path + ".bak"
exists, err := service.FileExists(backupPath)
if err != nil {
@@ -671,7 +672,7 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
// createBackupFileInStore makes a copy in the file store.
func (service *Service) createBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path)
backupPath := path + ".bak"
return service.Copy(path, backupPath, true)
}
@@ -679,7 +680,7 @@ func (service *Service) createBackupFileInStore(filePath string) error {
// removeBackupFileInStore removes the copy in the file store.
func (service *Service) removeBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path)
backupPath := path + ".bak"
exists, err := service.FileExists(backupPath)
if err != nil {
@@ -799,7 +800,7 @@ func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID strin
return err
}
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
filePath := JoinPaths(edgeJobStorePath, "logs_"+taskID)
r := bytes.NewReader(data)
return service.createFileInStore(filePath, r)
}
@@ -990,7 +991,7 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
if alreadyExists {
if !overwriteTargetPath {
return fmt.Errorf("Target path already exists")
return errors.New("Target path already exists")
}
if err = os.RemoveAll(newPath); err != nil {

View File

@@ -51,7 +51,7 @@ func FilterDirForEntryFile(dirEntries []DirEntry, entryFile string) []DirEntry {
// FilterDirForCompatibility returns the content of the entry file if agent version is less than 2.19.0
func FilterDirForCompatibility(dirEntries []DirEntry, entryFilePath, agentVersion string) (string, error) {
if semver.Compare(fmt.Sprintf("v%s", agentVersion), "v2.19.0") == -1 {
if semver.Compare("v"+agentVersion, "v2.19.0") == -1 {
for _, dirEntry := range dirEntries {
if dirEntry.IsFile {
if dirEntry.Name == entryFilePath {

View File

@@ -116,7 +116,7 @@ func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool {
filterEqual := filepath.Join(configPath, deviceName)
// example: A/B/C/<deviceName>/
filterPrefix := fmt.Sprintf("%s.", filterEqual)
filterPrefix := filterEqual + "."
// include file entries: A/B/C/<deviceName> or A/B/C/<deviceName>.*
return dirEntry.Name == filterEqual || strings.HasPrefix(dirEntry.Name, filterPrefix)

View File

@@ -1,12 +1,11 @@
package git
import (
"fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -25,32 +24,28 @@ type CloneOptions struct {
}
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
backupProjectPath := fmt.Sprintf("%s-old", options.ProjectPath)
backupProjectPath := options.ProjectPath + "-old"
cleanUp := false
cleanFn := func() {
if !cleanUp {
return
}
err = fileService.RemoveDirectory(backupProjectPath)
if err != nil {
if err := fileService.RemoveDirectory(backupProjectPath); err != nil {
log.Warn().Err(err).Msg("unable to remove git repository directory")
}
}
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true)
if err != nil {
if err := filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true); err != nil {
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
}
cleanUp = true
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
if err != nil {
if err := gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify); err != nil {
cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false)
if restoreError != nil {
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil {
log.Warn().Err(err).Msg("failed restoring backup folder")
}
if errors.Is(err, gittypes.ErrAuthenticationFailure) {

View File

@@ -34,6 +34,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.username, opt.password),
Tags: git.NoTags,
}
if opt.referenceName != "" {

View File

@@ -24,8 +24,7 @@ func setup(t *testing.T) string {
t.Fatal(errors.Wrap(err, "failed to open an archive"))
}
err = archive.ExtractTarGz(file, dir)
if err != nil {
if err := archive.ExtractTarGz(file, dir); err != nil {
t.Fatal(errors.Wrapf(err, "failed to extract file from the archive to a folder %s", dir))
}

View File

@@ -123,7 +123,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configuration.MPSToken))
req.Header.Set("Authorization", "Bearer "+configuration.MPSToken)
response, err := service.httpsClient.Do(req)
if err != nil {

View File

@@ -97,7 +97,7 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Authorization", "Bearer "+token)
response, err := service.httpsClient.Do(req)
if err != nil {
@@ -128,7 +128,7 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Authorization", "Bearer "+token)
response, err := service.httpsClient.Do(req)
if err != nil {

View File

@@ -1,7 +1,6 @@
package backup
import (
"fmt"
"net/http"
"os"
"path/filepath"
@@ -37,8 +36,7 @@ func (p *backupPayload) Validate(r *http.Request) error {
// @router /backup [post]
func (h *Handler) backup(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload backupPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@@ -48,7 +46,7 @@ func (h *Handler) backup(w http.ResponseWriter, r *http.Request) *httperror.Hand
}
defer os.RemoveAll(filepath.Dir(archivePath))
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("portainer-backup_%s", filepath.Base(archivePath))))
w.Header().Set("Content-Disposition", "attachment; filename=portainer-backup_"+filepath.Base(archivePath))
http.ServeFile(w, r, archivePath)
return nil

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"errors"
"fmt"
"net/http"
"os"
"regexp"
@@ -52,15 +51,13 @@ func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Requ
}
}
err = handler.DataStore.CustomTemplate().Create(customTemplate)
if err != nil {
if err := handler.DataStore.CustomTemplate().Create(customTemplate); err != nil {
return httperror.InternalServerError("Unable to create custom template", err)
}
resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, tokenData.ID)
err = handler.DataStore.ResourceControl().Create(resourceControl)
if err != nil {
if err := handler.DataStore.ResourceControl().Create(resourceControl); err != nil {
return httperror.InternalServerError("Unable to persist resource control inside the database", err)
}
@@ -155,8 +152,7 @@ func isValidNote(note string) bool {
// @router /custom_templates/create/string [post]
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return nil, err
}
@@ -272,8 +268,7 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
// @router /custom_templates/create/repository [post]
func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return nil, err
}
@@ -423,12 +418,10 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
varsString, _ := request.RetrieveMultiPartFormValue(r, "Variables", true)
if varsString != "" {
err = json.Unmarshal([]byte(varsString), &payload.Variables)
if err != nil {
if err := json.Unmarshal([]byte(varsString), &payload.Variables); err != nil {
return errors.New("Invalid variables. Ensure that the variables are valid JSON")
}
err = validateVariablesDefinitions(payload.Variables)
if err != nil {
if err := validateVariablesDefinitions(payload.Variables); err != nil {
return err
}
}
@@ -462,8 +455,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
// @router /custom_templates/create/file [post]
func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) {
payload := &customTemplateFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
if err := payload.Validate(r); err != nil {
return nil, err
}
@@ -513,6 +505,5 @@ func deprecatedCustomTemplateCreateUrlParser(w http.ResponseWriter, r *http.Requ
return "", httperror.BadRequest("Invalid query parameter: method", err)
}
url := fmt.Sprintf("/custom_templates/create/%s", method)
return url, nil
return "/custom_templates/create/" + method, nil
}

View File

@@ -1,7 +1,6 @@
package customtemplates
import (
"fmt"
"net/http"
"os"
"sync"
@@ -80,8 +79,7 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re
if customTemplate.GitConfig.ConfigHash != commitHash {
customTemplate.GitConfig.ConfigHash = commitHash
err = handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate)
if err != nil {
if err := handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate); err != nil {
return httperror.InternalServerError("Unable to persist custom template changes inside the database", err)
}
}
@@ -100,9 +98,8 @@ func backupCustomTemplate(projectPath string) (string, error) {
return "", err
}
backupPath := fmt.Sprintf("%s-backup", projectPath)
err = os.Rename(projectPath, backupPath)
if err != nil {
backupPath := projectPath + "-backup"
if err := os.Rename(projectPath, backupPath); err != nil {
return "", err
}
@@ -110,8 +107,7 @@ func backupCustomTemplate(projectPath string) (string, error) {
}
func rollbackCustomTemplate(backupPath, projectPath string) error {
err := os.RemoveAll(projectPath)
if err != nil {
if err := os.RemoveAll(projectPath); err != nil {
return err
}

View File

@@ -8,6 +8,7 @@ import (
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/edge/cache"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/slicesx"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -55,8 +56,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
}
var payload edgeGroupUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@@ -105,8 +105,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
edgeGroup.PartialMatch = *payload.PartialMatch
}
err = tx.EdgeGroup().Update(edgeGroup.ID, edgeGroup)
if err != nil {
if err := tx.EdgeGroup().Update(edgeGroup.ID, edgeGroup); err != nil {
return httperror.InternalServerError("Unable to persist Edge group changes inside the database", err)
}
@@ -136,8 +135,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to get Environment from database", err)
}
err = handler.updateEndpointStacks(tx, endpoint, edgeGroups, edgeStacks)
if err != nil {
if err := handler.updateEndpointStacks(tx, endpoint, edgeGroups, edgeStacks); err != nil {
return httperror.InternalServerError("Unable to persist Environment relation changes inside the database", err)
}
@@ -156,8 +154,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
continue
}
err = handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation)
if err != nil {
if err := handler.updateEndpointEdgeJobs(edgeGroup.ID, endpoint, edgeJobs, operation); err != nil {
return httperror.InternalServerError("Unable to persist Environment Edge Jobs changes inside the database", err)
}
}
@@ -198,10 +195,8 @@ func (handler *Handler) updateEndpointEdgeJobs(edgeGroupID portainer.EdgeGroupID
}
switch operation {
case "add":
handler.ReverseTunnelService.AddEdgeJob(endpoint, &edgeJob)
case "remove":
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpoint.ID, edgeJob.ID)
case "add", "remove":
cache.Del(endpoint.ID)
}
}

View File

@@ -2,7 +2,6 @@ package edgejobs
import (
"errors"
"fmt"
"maps"
"net/http"
"strconv"
@@ -12,6 +11,7 @@ import (
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/edge/cache"
"github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
@@ -114,11 +114,14 @@ func (handler *Handler) createEdgeJob(tx dataservices.DataStoreTx, payload *edge
}
}
err = handler.addAndPersistEdgeJob(tx, edgeJob, fileContent, endpoints)
if err != nil {
if err := handler.addAndPersistEdgeJob(tx, edgeJob, fileContent, endpoints); err != nil {
return nil, httperror.InternalServerError("Unable to schedule Edge job", err)
}
for _, endpointID := range endpoints {
cache.Del(endpointID)
}
return edgeJob, nil
}
@@ -145,15 +148,13 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
payload.CronExpression = cronExpression
var endpoints []portainer.EndpointID
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, true)
if err != nil {
if err := request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, true); err != nil {
return errors.New("invalid environments")
}
payload.Endpoints = endpoints
var edgeGroups []portainer.EdgeGroupID
err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, true)
if err != nil {
if err := request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, true); err != nil {
return errors.New("invalid edge groups")
}
payload.EdgeGroups = edgeGroups
@@ -268,15 +269,6 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
return errors.New("environments or edge groups are mandatory for an Edge job")
}
for endpointID := range endpointsMap {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
}
return tx.EdgeJob().CreateWithID(edgeJob.ID, edgeJob)
}
@@ -300,5 +292,5 @@ func deprecatedEdgeJobCreateUrlParser(w http.ResponseWriter, r *http.Request) (s
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
}
return fmt.Sprintf("/edge_jobs/create/%s", method), nil
return "/edge_jobs/create/" + method, nil
}

View File

@@ -9,9 +9,11 @@ import (
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/edge/cache"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
@@ -33,10 +35,9 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h
return httperror.BadRequest("Invalid Edge job identifier route variable", err)
}
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return handler.deleteEdgeJob(tx, portainer.EdgeJobID(edgeJobID))
})
if err != nil {
}); err != nil {
var handlerError *httperror.HandlerError
if errors.As(err, &handlerError) {
return handlerError
@@ -57,13 +58,10 @@ func (handler *Handler) deleteEdgeJob(tx dataservices.DataStoreTx, edgeJobID por
}
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(int(edgeJobID)))
err = handler.FileService.RemoveDirectory(edgeJobFolder)
if err != nil {
if err := handler.FileService.RemoveDirectory(edgeJobFolder); err != nil {
log.Warn().Err(err).Msg("Unable to remove the files associated to the Edge job on the filesystem")
}
handler.ReverseTunnelService.RemoveEdgeJob(edgeJob.ID)
var endpointsMap map[portainer.EndpointID]portainer.EdgeJobEndpointMeta
if len(edgeJob.EdgeGroups) > 0 {
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
@@ -78,11 +76,10 @@ func (handler *Handler) deleteEdgeJob(tx dataservices.DataStoreTx, edgeJobID por
}
for endpointID := range endpointsMap {
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpointID, edgeJob.ID)
cache.Del(endpointID)
}
err = tx.EdgeJob().Delete(edgeJob.ID)
if err != nil {
if err := tx.EdgeJob().Delete(edgeJob.ID); err != nil {
return httperror.InternalServerError("Unable to remove the Edge job from the database", err)
}

View File

@@ -9,6 +9,7 @@ import (
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/edge/cache"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -53,7 +54,7 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
}
}
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if 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)
@@ -61,8 +62,7 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request
}
return handler.clearEdgeJobTaskLogs(tx, portainer.EdgeJobID(edgeJobID), portainer.EndpointID(taskID), updateEdgeJobFn)
})
if err != nil {
}); err != nil {
var handlerError *httperror.HandlerError
if errors.As(err, &handlerError) {
return handlerError
@@ -82,8 +82,7 @@ func (handler *Handler) clearEdgeJobTaskLogs(tx dataservices.DataStoreTx, edgeJo
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)))
if err != nil {
if err := handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID))); err != nil {
return httperror.InternalServerError("Unable to clear log file from disk", err)
}
@@ -92,17 +91,11 @@ func (handler *Handler) clearEdgeJobTaskLogs(tx dataservices.DataStoreTx, edgeJo
return httperror.InternalServerError("Unable to get Endpoints from EdgeGroups", err)
}
err = updateEdgeJob(edgeJob, endpointID, endpointsFromGroups)
if err != nil {
if err := updateEdgeJob(edgeJob, endpointID, endpointsFromGroups); err != nil {
return httperror.InternalServerError("Unable to persist Edge job changes in the database", err)
}
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
return httperror.NotFound("Unable to retrieve environment from the database", err)
}
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
cache.Del(endpointID)
return nil
}

View File

@@ -8,6 +8,7 @@ import (
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/edge/cache"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -38,7 +39,7 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
return httperror.BadRequest("Invalid Task identifier route variable", err)
}
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
edgeJob, err := tx.EdgeJob().Read(portainer.EdgeJobID(edgeJobID))
if tx.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find an Edge job with the specified identifier inside the database", err)
@@ -64,8 +65,7 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
edgeJob.Endpoints[endpointID] = meta
}
err = tx.EdgeJob().Update(edgeJob.ID, edgeJob)
if err != nil {
if err := tx.EdgeJob().Update(edgeJob.ID, edgeJob); err != nil {
return httperror.InternalServerError("Unable to persist Edge job changes in the database", err)
}
@@ -74,16 +74,14 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
}
cache.Del(endpointID)
if endpoint.Edge.AsyncMode {
return httperror.BadRequest("Async Edge Endpoints are not supported in Portainer CE", nil)
}
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
return nil
})
if err != nil {
}); err != nil {
var handlerError *httperror.HandlerError
if errors.As(err, &handlerError) {
return handlerError

View File

@@ -10,6 +10,7 @@ import (
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/edge/cache"
"github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
@@ -56,8 +57,7 @@ func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *h
}
var payload edgeJobUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@@ -78,13 +78,11 @@ func (handler *Handler) updateEdgeJob(tx dataservices.DataStoreTx, edgeJobID por
return nil, httperror.InternalServerError("Unable to find an Edge job with the specified identifier inside the database", err)
}
err = handler.updateEdgeSchedule(tx, edgeJob, &payload)
if err != nil {
if err := handler.updateEdgeSchedule(tx, edgeJob, &payload); err != nil {
return nil, httperror.InternalServerError("Unable to update Edge job", err)
}
err = tx.EdgeJob().Update(edgeJob.ID, edgeJob)
if err != nil {
if err := tx.EdgeJob().Update(edgeJob.ID, edgeJob); err != nil {
return nil, httperror.InternalServerError("Unable to persist Edge job changes inside the database", err)
}
@@ -149,8 +147,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
if len(payload.EdgeGroups) > 0 {
for _, edgeGroupID := range payload.EdgeGroups {
_, err := tx.EdgeGroup().Read(edgeGroupID)
if err != nil {
if _, err := tx.EdgeGroup().Read(edgeGroupID); err != nil {
return err
}
@@ -203,8 +200,7 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
if payload.FileContent != nil && *payload.FileContent != string(fileContent) {
fileContent = []byte(*payload.FileContent)
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), fileContent)
if err != nil {
if _, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), fileContent); err != nil {
return err
}
@@ -223,16 +219,11 @@ func (handler *Handler) updateEdgeSchedule(tx dataservices.DataStoreTx, edgeJob
maps.Copy(endpointsFromGroupsToAddMap, edgeJob.Endpoints)
for endpointID := range endpointsFromGroupsToAddMap {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
cache.Del(endpointID)
}
for endpointID := range endpointsToRemove {
handler.ReverseTunnelService.RemoveEdgeJobFromEndpoint(endpointID, edgeJob.ID)
cache.Del(endpointID)
}
return nil

View File

@@ -1,7 +1,6 @@
package edgestacks
import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@@ -78,5 +77,5 @@ func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request)
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
}
return fmt.Sprintf("/edge_stacks/create/%s", method), nil
return "/edge_stacks/create/" + method, nil
}

View File

@@ -133,7 +133,7 @@ func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStore
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
}
if hasWrongType {
return "", "", "", fmt.Errorf("edge stack with config do not match the environment type")
return "", "", "", errors.New("edge stack with config do not match the environment type")
}
projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder)

View File

@@ -92,7 +92,7 @@ func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolde
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
}
if hasWrongType {
return "", "", "", fmt.Errorf("edge stack with config do not match the environment type")
return "", "", "", errors.New("edge stack with config do not match the environment type")
}
if deploymentType == portainer.EdgeStackDeploymentCompose {
@@ -107,7 +107,6 @@ func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolde
}
if deploymentType == portainer.EdgeStackDeploymentKubernetes {
manifestPath = filesystem.ManifestFileDefaultName
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, manifestPath, fileContent)

View File

@@ -207,7 +207,7 @@ func TestCreateWithInvalidPayload(t *testing.T) {
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/edge_stacks/create/%s", tc.Method), r)
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/"+tc.Method, r)
if err != nil {
t.Fatal("request error:", err)
}

View File

@@ -9,6 +9,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/internal/edge/cache"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -84,8 +85,7 @@ func (handler *Handler) getEdgeJobLobs(tx dataservices.DataStoreTx, endpointID p
return httperror.InternalServerError("Unable to find an edge job with the specified identifier inside the database", err)
}
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpointID)), []byte(payload.FileContent))
if err != nil {
if err := handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(int(edgeJobID)), strconv.Itoa(int(endpoint.ID)), []byte(payload.FileContent)); err != nil {
return httperror.InternalServerError("Unable to save task log to the filesystem", err)
}
@@ -96,13 +96,11 @@ func (handler *Handler) getEdgeJobLobs(tx dataservices.DataStoreTx, endpointID p
edgeJob.Endpoints[endpoint.ID] = meta
}
err = tx.EdgeJob().Update(edgeJob.ID, edgeJob)
handler.ReverseTunnelService.AddEdgeJob(endpoint, edgeJob)
if err != nil {
if err := tx.EdgeJob().Update(edgeJob.ID, edgeJob); err != nil {
return httperror.InternalServerError("Unable to persist edge job changes to the database", err)
}
cache.Del(endpointID)
return nil
}

View File

@@ -170,7 +170,7 @@ func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Reque
Credentials: tunnel.Credentials,
}
schedules, handlerErr := handler.buildSchedules(endpoint.ID)
schedules, handlerErr := handler.buildSchedules(tx, endpoint.ID)
if handlerErr != nil {
return nil, handlerErr
}
@@ -208,9 +208,33 @@ func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
}
}
func (handler *Handler) buildSchedules(endpointID portainer.EndpointID) ([]edgeJobResponse, *httperror.HandlerError) {
func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) ([]edgeJobResponse, *httperror.HandlerError) {
schedules := []edgeJobResponse{}
for _, job := range handler.ReverseTunnelService.EdgeJobs(endpointID) {
edgeJobs, err := tx.EdgeJob().ReadAll()
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve Edge Jobs", err)
}
for _, job := range edgeJobs {
_, endpointHasJob := job.Endpoints[endpointID]
if !endpointHasJob {
for _, edgeGroupID := range job.EdgeGroups {
member, _, err := edge.EndpointInEdgeGroup(tx, endpointID, edgeGroupID)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve relations", err)
} else if member {
endpointHasJob = true
break
}
}
}
if !endpointHasJob {
continue
}
var collectLogs bool
if _, ok := job.GroupLogsCollection[endpointID]; ok {
collectLogs = job.GroupLogsCollection[endpointID].CollectLogs

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
@@ -36,7 +37,7 @@ var endpointTestCases = []endpointTestCase{
{
portainer.Endpoint{
ID: -1,
Name: "endpoint-id--1",
Name: "endpoint-id-1",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
@@ -342,28 +343,48 @@ func TestEdgeStackStatus(t *testing.T) {
func TestEdgeJobsResponse(t *testing.T) {
handler := mustSetupHandler(t)
endpointID := portainer.EndpointID(77)
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-77",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
LastCheckInDate: time.Now().Unix(),
localCreateEndpoint := func(endpointID portainer.EndpointID, tagIDs []portainer.TagID) *portainer.Endpoint {
endpoint := portainer.Endpoint{
ID: endpointID,
Name: "test-endpoint-" + strconv.Itoa(int(endpointID)),
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id-" + strconv.Itoa(int(endpointID)),
TagIDs: tagIDs,
LastCheckInDate: time.Now().Unix(),
UserTrusted: true,
}
err := createEndpoint(handler, endpoint,
portainer.EndpointRelation{EndpointID: endpointID})
require.NoError(t, err)
return &endpoint
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
}
dynamicGroupTags := []portainer.TagID{1, 2, 3}
if err := createEndpoint(handler, endpoint, endpointRelation); err != nil {
t.Fatal(err)
endpoint := localCreateEndpoint(77, nil)
endpointFromStaticEdgeGroup := localCreateEndpoint(78, nil)
endpointFromDynamicEdgeGroup := localCreateEndpoint(79, dynamicGroupTags)
unrelatedEndpoint := localCreateEndpoint(80, nil)
staticEdgeGroup := portainer.EdgeGroup{
ID: 1,
Endpoints: []portainer.EndpointID{endpointFromStaticEdgeGroup.ID},
}
err := handler.DataStore.EdgeGroup().Create(&staticEdgeGroup)
require.NoError(t, err)
dynamicEdgeGroup := portainer.EdgeGroup{
ID: 2,
Dynamic: true,
TagIDs: dynamicGroupTags,
}
err = handler.DataStore.EdgeGroup().Create(&dynamicEdgeGroup)
require.NoError(t, err)
path, err := handler.FileService.StoreEdgeJobFileFromBytes("test-script", []byte("pwd"))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
edgeJobID := portainer.EdgeJobID(35)
edgeJob := portainer.EdgeJob{
@@ -374,32 +395,42 @@ func TestEdgeJobsResponse(t *testing.T) {
ScriptPath: path,
Recurring: true,
Version: 57,
Endpoints: map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{
endpoint.ID: {},
},
EdgeGroups: []portainer.EdgeGroupID{staticEdgeGroup.ID, dynamicEdgeGroup.ID},
}
handler.ReverseTunnelService.AddEdgeJob(&endpoint, &edgeJob)
err = handler.DataStore.EdgeJob().Create(&edgeJob)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
f := func(endpoint *portainer.Endpoint, scheduleLen int) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
require.NoError(t, err)
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var data endpointEdgeStatusInspectResponse
err = json.NewDecoder(rec.Body).Decode(&data)
require.NoError(t, err)
require.Len(t, data.Schedules, scheduleLen)
if scheduleLen > 0 {
require.Equal(t, edgeJob.ID, data.Schedules[0].ID)
require.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
require.Equal(t, edgeJob.Version, data.Schedules[0].Version)
}
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
var data endpointEdgeStatusInspectResponse
if err := json.NewDecoder(rec.Body).Decode(&data); err != nil {
t.Fatal("error decoding response:", err)
}
assert.Len(t, data.Schedules, 1)
assert.Equal(t, edgeJob.ID, data.Schedules[0].ID)
assert.Equal(t, edgeJob.CronExpression, data.Schedules[0].CronExpression)
assert.Equal(t, edgeJob.Version, data.Schedules[0].Version)
f(endpoint, 1)
f(endpointFromStaticEdgeGroup, 1)
f(endpointFromDynamicEdgeGroup, 1)
f(unrelatedEndpoint, 0)
}

View File

@@ -2,7 +2,6 @@ package endpoints
import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
@@ -33,7 +32,7 @@ type endpointDeleteBatchPartialResponse struct {
func (payload *endpointDeleteBatchPayload) Validate(r *http.Request) error {
if payload == nil || len(payload.Endpoints) == 0 {
return fmt.Errorf("invalid request payload. You must provide a list of environments to delete")
return errors.New("invalid request payload. You must provide a list of environments to delete")
}
return nil

View File

@@ -137,7 +137,7 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Add("Authorization", "Bearer "+token)
resp, err := httpClient.Do(req)
if err != nil {

View File

@@ -202,7 +202,7 @@ func setupEndpointListHandler(t *testing.T, endpoints []portainer.Endpoint) *Han
}
func buildEndpointListRequest(query string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
req := httptest.NewRequest(http.MethodGet, "/endpoints?"+query, nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)

View File

@@ -557,7 +557,7 @@ func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.En
}
func getArrayQueryParameter(r *http.Request, parameter string) []string {
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
list, exists := r.Form[parameter+"[]"]
if !exists {
list = []string{}
}
@@ -576,7 +576,6 @@ func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]
number, err := strconv.Atoi(item)
if err != nil {
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
}
result = append(result, T(number))

View File

@@ -83,7 +83,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.22.0
// @version 2.23.0
// @description.markdown api-description.md
// @termsOfService

View File

@@ -1,7 +1,6 @@
package helm
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -46,7 +45,7 @@ func Test_helmDelete(t *testing.T) {
h.helmPackageManager.Install(options)
t.Run("helmDelete succeeds with admin user", func(t *testing.T) {
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/1/kubernetes/helm/%s", options.Name), nil)
req := httptest.NewRequest(http.MethodDelete, "/1/kubernetes/helm/"+options.Name, nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
testhelpers.AddTestSecurityCookie(req, "Bearer dummytoken")

View File

@@ -1,7 +1,6 @@
package helm
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -26,7 +25,7 @@ func Test_helmRepoSearch(t *testing.T) {
for _, repo := range repos {
t.Run(repo, func(t *testing.T) {
repoUrlEncoded := url.QueryEscape(repo)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
req := httptest.NewRequest(http.MethodGet, "/templates/helm?repo="+repoUrlEncoded, nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
@@ -41,7 +40,7 @@ func Test_helmRepoSearch(t *testing.T) {
t.Run("fails on invalid URL", func(t *testing.T) {
repo := "abc.com"
repoUrlEncoded := url.QueryEscape(repo)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/templates/helm?repo=%s", repoUrlEncoded), nil)
req := httptest.NewRequest(http.MethodGet, "/templates/helm?repo="+repoUrlEncoded, nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)

View File

@@ -2,7 +2,6 @@ package openamt
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@@ -37,7 +36,7 @@ func (handler *Handler) openAMTActivate(w http.ResponseWriter, r *http.Request)
} else if err != nil {
return httperror.InternalServerError("Unable to find an endpoint with the specified identifier inside the database", err)
} else if !endpointutils.IsAgentEndpoint(endpoint) {
errMsg := fmt.Sprintf("%s is not an agent environment", endpoint.Name)
errMsg := endpoint.Name + " is not an agent environment"
return httperror.BadRequest(errMsg, errors.New(errMsg))
}
@@ -46,8 +45,7 @@ func (handler *Handler) openAMTActivate(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
err = handler.activateDevice(endpoint, *settings)
if err != nil {
if err := handler.activateDevice(endpoint, *settings); err != nil {
return httperror.InternalServerError("Unable to activate device", err)
}
@@ -63,8 +61,7 @@ func (handler *Handler) openAMTActivate(w http.ResponseWriter, r *http.Request)
}
endpoint.AMTDeviceGUID = hostInfo.UUID
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
if err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
return httperror.InternalServerError("Unable to persist environment changes inside the database", err)
}

View File

@@ -13,7 +13,7 @@ import (
// @id GetApplicationsResources
// @summary Get the total resource requests and limits of all applications
// @description Get the total CPU (cores) and memory requests (MB) and limits of all applications across all namespaces.
// @description Get the total CPU (cores) and memory (bytes) requests and limits of all applications across all namespaces.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt

View File

@@ -172,13 +172,12 @@ func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoin
}
func buildClusterName(endpointName string) string {
return fmt.Sprintf("portainer-cluster-%s", endpointName)
return "portainer-cluster-" + endpointName
}
func buildContext(serviceAccountName string, endpoint portainer.Endpoint) clientV1.NamedContext {
contextName := fmt.Sprintf("portainer-ctx-%s", endpoint.Name)
return clientV1.NamedContext{
Name: contextName,
Name: "portainer-ctx-" + endpoint.Name,
Context: clientV1.Context{
AuthInfo: serviceAccountName,
Cluster: buildClusterName(endpoint.Name),

View File

@@ -185,7 +185,7 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http
}
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to create the namespace")
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the CreateKubernetesNamespace operation, unable to create the namespace: %s", namespaceName), err)
return httperror.InternalServerError("an error occurred during the CreateKubernetesNamespace operation, unable to create the namespace: "+namespaceName, err)
}
return response.JSON(w, namespace)
@@ -217,15 +217,14 @@ func (handler *Handler) deleteKubernetesNamespace(w http.ResponseWriter, r *http
}
for _, namespaceName := range *namespaceNames {
_, err := cli.DeleteNamespace(namespaceName)
if err != nil {
if _, err := cli.DeleteNamespace(namespaceName); err != nil {
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to find the namespace")
return httperror.NotFound(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to find the namespace. Error: ", namespaceName), err)
return httperror.NotFound("an error occurred during the DeleteKubernetesNamespace operation for the namespace "+namespaceName+", unable to find the namespace. Error: ", err)
}
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to delete the namespace")
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to delete the Kubernetes namespace. Error: ", namespaceName), err)
return httperror.InternalServerError("an error occurred during the DeleteKubernetesNamespace operation for the namespace "+namespaceName+", unable to delete the Kubernetes namespace. Error: ", err)
}
}
@@ -262,8 +261,7 @@ func (payload deleteKubernetesNamespacePayload) Validate(r *http.Request) error
// @router /kubernetes/{id}/namespaces/{namespace} [put]
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
payload := models.K8sNamespaceDetails{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("an error occurred during the UpdateKubernetesNamespace operation, invalid request payload. Error: ", err)
}

View File

@@ -1,7 +1,7 @@
package websocket
import (
"fmt"
"errors"
"io"
"net/http"
"strings"
@@ -69,8 +69,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to find the environment associated to the stack inside the database", err)
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
}
@@ -87,15 +86,13 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
r.Header.Del("Origin")
if endpoint.Type == portainer.AgentOnKubernetesEnvironment {
err := handler.proxyAgentWebsocketRequest(w, r, params)
if err != nil {
if err := handler.proxyAgentWebsocketRequest(w, r, params); err != nil {
return httperror.InternalServerError("Unable to proxy websocket request to agent", err)
}
return nil
} else if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err := handler.proxyEdgeAgentWebsocketRequest(w, r, params)
if err != nil {
if err := handler.proxyEdgeAgentWebsocketRequest(w, r, params); err != nil {
return httperror.InternalServerError("Unable to proxy websocket request to Edge agent", err)
}
@@ -187,7 +184,7 @@ func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endp
}
if token == "" {
return "", false, fmt.Errorf("can not get a valid user service account token")
return "", false, errors.New("can not get a valid user service account token")
}
return token, false, nil

View File

@@ -2,7 +2,6 @@ package websocket
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
@@ -34,7 +33,7 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
endpointURL := params.endpoint.URL
if params.endpoint.Type == portainer.AgentOnKubernetesEnvironment {
endpointURL = fmt.Sprintf("http://%s", params.endpoint.URL)
endpointURL = "http://" + params.endpoint.URL
}
agentURL, err := url.Parse(endpointURL)

View File

@@ -22,13 +22,11 @@ type K8sResourceQuota struct {
func (r *K8sNamespaceDetails) Validate(request *http.Request) error {
if r.ResourceQuota != nil && r.ResourceQuota.Enabled {
_, err := resource.ParseQuantity(r.ResourceQuota.Memory)
if err != nil {
if _, err := resource.ParseQuantity(r.ResourceQuota.Memory); err != nil {
return fmt.Errorf("error parsing memory quota value: %w", err)
}
_, err = resource.ParseQuantity(r.ResourceQuota.CPU)
if err != nil {
if _, err := resource.ParseQuantity(r.ResourceQuota.CPU); err != nil {
return fmt.Errorf("error parsing cpu quota value: %w", err)
}
}

View File

@@ -38,6 +38,7 @@ type (
VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode"`
OwningApplications []K8sApplication `json:"owningApplications,omitempty"`
Phase corev1.PersistentVolumeClaimPhase `json:"phase"`
Labels map[string]string `json:"labels"`
}
K8sStorageClass struct {

View File

@@ -2,7 +2,6 @@ package azure
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@@ -40,7 +39,7 @@ func (transport *Transport) proxyContainerGroupPutRequest(request *http.Request)
Method: http.MethodGet,
URL: request.URL,
Header: http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", tokenData.Token)},
"Authorization": []string{"Bearer " + tokenData.Token},
},
}

View File

@@ -1,7 +1,7 @@
package azure
import (
"fmt"
"errors"
"net/http"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
@@ -41,7 +41,7 @@ func (transport *Transport) proxyContainerGroupsGetRequest(request *http.Request
utils.RewriteResponse(response, responseObject, http.StatusOK)
} else {
return nil, fmt.Errorf("The container groups response has no value property")
return nil, errors.New("The container groups response has no value property")
}
return response, nil

View File

@@ -120,7 +120,7 @@ func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string,
return "", err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
request.Header.Set("Authorization", "Bearer "+token)
return token, nil
}

View File

@@ -1,7 +1,6 @@
package security
import (
"fmt"
"net/http"
"strings"
"sync"
@@ -426,7 +425,7 @@ func (bouncer *RequestBouncer) apiKeyLookup(r *http.Request) (*portainer.TokenDa
}
if _, _, err := bouncer.jwtService.GenerateToken(tokenData); err != nil {
log.Debug().Err(err).Msg("Failed to generate token")
return nil, fmt.Errorf("failed to generate token")
return nil, errors.New("failed to generate token")
}
if now := time.Now().UTC().Unix(); now-apiKey.LastUsed > 60 { // [seconds]

View File

@@ -1,27 +0,0 @@
package edge
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
// LoadEdgeJobs registers all edge jobs inside corresponding environment(endpoint) tunnel
func LoadEdgeJobs(dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().ReadAll()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
endpoint, err := dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
reverseTunnelService.AddEdgeJob(endpoint, &edgeJob)
}
}
return nil
}

View File

@@ -1,8 +1,12 @@
package edge
import (
"slices"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Environment(Endpoint)
@@ -39,3 +43,33 @@ func EffectiveCheckinInterval(tx dataservices.DataStoreTx, endpoint *portainer.E
return portainer.DefaultEdgeAgentCheckinIntervalInSeconds
}
// EndpointInEdgeGroup returns true and the edge group name if the endpoint is in the edge group
func EndpointInEdgeGroup(
tx dataservices.DataStoreTx,
endpointID portainer.EndpointID,
edgeGroupID portainer.EdgeGroupID,
) (bool, string, error) {
endpointIDs, err := GetEndpointsFromEdgeGroups(
[]portainer.EdgeGroupID{edgeGroupID}, tx,
)
if err != nil {
return false, "", err
}
if slices.Contains(endpointIDs, endpointID) {
edgeGroup, err := tx.EdgeGroup().Read(edgeGroupID)
if err != nil {
log.Warn().
Err(err).
Int("edgeGroupID", int(edgeGroupID)).
Msg("Unable to retrieve edge group")
return false, "", err
}
return true, edgeGroup.Name, nil
}
return false, "", nil
}

View File

@@ -1,7 +1,7 @@
package endpointutils
import (
"fmt"
"errors"
"strings"
"time"
@@ -171,7 +171,7 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
} else if len(storage) == 0 {
log.Info().Err(err).Msg("zero storage classes found: they may be still building, retrying in 30 seconds")
return fmt.Errorf("zero storage classes found: they may be still building, retrying in 30 seconds")
return errors.New("zero storage classes found: they may be still building, retrying in 30 seconds")
}
endpoint.Kubernetes.Configuration.StorageClasses = storage

View File

@@ -1,7 +1,7 @@
package access
import (
"fmt"
"errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
@@ -52,7 +52,7 @@ func GetAccessibleRegistry(
}
if !hasPermission {
err = fmt.Errorf("user does not has permission to get the registry")
err = errors.New("user does not has permission to get the registry")
return nil, err
}

View File

@@ -2,7 +2,6 @@ package cli
import (
"context"
"fmt"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/rs/zerolog/log"
@@ -28,7 +27,7 @@ func (kcl *KubeClient) GetApplications(namespace, nodeName string, withDependenc
func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
podListOptions := metav1.ListOptions{}
if nodeName != "" {
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
podListOptions.FieldSelector = "spec.nodeName=" + nodeName
}
if !withDependencies {
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
@@ -59,7 +58,7 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
podListOptions := metav1.ListOptions{}
if nodeName != "" {
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
podListOptions.FieldSelector = "spec.nodeName=" + nodeName
}
if !withDependencies {
@@ -125,7 +124,7 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
resource := models.K8sApplicationResource{}
podListOptions := metav1.ListOptions{}
if node != "" {
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", node)
podListOptions.FieldSelector = "spec.nodeName=" + node
}
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
@@ -134,27 +133,16 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
}
for _, pod := range pods.Items {
for _, container := range pod.Spec.Containers {
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue())
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue())
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
}
podResources := calculateResourceUsage(pod)
resource.CPURequest += podResources.CPURequest
resource.CPULimit += podResources.CPULimit
resource.MemoryRequest += podResources.MemoryRequest
resource.MemoryLimit += podResources.MemoryLimit
}
return resource, nil
}
// convertApplicationResourceUnits converts the resource units from milli to core and bytes to mega bytes
func convertApplicationResourceUnits(resource models.K8sApplicationResource) models.K8sApplicationResource {
return models.K8sApplicationResource{
CPURequest: resource.CPURequest / 1000,
CPULimit: resource.CPULimit / 1000,
MemoryRequest: resource.MemoryRequest / 1024 / 1024,
MemoryLimit: resource.MemoryLimit / 1024 / 1024,
}
}
// GetApplicationsFromConfigMap gets a list of applications that use a specific ConfigMap
// by checking all pods in the same namespace as the ConfigMap
func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
@@ -352,16 +340,18 @@ func updateApplicationWithService(application models.K8sApplication, services []
return application
}
// calculateResourceUsage calculates the resource usage for a pod
// calculateResourceUsage calculates the resource usage for a pod in CPU cores and Bytes
func calculateResourceUsage(pod corev1.Pod) models.K8sApplicationResource {
resource := models.K8sApplicationResource{}
for _, container := range pod.Spec.Containers {
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue())
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue())
// CPU cores as a decimal
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue()) / 1000
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue()) / 1000
// Bytes
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
}
return convertApplicationResourceUnits(resource)
return resource
}
// GetApplicationFromServiceSelector gets applications based on service selectors

View File

@@ -273,7 +273,7 @@ func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*re
func (factory *ClientFactory) CreateRemoteMetricsClient(endpoint *portainer.Endpoint) (*metricsv.Clientset, error) {
config, err := factory.CreateConfig(endpoint)
if err != nil {
return nil, fmt.Errorf("failed to create metrics KubeConfig")
return nil, errors.New("failed to create metrics KubeConfig")
}
return metricsv.NewForConfig(config)
}

View File

@@ -2,7 +2,7 @@ package cli
import (
"context"
"fmt"
"errors"
models "github.com/portainer/portainer/api/http/models/kubernetes"
rbacv1 "k8s.io/api/rbac/v1"
@@ -16,7 +16,7 @@ func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) {
return kcl.fetchClusterRoles()
}
return []models.K8sClusterRole{}, fmt.Errorf("non-admin users are not allowed to access cluster roles")
return []models.K8sClusterRole{}, errors.New("non-admin users are not allowed to access cluster roles")
}
// fetchClusterRoles returns a list of all Roles in the specified namespace.

View File

@@ -2,7 +2,7 @@ package cli
import (
"context"
"fmt"
"errors"
models "github.com/portainer/portainer/api/http/models/kubernetes"
rbacv1 "k8s.io/api/rbac/v1"
@@ -16,7 +16,7 @@ func (kcl *KubeClient) GetClusterRoleBindings() ([]models.K8sClusterRoleBinding,
return kcl.fetchClusterRoleBindings()
}
return []models.K8sClusterRoleBinding{}, fmt.Errorf("non-admin users are not allowed to access cluster role bindings")
return []models.K8sClusterRoleBinding{}, errors.New("non-admin users are not allowed to access cluster role bindings")
}
// fetchClusterRoleBindings returns a list of all cluster roles in the cluster.

View File

@@ -135,6 +135,7 @@ func parsePersistentVolumeClaim(volume *corev1.PersistentVolumeClaim) models.K8s
VolumeMode: volume.Spec.VolumeMode,
OwningApplications: nil,
Phase: volume.Status.Phase,
Labels: volume.Labels,
}
}

View File

@@ -1568,10 +1568,6 @@ type (
TunnelAddr(endpoint *Endpoint) (string, error)
UpdateLastActivity(endpointID EndpointID)
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
EdgeJobs(endpointId EndpointID) []EdgeJob
AddEdgeJob(endpoint *Endpoint, edgeJob *EdgeJob)
RemoveEdgeJob(edgeJobID EdgeJobID)
RemoveEdgeJobFromEndpoint(endpointID EndpointID, edgeJobID EdgeJobID)
}
// Server defines the interface to serve the API
@@ -1599,7 +1595,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.22.0"
APIVersion = "2.23.0"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -3,7 +3,6 @@ package scheduler
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
@@ -59,7 +58,7 @@ func Test_JobShouldStop_UponPermError(t *testing.T) {
s.StartJobEvery(jobInterval, func() error {
acc++
close(ch)
return NewPermanentError(fmt.Errorf("failed"))
return NewPermanentError(errors.New("failed"))
})
<-time.After(3 * jobInterval)
@@ -76,7 +75,7 @@ func Test_JobShouldNotStop_UponError(t *testing.T) {
s.StartJobEvery(jobInterval, func() error {
if acc.Add(1) == 2 {
close(ch)
return NewPermanentError(fmt.Errorf("failed"))
return NewPermanentError(errors.New("failed"))
}
return errors.New("non-permanent error")

View File

@@ -1,6 +1,7 @@
package deployments
import (
"cmp"
"crypto/tls"
"fmt"
"strconv"
@@ -43,21 +44,19 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
// Webhook
if stack.AutoUpdate != nil && stack.AutoUpdate.Webhook != "" {
return redeployWhenChanged(stack, deployer, datastore, gitService)
return redeployWhenChanged(stack, deployer, datastore, gitService, true)
}
// Polling
_, err, _ = singleflightGroup.Do(strconv.Itoa(int(stackID)), func() (any, error) {
return nil, redeployWhenChanged(stack, deployer, datastore, gitService)
return nil, redeployWhenChanged(stack, deployer, datastore, gitService, false)
})
return err
}
func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) error {
stackID := stack.ID
log.Debug().Int("stack_id", int(stackID)).Msg("redeploying stack")
func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService, webhook bool) error {
log.Debug().Int("stack_id", int(stack.ID)).Msg("redeploying stack")
if stack.GitConfig == nil {
return nil // do nothing if it isn't a git-based stack
@@ -76,17 +75,14 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
}
author := stack.UpdatedBy
if author == "" {
author = stack.CreatedBy
}
author := cmp.Or(stack.UpdatedBy, stack.CreatedBy)
user, err := datastore.User().UserByUsername(author)
if err != nil {
log.Warn().
Int("stack_id", int(stackID)).
Str("author", author).
Int("stack_id", int(stack.ID)).
Str("stack", stack.Name).
Str("author", author).
Int("endpoint_id", int(stack.EndpointID)).
Msg("cannot auto update a stack, stack author user is missing")
@@ -97,9 +93,36 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
return nil
}
if webhook {
go func() {
if err := redeployWhenChangedSecondStage(stack, deployer, datastore, gitService, user, endpoint); err != nil {
log.Error().Err(err).
Int("stack_id", int(stack.ID)).
Str("stack", stack.Name).
Str("author", author).
Int("endpoint_id", int(stack.EndpointID)).
Msg("webhook failed to redeploy a stack")
}
}()
return nil
}
return redeployWhenChangedSecondStage(stack, deployer, datastore, gitService, user, endpoint)
}
func redeployWhenChangedSecondStage(
stack *portainer.Stack,
deployer StackDeployer,
datastore dataservices.DataStore,
gitService portainer.GitService,
user *portainer.User,
endpoint *portainer.Endpoint,
) error {
var gitCommitChangedOrForceUpdate bool
if !stack.FromAppTemplate {
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, false, false, stack.ProjectPath)
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stack.ID), stack.GitConfig, false, false, stack.ProjectPath)
if err != nil {
return err
}
@@ -124,7 +147,6 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
switch stack.Type {
case portainer.DockerComposeStack:
if stackutils.IsRelativePathStack(stack) {
err = deployer.DeployRemoteComposeStack(stack, endpoint, registries, true, false)
} else {
@@ -132,7 +154,7 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
}
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stack.ID)
}
case portainer.DockerSwarmStack:
if stackutils.IsRelativePathStack(stack) {
@@ -141,16 +163,13 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
err = deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
}
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stack.ID)
}
case portainer.KubernetesStack:
log.Debug().
Int("stack_id", int(stackID)).
Msg("deploying a kube app")
log.Debug().Int("stack_id", int(stack.ID)).Msg("deploying a kube app")
err := deployer.DeployKubernetesStack(stack, endpoint, user)
if err != nil {
return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stackID)
if err := deployer.DeployKubernetesStack(stack, endpoint, user); err != nil {
return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stack.ID)
}
default:
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)

View File

@@ -31,9 +31,7 @@ import {
KubernetesPodNodeAffinityPayload,
KubernetesPreferredSchedulingTermPayload,
} from 'Kubernetes/pod/payloads/affinities';
export const PodKubernetesInstanceLabel = 'app.kubernetes.io/instance';
export const PodManagedByLabel = 'app.kubernetes.io/managed-by';
import { PodKubernetesInstanceLabel, PodManagedByLabel } from '@/react/kubernetes/applications/constants';
class KubernetesApplicationHelper {
/* #region UTILITY FUNCTIONS */

View File

@@ -15,8 +15,6 @@ export const applicationsModule = angular
'namespaces',
'onNamespaceChange',
'onRefresh',
'showSystem',
'onShowSystemChange',
'onRemove',
'hideStacks',
])

View File

@@ -215,8 +215,6 @@ export const ngModule = angular
'namespace',
'namespaces',
'onNamespaceChange',
'showSystem',
'setSystemResources',
])
)
.component(

View File

@@ -18,8 +18,6 @@
namespace="ctrl.state.namespaceName"
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
is-loading="ctrl.state.isAppsLoading"
show-system="ctrl.state.isSystemResources"
on-show-system-change="(ctrl.setSystemResources)"
on-remove="(ctrl.removeAction)"
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
>
@@ -50,8 +48,6 @@
namespace="ctrl.state.namespaceName"
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
is-loading="ctrl.state.isAppsLoading"
show-system="ctrl.state.isSystemResources"
on-show-system-change="(ctrl.setSystemResources)"
on-remove="(ctrl.removeAction)"
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
>

View File

@@ -42,7 +42,6 @@ class KubernetesApplicationsController {
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
this.setSystemResources = this.setSystemResources.bind(this);
}
selectTab(index) {
@@ -133,12 +132,6 @@ class KubernetesApplicationsController {
});
}
setSystemResources(flag) {
return this.$scope.$applyAsync(() => {
this.state.isSystemResources = flag;
});
}
async onInit() {
this.state = {
activeTab: this.LocalStorage.getActiveTab('applications'),
@@ -150,7 +143,6 @@ class KubernetesApplicationsController {
ports: [],
namespaces: [],
namespaceName: '',
isSystemResources: undefined,
};
this.deploymentOptions = await getDeploymentOptions();

View File

@@ -363,7 +363,9 @@ class KubernetesNodeController {
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
};
await Promise.allSettled([this.getNodes(), this.getEvents(), this.getEndpoints(), this.getNodeUsage()]);
// getEvents depends on nodes, so get nodes first
await this.getNodes();
await Promise.allSettled([this.getEvents(), this.getEndpoints(), this.getNodeUsage()]);
this.availableEffects = _.values(KubernetesNodeTaintEffects);
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
@@ -371,7 +373,6 @@ class KubernetesNodeController {
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
this.resourceReservation = await getTotalResourcesForAllApplications(this.$state.params.endpointId, this.node.Name);
this.resourceReservation.CpuRequest = Math.round(this.resourceReservation.CpuRequest / 1000);
this.resourceReservation.MemoryRequest = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.MemoryRequest);
this.node.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);

View File

@@ -6,7 +6,7 @@ import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { confirmRedeploy } from '@/react/kubernetes/volumes/ItemView/ConfirmRedeployModal';
import { isVolumeUsed, isVolumeExternal } from '@/react/kubernetes/volumes/utils';
import { isVolumeUsed } from '@/react/kubernetes/volumes/utils';
class KubernetesVolumeController {
/* @ngInject */
@@ -50,7 +50,7 @@ class KubernetesVolumeController {
}
isExternalVolume() {
return isVolumeExternal(this.volume);
return !this.volume.PersistentVolumeClaim.ApplicationOwner;
}
isSystemNamespace() {

View File

@@ -1,8 +1,27 @@
/* @ngInject */
export default function CodeEditorController($scope) {
this.type = '';
this.handleChange = (value) => {
$scope.$evalAsync(() => {
this.onChange(value);
});
};
this.$onInit = () => {
this.type = getType({ yaml: this.yml, dockerFile: this.dockerFile, shell: this.shell });
};
}
function getType({ yaml, dockerFile, shell }) {
if (yaml) {
return 'yaml';
}
if (dockerFile) {
return 'dockerfile';
}
if (shell) {
return 'shell';
}
return undefined;
}

View File

@@ -1,9 +1,7 @@
<react-code-editor
id="$ctrl.identifier"
placeholder="$ctrl.placeholder"
yaml="$ctrl.yml"
docker-file="$ctrl.dockerFile"
shell="$ctrl.shell"
type="$ctrl.type"
readonly="$ctrl.readOnly"
on-change="($ctrl.handleChange)"
value="$ctrl.value"

View File

@@ -223,9 +223,7 @@ export const ngModule = angular
r2a(CodeEditor, [
'id',
'placeholder',
'yaml',
'dockerFile',
'shell',
'type',
'readonly',
'onChange',
'value',

View File

@@ -15,12 +15,11 @@ import styles from './CodeEditor.module.css';
import { TextTip } from './Tip/TextTip';
import { StackVersionSelector } from './StackVersionSelector';
type Type = 'yaml' | 'shell' | 'dockerfile';
interface Props extends AutomationTestingProps {
id: string;
placeholder?: string;
yaml?: boolean;
dockerFile?: boolean;
shell?: boolean;
type?: Type;
readonly?: boolean;
onChange: (value: string) => void;
value: string;
@@ -62,6 +61,12 @@ const dockerFileLanguage = new LanguageSupport(
);
const shellLanguage = new LanguageSupport(StreamLanguage.define(shell));
const docTypeExtensionMap: Record<Type, LanguageSupport> = {
yaml: yamlLanguage,
dockerfile: dockerFileLanguage,
shell: shellLanguage,
};
export function CodeEditor({
id,
onChange,
@@ -71,26 +76,18 @@ export function CodeEditor({
versions,
onVersionChange,
height = '500px',
yaml: isYaml,
dockerFile: isDockerFile,
shell: isShell,
type,
'data-cy': dataCy,
}: Props) {
const [isRollback, setIsRollback] = useState(false);
const extensions = useMemo(() => {
const extensions = [];
if (isYaml) {
extensions.push(yamlLanguage);
}
if (isDockerFile) {
extensions.push(dockerFileLanguage);
}
if (isShell) {
extensions.push(shellLanguage);
if (type && docTypeExtensionMap[type]) {
extensions.push(docTypeExtensionMap[type]);
}
return extensions;
}, [isYaml, isDockerFile, isShell]);
}, [type]);
function handleVersionChange(version: number) {
if (versions && versions.length > 1) {

View File

@@ -1,8 +1,13 @@
import { PropsWithChildren, useEffect, useMemo } from 'react';
import {
ComponentProps,
PropsWithChildren,
ReactNode,
useEffect,
useMemo,
} from 'react';
import { useTransitionHook } from '@uirouter/react';
import { BROWSER_OS_PLATFORM } from '@/react/constants';
import { AutomationTestingProps } from '@/types';
import { CodeEditor } from '@@/CodeEditor';
import { Tooltip } from '@@/Tip/Tooltip';
@@ -52,39 +57,21 @@ export const editorConfig = {
win: otherEditorConfig,
} as const;
interface Props extends AutomationTestingProps {
value: string;
onChange: (value: string) => void;
type CodeEditorProps = ComponentProps<typeof CodeEditor>;
id: string;
placeholder?: string;
yaml?: boolean;
shell?: boolean;
readonly?: boolean;
titleContent?: React.ReactNode;
interface Props extends CodeEditorProps {
titleContent?: ReactNode;
hideTitle?: boolean;
error?: string;
versions?: number[];
onVersionChange?: (version: number) => void;
height?: string;
}
export function WebEditorForm({
id,
onChange,
placeholder,
value,
titleContent = '',
hideTitle,
readonly,
yaml,
shell,
children,
error,
versions,
onVersionChange,
height,
'data-cy': dataCy,
...props
}: PropsWithChildren<Props>) {
return (
<div>
@@ -107,16 +94,8 @@ export function WebEditorForm({
<div className="col-sm-12 col-lg-12">
<CodeEditor
id={id}
placeholder={placeholder}
readonly={readonly}
yaml={yaml}
shell={shell}
value={value}
onChange={onChange}
versions={versions}
onVersionChange={(v) => onVersionChange && onVersionChange(v)}
height={height}
data-cy={dataCy}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</div>
</div>

View File

@@ -87,7 +87,7 @@ function InnerForm({ isLoading }: { isLoading: boolean }) {
onChange={(value) => setFieldValue('fileContent', value)}
value={values.fileContent}
placeholder="Define or paste the content of your script file here"
shell
type="shell"
error={errors.fileContent}
/>
)}

View File

@@ -83,7 +83,7 @@ function InnerForm({ isLoading }: { isLoading: boolean }) {
onChange={(value) => setFieldValue('fileContent', value)}
value={values.fileContent}
placeholder="Define or paste the content of your script file here"
shell
type="shell"
error={errors.fileContent}
/>

View File

@@ -16,7 +16,7 @@ export function DockerContentField({
id="stack-creation-editor"
value={value}
onChange={onChange}
yaml
type="yaml"
placeholder="Define or paste the content of your docker compose file here"
error={error}
readonly={readonly}

View File

@@ -72,7 +72,7 @@ export function KubeManifestForm({
id="stack-creation-editor"
value={values.fileContent}
onChange={(value) => handleChange({ fileContent: value })}
yaml
type="yaml"
placeholder="Define or paste the content of your manifest file here"
error={errors?.fileContent}
data-cy="stack-creation-editor"

View File

@@ -60,7 +60,7 @@ export function ComposeForm({
<WebEditorForm
data-cy="compose-editor"
value={values.content}
yaml
type="yaml"
id="compose-editor"
placeholder="Define or paste the content of your docker compose file here"
onChange={(value) => handleContentChange(DeploymentType.Compose, value)}

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