Compare commits

...

64 Commits

Author SHA1 Message Date
andres-portainer
2c8434c609 fix(compose): fix support for ECR BE-11392 (#150) 2024-11-20 08:49:42 +13:00
andres-portainer
02c006be8a fix(stacks): pass the registry credentials to Compose stacks BE-11388 (#148)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2024-11-18 08:39:16 +13:00
andres-portainer
60a2696a8d fix(libstack): add missing private registry credentials BE-11388 (#144) 2024-11-15 17:39:00 -03:00
Oscar Zhou
64b5d1df2d fix(swarm): failed to deploy app template [BE-11385] (#135) 2024-11-15 11:19:33 +13:00
andres-portainer
025a409ab5 fix(compose): avoid leftovers in Run() BE-11381 (#130) 2024-11-13 20:24:14 -03:00
andres-portainer
9b65f01748 feat(edgestacks): add a retry period to edge stack deployments BE-11155 (#128)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-11-13 20:22:07 -03:00
andres-portainer
e112ddfbeb fix(libstack): fix compose run BE-11381 (#127) 2024-11-13 14:38:44 -03:00
LP B
707fc91a32 fix(edge/stacks): use default namespace when none is specified in manifest (#125) 2024-11-13 16:30:16 +13:00
andres-portainer
b2eb4388fd fix(libstack): add a different timeout for WaitForStatus BE-11376 (#119) 2024-11-12 19:30:42 -03:00
andres-portainer
5a451b2035 fix(compose): provide the project name for proper validation BE-11375 (#117) 2024-11-12 17:18:36 -03:00
Oscar Zhou
370d224d76 fix(libstack): empty project name [BE-10801] (#115) 2024-11-12 10:20:41 -03:00
Ali
b4c36b0e48 fix(configmap): create portainer configmap if it doesn't exist [r8s-141] #113 (#114) 2024-11-12 18:34:58 +13:00
Oscar Zhou
e6508140f8 version: bump version to 2.24.0 (#102) 2024-11-12 12:13:27 +13:00
andres-portainer
a7127bc74f feat(libstack): remove the docker-compose binary BE-10801 (#111)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-11-11 19:05:56 -03:00
Malcolm Lockyer
55aa0c0c5d fix(ui): kubernetes create from file page - fix template load failed mistake in ce (#112) 2024-11-12 10:46:37 +13:00
Ali
d25de4f459 fix(more-resources): address CE review comments [r8s-103] (#110) 2024-11-12 10:41:43 +13:00
Yajith Dayarathna
6d31f4876a fix(more resources): fix porting and functionality [r8s-103] (#8)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2024-11-12 09:55:30 +13:00
Steven Kang
e6577ca269 kubernetes: improved the node view [r8s-47] (#108) 2024-11-12 09:42:14 +13:00
Ali
08d77b4333 fix(namespace): handle no accesses found [r8s-141] (#106) 2024-11-12 09:29:55 +13:00
Ali
1ead121c9b fix(apps): for helm uninstall, ignore manual associated resource deletion [r8s-124] (#103) 2024-11-12 09:03:22 +13:00
LP B
ad19b4a421 fix(app): relocate Skip TLS switch next to git repo URL field (#107) 2024-11-11 17:16:37 +01:00
LP B
6bc52dd39c feat(edge): kubernetes WaitForStatus support (#85) 2024-11-11 14:02:20 +01:00
Malcolm Lockyer
fd2b00bf3b fix(ui): kubernetes create from file page - fix template load failed message style [R8S-68] (#95) 2024-11-11 12:06:56 +13:00
Ali
cd8c6d1ce0 fix(apps): don't delete the 'kubernetes' service or duplicate service names [r8s-124] (#90) 2024-11-11 08:26:56 +13:00
Ali
e9fc6d5598 refactor(namespace): migrate namespace access view to react [r8s-141] (#87) 2024-11-11 08:17:20 +13:00
Steven Kang
8ed7cd80cb feat(ui): improve Kubernetes node view [r8s-47] (#84) 2024-11-07 14:10:19 +13:00
Malcolm Lockyer
81322664ea fix(ui): kubernetes create from manifest page misalignments and incorrect loading icon [R8S-68] (#88) 2024-11-07 09:04:24 +13:00
Ali
458d722d47 fix(ui): consistent widget padding [r8s-136] (#82) 2024-11-05 14:25:40 +13:00
Malcolm Lockyer
3c0d25f3bd fix(ui): rename create from manifest to create from file [BE-11335] (#86) 2024-11-05 14:10:08 +13:00
Oscar Zhou
ca7e4dd66e fix(edge/async): onboarding agent without predefined group cannot be associated [BE-11281] (#83) 2024-11-05 09:32:25 +13:00
Ali
c1316532eb fix(apps): update associated resources on deletion [r8s-124] (#75) 2024-11-01 21:03:49 +13:00
Ali
d418784346 fix(rbac): revert rbac detection logic [r8s-137] (#81) 2024-11-01 19:28:23 +13:00
andres-portainer
1061601714 feat(activity-log): set descending timestamps as the default sorting order BE-11343 (#66) 2024-10-31 18:07:26 -03:00
andres-portainer
2f3d4a5511 fix(activity-log): fix broken sorting BE-11342 (#65) 2024-10-31 17:25:38 -03:00
LP B
9ea62bda28 fix(app/image-details): export images to tar (#40) 2024-10-31 17:40:01 +01:00
Steven Kang
94b1d446c0 fix(ingresses): load cluster wide ingresses [r8s-78] (#78) 2024-10-31 13:08:09 +13:00
Ali
6c57a00a65 fix(cluster): UI RBAC alert fix [r8s-138] (#72) 2024-10-31 10:12:56 +13:00
Yajith Dayarathna
8808531cd5 update ci trigger paths for portainer-ee - develop (#68) 2024-10-29 12:23:31 +13:00
andres-portainer
966fca950b fix(oauth): add a timeout to getOAuthToken() BE-11283 (#63) 2024-10-28 17:28:22 -03:00
Yajith Dayarathna
e528cff615 bump golang version to 1.23.2 (#60) 2024-10-29 09:02:18 +13:00
andres-portainer
1d037f2f1f feat(websocket): improve websocket code sharing BE-11340 (#61) 2024-10-25 11:21:49 -03:00
James Carppe
b2d67795b3 Update bug report template for 2.21.4 (#62) 2024-10-25 15:49:31 +13:00
Ali
959c527be7 refactor(apps): migrate applications view to react [r8s-124] (#28) 2024-10-25 12:28:05 +13:00
andres-portainer
cc75167437 fix(swarm): fix service updates BE-11219 (#57) 2024-10-23 18:23:24 -03:00
andres-portainer
3114d4b5c5 fix(security): add initial support for HSTS and CSP BE-11311 (#47) 2024-10-21 13:52:11 -03:00
andres-portainer
ac293cda1c feat(database): share more database code between CE and EE BE-11303 (#43) 2024-10-18 10:33:10 -03:00
Ali
7b88975bcb fix(applications): scale resource usage by pod count [r8s-127] (#33) 2024-10-16 14:33:45 +13:00
James Carppe
da4b2e3a56 Updated bug report template for 2.23.0 (#32) 2024-10-16 09:23:02 +13:00
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
364 changed files with 5911 additions and 5246 deletions

View File

@@ -93,7 +93,9 @@ body:
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.23.0'
- '2.22.0'
- '2.21.4'
- '2.21.3'
- '2.21.2'
- '2.21.1'

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,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 10
WAIT_MS: 60000

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,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,28 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

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

@@ -9,7 +9,7 @@ ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
TAG=local
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
GOTESTSUM=go run gotest.tools/gotestsum@latest
# Don't change anything below this line unless you know what you're doing
@@ -64,9 +64,6 @@ clean: ## Remove all build and download artifacts
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-deps: init-dist
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
test-client: ## Run client tests
yarn test $(ARGS)
@@ -75,11 +72,11 @@ test-server: ## Run server tests
##@ Dev
.PHONY: dev dev-client dev-server
dev: ## Run both the client and server in development mode
dev: ## Run both the client and server in development mode
make dev-server
make dev-client
dev-client: ## Run the client in development mode
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-server ## Run the server in development mode
@@ -119,7 +116,7 @@ dev-extension: build-server build-client ## Run the extension in development mod
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml

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"
@@ -50,7 +49,6 @@ import (
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/portainer/portainer/pkg/libstack"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/gofrs/uuid"
@@ -167,26 +165,6 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
}
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
return composeWrapper
}
func initSwarmStackManager(
assetsPath string,
configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
dataStore dataservices.DataStore,
) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
@@ -434,14 +412,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
dockerConfigPath := fileService.GetDockerConfigPath()
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing compose deployer")
}
composeDeployer := compose.NewComposeDeployer()
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
}
@@ -467,10 +442,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

@@ -49,8 +49,8 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
if err := connection.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
var list []any
version := make(map[string]string)
@@ -84,27 +84,22 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
return nil
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
backup[bucketName] = list
return nil
}
backup[bucketName] = list
return nil
})
return err
})
if err != nil {
}); err != nil {
return []byte("{}"), 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

@@ -1,10 +1,15 @@
{
"api_key": null,
"customtemplates": null,
"dockerhub": [
{
"Authentication": false,
"Username": ""
}
],
"edge_stack": null,
"edgegroups": null,
"edgejobs": null,
"endpoint_groups": [
{
"AuthorizedTeams": null,
@@ -103,6 +108,9 @@
"UserAccessPolicies": {}
}
],
"extension": null,
"helm_user_repository": null,
"pending_actions": null,
"registries": [
{
"Authentication": true,
@@ -602,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
"KubectlShellImage": "portainer/kubectl-shell:2.24.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -860,6 +868,8 @@
"UpdatedBy": ""
}
],
"tags": null,
"team_membership": null,
"teams": [
{
"Id": 1,
@@ -932,6 +942,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
"VERSION": "{\"SchemaVersion\":\"2.24.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}

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

@@ -31,15 +31,18 @@ type (
// RegistryCredentials holds the credentials for a Docker registry.
// Used only for EE
RegistryCredentials []RegistryCredentials
// PrePullImage is a flag indicating if the agent should pull the image before deploying the stack.
// PrePullImage is a flag indicating if the agent must pull the image before deploying the stack.
// Used only for EE
PrePullImage bool
// RePullImage is a flag indicating if the agent should pull the image if it is already present on the node.
// RePullImage is a flag indicating if the agent must pull the image if it is already present on the node.
// Used only for EE
RePullImage bool
// RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails.
// RetryDeploy is a flag indicating if the agent must retry to deploy the stack if it fails.
// Used only for EE
RetryDeploy bool
// RetryPeriod specifies the duration, in seconds, for which the agent should continue attempting to deploy the stack after a failure
// Used only for EE
RetryPeriod int
// EdgeUpdateID is the ID of the edge update related to this stack.
// Used only for EE
EdgeUpdateID int

View File

@@ -9,27 +9,32 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/portainer/portainer/pkg/libstack"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct {
deployer libstack.Deployer
proxyManager *proxy.Manager
dataStore dataservices.DataStore
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
// NewComposeStackManager returns a Compose stack manager
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
return &ComposeStackManager{
deployer: deployer,
proxyManager: proxyManager,
}, nil
dataStore: dataStore,
}
}
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
@@ -60,6 +65,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
},
ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit,
@@ -90,6 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
},
Remove: options.Remove,
Args: options.Args,
@@ -103,14 +110,15 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
} else if proxy != nil {
defer proxy.Close()
}
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
WorkingDir: "",
Host: url,
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.RemoveOptions{
Options: libstack.Options{
WorkingDir: "",
Host: url,
},
})
return errors.Wrap(err, "failed to remove a stack")
@@ -118,12 +126,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
// but does not start containers based on those images.
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
} else if proxy != nil {
defer proxy.Close()
}
@@ -138,6 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
})
return errors.Wrap(err, "failed to pull images of the stack")
}
@@ -176,16 +184,16 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
// Copy from default .env file
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
if err = copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
if err := copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
return "", err
}
// Copy from stack env vars
if err = copyConfigEnvVars(envfile, stack.Env); err != nil {
if err := copyConfigEnvVars(envfile, stack.Env); err != nil {
return "", err
}
return "stack.env", nil
return envFilePath, nil
}
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
@@ -217,3 +225,49 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
}
return nil
}
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
var authConfigs []types.AuthConfig
for _, r := range registries {
ac := types.AuthConfig{
Username: r.Username,
Password: r.Password,
ServerAddress: r.URL,
}
if r.Authentication {
var err error
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
if err != nil {
continue
}
}
authConfigs = append(authConfigs, ac)
}
return authConfigs
}
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
return "", "", err
}
username, password, err := registryutils.GetRegEffectiveCredential(registry)
if err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
}
return username, password, err
}

View File

@@ -2,7 +2,6 @@ package exec
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
@@ -43,25 +42,17 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
}
func Test_UpAndDown(t *testing.T) {
testhelpers.IntegrationTest(t)
stack, endpoint := setup(t)
deployer, err := compose.NewComposeDeployer("", "")
if err != nil {
t.Fatal(err)
}
deployer := compose.NewComposeDeployer()
w, err := NewComposeStackManager(deployer, nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
w := NewComposeStackManager(deployer, nil, nil)
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 +60,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 +70,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

@@ -4,6 +4,7 @@ import (
"io"
"os"
"path"
"path/filepath"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -53,7 +54,7 @@ func Test_createEnvFile(t *testing.T) {
result, _ := createEnvFile(tt.stack)
if tt.expected != "" {
assert.Equal(t, "stack.env", result)
assert.Equal(t, filepath.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := io.ReadAll(f)
@@ -77,7 +78,7 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
},
}
result, err := createEnvFile(stack)
assert.Equal(t, "stack.env", result)
assert.Equal(t, filepath.Join(stack.ProjectPath, "stack.env"), result)
assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env"))

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

@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
@@ -46,8 +45,7 @@ func NewSwarmStackManager(
dataStore: datastore,
}
err := manager.updateDockerCLIConfiguration(manager.configPath)
if err != nil {
if err := manager.updateDockerCLIConfiguration(manager.configPath); err != nil {
return nil, err
}
@@ -63,33 +61,14 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
for _, registry := range registries {
if registry.Authentication {
err = registryutils.EnsureRegTokenValid(manager.dataStore, &registry)
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, &registry)
if err != nil {
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
continue
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
if err != nil {
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
continue
}
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
if err != nil {
log.
Warn().
if err := runCommandAndCaptureStdErr(command, registryArgs, nil, ""); err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to login.")
@@ -155,6 +134,7 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
@@ -167,8 +147,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
cmd.Env = append(cmd.Env, env...)
}
err := cmd.Run()
if err != nil {
if err := cmd.Run(); err != nil {
return errors.New(stderr.String())
}
@@ -192,6 +171,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
if err != nil {
return "", nil, err
}
endpointURL = "tcp://" + tunnelAddr
}
@@ -216,6 +196,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
configFilePath := path.Join(configPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil {
return err
@@ -246,8 +227,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
return make(map[string]any), nil
}
err = json.Unmarshal(raw, &config)
if err != nil {
if err := json.Unmarshal(raw, &config); err != nil {
return nil, err
}

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"
@@ -27,11 +26,10 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
}
var edgeStack *portainer.EdgeStack
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
edgeStack, err = handler.createSwarmStack(tx, method, dryrun, tokenData.ID, r)
return err
})
if err != nil {
}); err != nil {
switch {
case httperrors.IsInvalidPayloadError(err):
return httperror.BadRequest("Invalid payload", err)
@@ -78,5 +76,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

@@ -57,17 +57,15 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
}
var payload updateEdgeStackPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
var stack *portainer.EdgeStack
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
stack, err = handler.updateEdgeStack(tx, portainer.EdgeStackID(stackID), payload)
return err
})
if err != nil {
}); err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
@@ -122,14 +120,12 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
stack.EdgeGroups = groupsIds
if payload.UpdateVersion {
err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds)
if err != nil {
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
return nil, httperror.InternalServerError("Unable to update stack version", err)
}
}
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
if err != nil {
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
}
@@ -160,8 +156,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
delete(relation.EdgeStacks, edgeStackID)
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
}
}
@@ -181,8 +176,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
relation.EdgeStacks[edgeStackID] = true
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
}
}

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

@@ -88,13 +88,11 @@ func (handler *Handler) updateRelations(w http.ResponseWriter, r *http.Request)
}
if updateRelations {
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
return errors.WithMessage(err, "Unable to update environment")
}
err = handler.updateEdgeRelations(tx, endpoint)
if err != nil {
if err := handler.updateEdgeRelations(tx, endpoint); err != nil {
return errors.WithMessage(err, "Unable to update environment relations")
}
}

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

@@ -17,7 +17,16 @@ func (handler *Handler) updateEdgeRelations(tx dataservices.DataStoreTx, endpoin
relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
return errors.WithMessage(err, "Unable to find environment relation inside the database")
if !tx.IsErrObjectNotFound(err) {
return errors.WithMessage(err, "Unable to retrieve environment relation inside the database")
}
relation = &portainer.EndpointRelation{
EndpointID: endpoint.ID,
}
if err := tx.EndpointRelation().Create(relation); err != nil {
return errors.WithMessage(err, "Unable to create environment relation inside the database")
}
}
endpointGroup, err := tx.EndpointGroup().Read(endpoint.GroupID)

View File

@@ -4,6 +4,9 @@ import (
"net/http"
"strings"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/gorilla/handlers"
)
@@ -16,8 +19,10 @@ type Handler struct {
// NewHandler creates a handler to serve static files.
func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler {
h := &Handler{
Handler: handlers.CompressHandler(
http.FileServer(http.Dir(assetPublicPath)),
Handler: security.MWSecureHeaders(
handlers.CompressHandler(http.FileServer(http.Dir(assetPublicPath))),
featureflags.IsEnabled("hsts"),
featureflags.IsEnabled("csp"),
),
wasInstanceDisabled: wasInstanceDisabled,
}
@@ -53,7 +58,5 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
w.Header().Add("X-XSS-Protection", "1; mode=block")
w.Header().Add("X-Content-Type-Options", "nosniff")
handler.Handler.ServeHTTP(w, r)
}

View File

@@ -83,7 +83,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.22.0
// @version 2.24.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

@@ -3,7 +3,9 @@ package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
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"
)
@@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWrite
return response.JSON(w, clusterrolebindings)
}
// @id DeleteClusterRoleBindings
// @summary Delete cluster role bindings
// @description Delete the provided list of cluster role bindings.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @param id path int true "Environment identifier"
// @param payload body models.K8sClusterRoleBindingDeleteRequests true "A list of cluster role bindings to delete"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific cluster role binding."
// @failure 500 "Server error occurred while attempting to delete cluster role bindings."
// @router /kubernetes/{id}/cluster_role_bindings/delete [POST]
func (handler *Handler) deleteClusterRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sClusterRoleBindingDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
err = cli.DeleteClusterRoleBindings(payload)
if err != nil {
return httperror.InternalServerError("Failed to delete cluster role bindings", err)
}
return response.Empty(w)
}

View File

@@ -3,7 +3,9 @@ package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
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"
)
@@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *h
return response.JSON(w, clusterroles)
}
// @id DeleteClusterRoles
// @summary Delete cluster roles
// @description Delete the provided list of cluster roles.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @param id path int true "Environment identifier"
// @param payload body models.K8sClusterRoleDeleteRequests true "A list of cluster roles to delete"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific cluster role."
// @failure 500 "Server error occurred while attempting to delete cluster roles."
// @router /kubernetes/{id}/cluster_roles/delete [POST]
func (handler *Handler) deleteClusterRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sClusterRoleDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
err = cli.DeleteClusterRoles(payload)
if err != nil {
return httperror.InternalServerError("Failed to delete cluster roles", err)
}
return response.Empty(w)
}

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

@@ -56,7 +56,9 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost)
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost)
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.getKubernetesDashboard)).Methods(http.MethodGet)
@@ -72,15 +74,12 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
endpointRouter.Handle("/ingresses", httperror.LoggerHandler(h.GetAllKubernetesClusterIngresses)).Methods(http.MethodGet)
endpointRouter.Handle("/ingresses/count", httperror.LoggerHandler(h.getAllKubernetesClusterIngressesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
endpointRouter.Handle("/services", httperror.LoggerHandler(h.GetAllKubernetesServices)).Methods(http.MethodGet)
endpointRouter.Handle("/services/count", httperror.LoggerHandler(h.getAllKubernetesServicesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/secrets", httperror.LoggerHandler(h.GetAllKubernetesSecrets)).Methods(http.MethodGet)
endpointRouter.Handle("/secrets/count", httperror.LoggerHandler(h.getAllKubernetesSecretsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.getKubernetesRBACStatus)).Methods(http.MethodGet)
endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
@@ -89,6 +88,16 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes", httperror.LoggerHandler(h.GetAllKubernetesVolumes)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes/count", httperror.LoggerHandler(h.getAllKubernetesVolumesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
endpointRouter.Handle("/service_accounts/delete", httperror.LoggerHandler(h.deleteKubernetesServiceAccounts)).Methods(http.MethodPost)
endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/roles/delete", httperror.LoggerHandler(h.deleteRoles)).Methods(http.MethodPost)
endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/role_bindings/delete", httperror.LoggerHandler(h.deleteRoleBindings)).Methods(http.MethodPost)
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost)
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)

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

@@ -3,7 +3,9 @@ package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
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"
)
@@ -38,3 +40,38 @@ func (handler *Handler) getAllKubernetesRoleBindings(w http.ResponseWriter, r *h
return response.JSON(w, rolebindings)
}
// @id DeleteRoleBindings
// @summary Delete role bindings
// @description Delete the provided list of role bindings.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @param id path int true "Environment identifier"
// @param payload body models.K8sRoleBindingDeleteRequests true "A map where the key is the namespace and the value is an array of role bindings to delete"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific role binding."
// @failure 500 "Server error occurred while attempting to delete role bindings."
// @router /kubernetes/{id}/role_bindings/delete [POST]
func (h *Handler) deleteRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sRoleBindingDeleteRequests
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := h.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
if err := cli.DeleteRoleBindings(payload); err != nil {
return httperror.InternalServerError("Failed to delete role bindings", err)
}
return response.Empty(w)
}

View File

@@ -3,7 +3,9 @@ package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
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"
)
@@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesRoles(w http.ResponseWriter, r *http.Req
return response.JSON(w, roles)
}
// @id DeleteRoles
// @summary Delete roles
// @description Delete the provided list of roles.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @param id path int true "Environment identifier"
// @param payload body models.K8sRoleDeleteRequests true "A map where the key is the namespace and the value is an array of roles to delete"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific role."
// @failure 500 "Server error occurred while attempting to delete roles."
// @router /kubernetes/{id}/roles/delete [POST]
func (h *Handler) deleteRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sRoleDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := h.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
err = cli.DeleteRoles(payload)
if err != nil {
return httperror.InternalServerError("Failed to delete roles", err)
}
return response.Empty(w)
}

View File

@@ -3,7 +3,9 @@ package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
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"
)
@@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesServiceAccounts(w http.ResponseWriter, r
return response.JSON(w, serviceAccounts)
}
// @id DeleteServiceAccounts
// @summary Delete service accounts
// @description Delete the provided list of service accounts.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @param id path int true "Environment identifier"
// @param payload body models.K8sServiceAccountDeleteRequests true "A map where the key is the namespace and the value is an array of service accounts to delete"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific service account."
// @failure 500 "Server error occurred while attempting to delete service accounts."
// @router /kubernetes/{id}/service_accounts/delete [POST]
func (handler *Handler) deleteKubernetesServiceAccounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sServiceAccountDeleteRequests
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
err = cli.DeleteServiceAccounts(payload)
if err != nil {
return httperror.InternalServerError("Unable to delete service accounts", err)
}
return response.Empty(w)
}

View File

@@ -56,8 +56,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
}
var payload stackMigratePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
@@ -79,8 +78,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return httperror.InternalServerError("Unable to find an endpoint with the specified identifier 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 endpoint", err)
}
@@ -156,14 +154,12 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
newName := stack.Name
stack.Name = oldName
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
if err != nil {
if err := handler.deleteStack(securityContext.UserID, stack, endpoint); err != nil {
return httperror.InternalServerError(err.Error(), err)
}
stack.Name = newName
err = handler.DataStore.Stack().Update(stack.ID, stack)
if err != nil {
if err := handler.DataStore.Stack().Update(stack.ID, stack); err != nil {
return httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
}
@@ -210,10 +206,10 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
}
// Deploy the stack
err = composeDeploymentConfig.Deploy()
if err != nil {
if err := composeDeploymentConfig.Deploy(); err != nil {
return httperror.InternalServerError(err.Error(), err)
}
return nil
}
@@ -237,8 +233,7 @@ func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stac
}
// Deploy the stack
err = swarmDeploymentConfig.Deploy()
if err != nil {
if err := swarmDeploymentConfig.Deploy(); err != nil {
return httperror.InternalServerError(err.Error(), err)
}

View File

@@ -197,17 +197,14 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, pul
switch stack.Type {
case portainer.DockerSwarmStack:
prune := false
if stack.Option != nil {
prune = stack.Option.Prune
}
// Create swarm deployment config
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
prune := stack.Option != nil && stack.Option.Prune
deploymentConfiger, err = deployments.CreateSwarmStackDeploymentConfig(securityContext, stack, endpoint, handler.DataStore, handler.FileService, handler.StackDeployer, prune, pullImage)
if err != nil {
return httperror.InternalServerError(err.Error(), err)

View File

@@ -5,10 +5,8 @@ import (
"net/http"
"time"
"github.com/portainer/portainer/api/http/security"
"github.com/rs/zerolog/log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/ws"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
@@ -76,14 +74,6 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
}
func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
log.Warn().
Err(err).
Msg("unable to retrieve user details from authentication token")
return err
}
r.Header.Del("Origin")
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
@@ -98,14 +88,13 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
}
defer websocketConn.Close()
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID)
}
func hijackAttachStartOperation(
websocketConn *websocket.Conn,
endpoint *portainer.Endpoint,
attachID string,
token string,
) error {
conn, err := initDial(endpoint)
if err != nil {
@@ -127,7 +116,7 @@ func hijackAttachStartOperation(
return err
}
return hijackRequest(websocketConn, conn, attachStartRequest, token)
return ws.HijackRequest(websocketConn, conn, attachStartRequest)
}
func createAttachStartRequest(attachID string) (*http.Request, error) {

View File

@@ -5,13 +5,12 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/ws"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -79,14 +78,6 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
}
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
log.Warn().
Err(err).
Msg("unable to retrieve user details from authentication token")
return err
}
r.Header.Del("Origin")
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
@@ -102,14 +93,13 @@ func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request
defer websocketConn.Close()
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID)
}
func hijackExecStartOperation(
websocketConn *websocket.Conn,
endpoint *portainer.Endpoint,
execID string,
token string,
) error {
conn, err := initDial(endpoint)
if err != nil {
@@ -121,7 +111,7 @@ func hijackExecStartOperation(
return err
}
return hijackRequest(websocketConn, conn, execStartRequest, token)
return ws.HijackRequest(websocketConn, conn, execStartRequest)
}
func createExecStartRequest(execID string) (*http.Request, error) {

View File

@@ -1,7 +1,7 @@
package websocket
import (
"fmt"
"errors"
"io"
"net/http"
"strings"
@@ -9,6 +9,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/ws"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
@@ -69,8 +70,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 +87,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)
}
@@ -139,8 +137,8 @@ func (handler *Handler) hijackPodExecStartOperation(
// errorChan is used to propagate errors from the go routines to the caller.
errorChan := make(chan error, 1)
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
go ws.StreamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
go ws.StreamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
// StartExecProcess is a blocking operation which streams IO to/from pod;
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
@@ -187,7 +185,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

@@ -1,70 +0,0 @@
package websocket
import (
"io"
"unicode/utf8"
"github.com/gorilla/websocket"
)
const readerBufferSize = 2048
func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer, errorChan chan error) {
for {
_, in, err := websocketConn.ReadMessage()
if err != nil {
errorChan <- err
break
}
_, err = writer.Write(in)
if err != nil {
errorChan <- err
break
}
}
}
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
out := make([]byte, readerBufferSize)
for {
n, err := reader.Read(out)
if err != nil {
errorChan <- err
break
}
processedOutput := validString(string(out[:n]))
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
if err != nil {
errorChan <- err
break
}
}
}
func validString(s string) string {
if utf8.ValidString(s) {
return s
}
v := make([]rune, 0, len(s))
for i, r := range s {
if r == utf8.RuneError {
_, size := utf8.DecodeRuneInString(s[i:])
if size == 1 {
continue
}
}
v = append(v, r)
}
return string(v)
}

View File

@@ -3,39 +3,41 @@ package kubernetes
import (
"time"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
)
type K8sApplication struct {
ID string `json:"Id"`
Name string `json:"Name"`
Image string `json:"Image"`
Containers []interface{} `json:"Containers,omitempty"`
Services []corev1.Service `json:"Services"`
CreationDate time.Time `json:"CreationDate"`
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
StackName string `json:"StackName,omitempty"`
ResourcePool string `json:"ResourcePool"`
ApplicationType string `json:"ApplicationType"`
Metadata *Metadata `json:"Metadata,omitempty"`
Status string `json:"Status"`
TotalPodsCount int `json:"TotalPodsCount"`
RunningPodsCount int `json:"RunningPodsCount"`
DeploymentType string `json:"DeploymentType"`
Pods []Pod `json:"Pods,omitempty"`
Configurations []Configuration `json:"Configurations,omitempty"`
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
Namespace string `json:"Namespace,omitempty"`
UID string `json:"Uid,omitempty"`
StackID string `json:"StackId,omitempty"`
ServiceID string `json:"ServiceId,omitempty"`
ServiceName string `json:"ServiceName,omitempty"`
ServiceType string `json:"ServiceType,omitempty"`
Kind string `json:"Kind,omitempty"`
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
Labels map[string]string `json:"Labels,omitempty"`
Resource K8sApplicationResource `json:"Resource,omitempty"`
ID string `json:"Id"`
Name string `json:"Name"`
Image string `json:"Image"`
Containers []interface{} `json:"Containers,omitempty"`
Services []corev1.Service `json:"Services"`
CreationDate time.Time `json:"CreationDate"`
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
StackName string `json:"StackName,omitempty"`
ResourcePool string `json:"ResourcePool"`
ApplicationType string `json:"ApplicationType"`
Metadata *Metadata `json:"Metadata,omitempty"`
Status string `json:"Status"`
TotalPodsCount int `json:"TotalPodsCount"`
RunningPodsCount int `json:"RunningPodsCount"`
DeploymentType string `json:"DeploymentType"`
Pods []Pod `json:"Pods,omitempty"`
Configurations []Configuration `json:"Configurations,omitempty"`
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
Namespace string `json:"Namespace,omitempty"`
UID string `json:"Uid,omitempty"`
StackID string `json:"StackId,omitempty"`
ServiceID string `json:"ServiceId,omitempty"`
ServiceName string `json:"ServiceName,omitempty"`
ServiceType string `json:"ServiceType,omitempty"`
Kind string `json:"Kind,omitempty"`
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
Labels map[string]string `json:"Labels,omitempty"`
Resource K8sApplicationResource `json:"Resource,omitempty"`
HorizontalPodAutoscaler *autoscalingv2.HorizontalPodAutoscaler `json:"HorizontalPodAutoscaler,omitempty"`
}
type Metadata struct {

View File

@@ -1,16 +1,33 @@
package kubernetes
import (
"errors"
"net/http"
"time"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/types"
)
type (
K8sClusterRoleBinding struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
Namespace string `json:"namespace"`
RoleRef rbacv1.RoleRef `json:"roleRef"`
Subjects []rbacv1.Subject `json:"subjects"`
CreationDate time.Time `json:"creationDate"`
IsSystem bool `json:"isSystem"`
}
// K8sRoleBindingDeleteRequests slice of cluster role cluster bindings.
K8sClusterRoleBindingDeleteRequests []string
)
func (r K8sClusterRoleBindingDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
return nil
}

View File

@@ -1,8 +1,28 @@
package kubernetes
import "time"
import (
"errors"
"net/http"
"time"
type K8sClusterRole struct {
Name string `json:"name"`
CreationDate time.Time `json:"creationDate"`
"k8s.io/apimachinery/pkg/types"
)
type (
K8sClusterRole struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
CreationDate time.Time `json:"creationDate"`
IsSystem bool `json:"isSystem"`
}
K8sClusterRoleDeleteRequests []string
)
func (r K8sClusterRoleDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
return nil
}

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

@@ -1,17 +1,38 @@
package kubernetes
import (
"errors"
"net/http"
"time"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/types"
)
type (
K8sRoleBinding struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
Namespace string `json:"namespace"`
RoleRef rbacv1.RoleRef `json:"roleRef"`
Subjects []rbacv1.Subject `json:"subjects"`
CreationDate time.Time `json:"creationDate"`
IsSystem bool `json:"isSystem"`
}
// K8sRoleBindingDeleteRequests is a mapping of namespace names to a slice of role bindings.
K8sRoleBindingDeleteRequests map[string][]string
)
func (r K8sRoleBindingDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
for ns := range r {
if len(ns) == 0 {
return errors.New("deletion given with empty namespace")
}
}
return nil
}

View File

@@ -1,9 +1,36 @@
package kubernetes
import "time"
import (
"errors"
"net/http"
"time"
type K8sRole struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
CreationDate time.Time `json:"creationDate"`
"k8s.io/apimachinery/pkg/types"
)
type (
K8sRole struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
Namespace string `json:"namespace"`
CreationDate time.Time `json:"creationDate"`
// isSystem is true if prefixed with "system:" or exists in the kube-system namespace
// or is one of the portainer roles
IsSystem bool `json:"isSystem"`
}
// K8sRoleDeleteRequests is a mapping of namespace names to a slice of roles.
K8sRoleDeleteRequests map[string][]string
)
func (r K8sRoleDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
for ns := range r {
if len(ns) == 0 {
return errors.New("deletion given with empty namespace")
}
}
return nil
}

View File

@@ -1,9 +1,34 @@
package kubernetes
import "time"
import (
"errors"
"net/http"
"time"
type K8sServiceAccount struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
CreationDate time.Time `json:"creationDate"`
"k8s.io/apimachinery/pkg/types"
)
type (
K8sServiceAccount struct {
Name string `json:"name"`
UID types.UID `json:"uid"`
Namespace string `json:"namespace"`
CreationDate time.Time `json:"creationDate"`
IsSystem bool `json:"isSystem"`
}
// K8sServiceAcountDeleteRequests is a mapping of namespace names to a slice of service account names.
K8sServiceAccountDeleteRequests map[string][]string
)
func (r K8sServiceAccountDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
for ns := range r {
if len(ns) == 0 {
return errors.New("deletion given with empty namespace")
}
}
return nil
}

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
}

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