Compare commits
16 Commits
test-updat
...
2.23.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369598bc96 | ||
|
|
61c5269353 | ||
|
|
7a35b5b0e4 | ||
|
|
20e9423390 | ||
|
|
cf230a1cbc | ||
|
|
a06a09afcf | ||
|
|
c88382ec1f | ||
|
|
fd0bc652a9 | ||
|
|
57e10dc911 | ||
|
|
1110f745e1 | ||
|
|
811d03a419 | ||
|
|
666c031821 | ||
|
|
4e457d97ad | ||
|
|
364e4f1b4e | ||
|
|
8aae557266 | ||
|
|
2bd880ec29 |
166
.github/workflows/ci.yaml
vendored
166
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
1
.github/workflows/label-conflcts.yaml
vendored
1
.github/workflows/label-conflcts.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
name: Label Conflicts
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
55
.github/workflows/lint.yml
vendored
55
.github/workflows/lint.yml
vendored
@@ -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
|
||||
254
.github/workflows/nightly-security-scan.yml
vendored
254
.github/workflows/nightly-security-scan.yml
vendored
@@ -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
|
||||
298
.github/workflows/pr-security.yml
vendored
298
.github/workflows/pr-security.yml
vendored
@@ -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
|
||||
76
.github/workflows/test.yaml
vendored
76
.github/workflows/test.yaml
vendored
@@ -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
|
||||
39
.github/workflows/validate-openapi-spec.yaml
vendored
39
.github/workflows/validate-openapi-spec.yaml
vendored
@@ -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
|
||||
@@ -9,6 +9,9 @@ linters:
|
||||
- gosimple
|
||||
- govet
|
||||
- errorlint
|
||||
- copyloopvar
|
||||
- intrange
|
||||
- perfsprint
|
||||
|
||||
linters-settings:
|
||||
depguard:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
cd $(dirname -- "$0") && yarn lint-staged
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 } }\""
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
@@ -467,10 +466,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
||||
}
|
||||
|
||||
if err := edge.LoadEdgeJobs(dataStore, reverseTunnelService); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(instanceID)
|
||||
|
||||
// channel to control when the admin user is created
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -602,7 +602,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.23.0",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -932,6 +932,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.23.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -60,8 +59,7 @@ func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
|
||||
if err != nil {
|
||||
if err := w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{}); err != nil {
|
||||
t.Fatalf("Error calling docker-compose up: %s", err)
|
||||
}
|
||||
|
||||
@@ -69,8 +67,7 @@ func Test_UpAndDown(t *testing.T) {
|
||||
t.Fatal("container should exist")
|
||||
}
|
||||
|
||||
err = w.Down(ctx, stack, endpoint)
|
||||
if err != nil {
|
||||
if err := w.Down(ctx, stack, endpoint); err != nil {
|
||||
t.Fatalf("Error calling docker-compose down: %s", err)
|
||||
}
|
||||
|
||||
@@ -80,7 +77,7 @@ func Test_UpAndDown(t *testing.T) {
|
||||
}
|
||||
|
||||
func containerExists(containerName string) bool {
|
||||
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName))
|
||||
cmd := exec.Command("docker", "ps", "-a", "-f", "name="+containerName)
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -78,5 +77,5 @@ func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request)
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/edge_stacks/create/%s", method), nil
|
||||
return "/edge_stacks/create/" + method, nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -83,7 +83,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.22.0
|
||||
// @version 2.23.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -69,8 +69,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.InternalServerError("Unable to find the environment associated to the stack inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
@@ -87,15 +86,13 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
err := handler.proxyAgentWebsocketRequest(w, r, params)
|
||||
if err != nil {
|
||||
if err := handler.proxyAgentWebsocketRequest(w, r, params); err != nil {
|
||||
return httperror.InternalServerError("Unable to proxy websocket request to agent", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
err := handler.proxyEdgeAgentWebsocketRequest(w, r, params)
|
||||
if err != nil {
|
||||
if err := handler.proxyEdgeAgentWebsocketRequest(w, r, params); err != nil {
|
||||
return httperror.InternalServerError("Unable to proxy websocket request to Edge agent", err)
|
||||
}
|
||||
|
||||
@@ -187,7 +184,7 @@ func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endp
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return "", false, fmt.Errorf("can not get a valid user service account token")
|
||||
return "", false, errors.New("can not get a valid user service account token")
|
||||
}
|
||||
|
||||
return token, false, nil
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -426,7 +425,7 @@ func (bouncer *RequestBouncer) apiKeyLookup(r *http.Request) (*portainer.TokenDa
|
||||
}
|
||||
if _, _, err := bouncer.jwtService.GenerateToken(tokenData); err != nil {
|
||||
log.Debug().Err(err).Msg("Failed to generate token")
|
||||
return nil, fmt.Errorf("failed to generate token")
|
||||
return nil, errors.New("failed to generate token")
|
||||
}
|
||||
|
||||
if now := time.Now().UTC().Unix(); now-apiKey.LastUsed > 60 { // [seconds]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// LoadEdgeJobs registers all edge jobs inside corresponding environment(endpoint) tunnel
|
||||
func LoadEdgeJobs(dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
|
||||
edgeJobs, err := dataStore.EdgeJob().ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeJob := range edgeJobs {
|
||||
for endpointID := range edgeJob.Endpoints {
|
||||
endpoint, err := dataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reverseTunnelService.AddEdgeJob(endpoint, &edgeJob)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package edge
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Environment(Endpoint)
|
||||
@@ -39,3 +43,33 @@ func EffectiveCheckinInterval(tx dataservices.DataStoreTx, endpoint *portainer.E
|
||||
|
||||
return portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||
}
|
||||
|
||||
// EndpointInEdgeGroup returns true and the edge group name if the endpoint is in the edge group
|
||||
func EndpointInEdgeGroup(
|
||||
tx dataservices.DataStoreTx,
|
||||
endpointID portainer.EndpointID,
|
||||
edgeGroupID portainer.EdgeGroupID,
|
||||
) (bool, string, error) {
|
||||
endpointIDs, err := GetEndpointsFromEdgeGroups(
|
||||
[]portainer.EdgeGroupID{edgeGroupID}, tx,
|
||||
)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
if slices.Contains(endpointIDs, endpointID) {
|
||||
edgeGroup, err := tx.EdgeGroup().Read(edgeGroupID)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Int("edgeGroupID", int(edgeGroupID)).
|
||||
Msg("Unable to retrieve edge group")
|
||||
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
return true, edgeGroup.Name, nil
|
||||
}
|
||||
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package endpointutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -171,7 +171,7 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
||||
} else if len(storage) == 0 {
|
||||
log.Info().Err(err).Msg("zero storage classes found: they may be still building, retrying in 30 seconds")
|
||||
|
||||
return fmt.Errorf("zero storage classes found: they may be still building, retrying in 30 seconds")
|
||||
return errors.New("zero storage classes found: they may be still building, retrying in 30 seconds")
|
||||
}
|
||||
|
||||
endpoint.Kubernetes.Configuration.StorageClasses = storage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
@@ -52,7 +52,7 @@ func GetAccessibleRegistry(
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
err = fmt.Errorf("user does not has permission to get the registry")
|
||||
err = errors.New("user does not has permission to get the registry")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -28,7 +27,7 @@ func (kcl *KubeClient) GetApplications(namespace, nodeName string, withDependenc
|
||||
func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDependencies bool) ([]models.K8sApplication, error) {
|
||||
podListOptions := metav1.ListOptions{}
|
||||
if nodeName != "" {
|
||||
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
|
||||
podListOptions.FieldSelector = "spec.nodeName=" + nodeName
|
||||
}
|
||||
if !withDependencies {
|
||||
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
|
||||
@@ -59,7 +58,7 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
|
||||
|
||||
podListOptions := metav1.ListOptions{}
|
||||
if nodeName != "" {
|
||||
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", nodeName)
|
||||
podListOptions.FieldSelector = "spec.nodeName=" + nodeName
|
||||
}
|
||||
|
||||
if !withDependencies {
|
||||
@@ -125,7 +124,7 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
|
||||
resource := models.K8sApplicationResource{}
|
||||
podListOptions := metav1.ListOptions{}
|
||||
if node != "" {
|
||||
podListOptions.FieldSelector = fmt.Sprintf("spec.nodeName=%s", node)
|
||||
podListOptions.FieldSelector = "spec.nodeName=" + node
|
||||
}
|
||||
|
||||
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||
@@ -134,27 +133,16 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
for _, container := range pod.Spec.Containers {
|
||||
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue())
|
||||
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue())
|
||||
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
|
||||
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
|
||||
}
|
||||
podResources := calculateResourceUsage(pod)
|
||||
resource.CPURequest += podResources.CPURequest
|
||||
resource.CPULimit += podResources.CPULimit
|
||||
resource.MemoryRequest += podResources.MemoryRequest
|
||||
resource.MemoryLimit += podResources.MemoryLimit
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// convertApplicationResourceUnits converts the resource units from milli to core and bytes to mega bytes
|
||||
func convertApplicationResourceUnits(resource models.K8sApplicationResource) models.K8sApplicationResource {
|
||||
return models.K8sApplicationResource{
|
||||
CPURequest: resource.CPURequest / 1000,
|
||||
CPULimit: resource.CPULimit / 1000,
|
||||
MemoryRequest: resource.MemoryRequest / 1024 / 1024,
|
||||
MemoryLimit: resource.MemoryLimit / 1024 / 1024,
|
||||
}
|
||||
}
|
||||
|
||||
// GetApplicationsFromConfigMap gets a list of applications that use a specific ConfigMap
|
||||
// by checking all pods in the same namespace as the ConfigMap
|
||||
func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
|
||||
@@ -352,16 +340,18 @@ func updateApplicationWithService(application models.K8sApplication, services []
|
||||
return application
|
||||
}
|
||||
|
||||
// calculateResourceUsage calculates the resource usage for a pod
|
||||
// calculateResourceUsage calculates the resource usage for a pod in CPU cores and Bytes
|
||||
func calculateResourceUsage(pod corev1.Pod) models.K8sApplicationResource {
|
||||
resource := models.K8sApplicationResource{}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue())
|
||||
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue())
|
||||
// CPU cores as a decimal
|
||||
resource.CPURequest += float64(container.Resources.Requests.Cpu().MilliValue()) / 1000
|
||||
resource.CPULimit += float64(container.Resources.Limits.Cpu().MilliValue()) / 1000
|
||||
// Bytes
|
||||
resource.MemoryRequest += container.Resources.Requests.Memory().Value()
|
||||
resource.MemoryLimit += container.Resources.Limits.Memory().Value()
|
||||
}
|
||||
return convertApplicationResourceUnits(resource)
|
||||
return resource
|
||||
}
|
||||
|
||||
// GetApplicationFromServiceSelector gets applications based on service selectors
|
||||
|
||||
@@ -273,7 +273,7 @@ func (factory *ClientFactory) buildEdgeConfig(endpoint *portainer.Endpoint) (*re
|
||||
func (factory *ClientFactory) CreateRemoteMetricsClient(endpoint *portainer.Endpoint) (*metricsv.Clientset, error) {
|
||||
config, err := factory.CreateConfig(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create metrics KubeConfig")
|
||||
return nil, errors.New("failed to create metrics KubeConfig")
|
||||
}
|
||||
return metricsv.NewForConfig(config)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -16,7 +16,7 @@ func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) {
|
||||
return kcl.fetchClusterRoles()
|
||||
}
|
||||
|
||||
return []models.K8sClusterRole{}, fmt.Errorf("non-admin users are not allowed to access cluster roles")
|
||||
return []models.K8sClusterRole{}, errors.New("non-admin users are not allowed to access cluster roles")
|
||||
}
|
||||
|
||||
// fetchClusterRoles returns a list of all Roles in the specified namespace.
|
||||
|
||||
@@ -2,7 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -16,7 +16,7 @@ func (kcl *KubeClient) GetClusterRoleBindings() ([]models.K8sClusterRoleBinding,
|
||||
return kcl.fetchClusterRoleBindings()
|
||||
}
|
||||
|
||||
return []models.K8sClusterRoleBinding{}, fmt.Errorf("non-admin users are not allowed to access cluster role bindings")
|
||||
return []models.K8sClusterRoleBinding{}, errors.New("non-admin users are not allowed to access cluster role bindings")
|
||||
}
|
||||
|
||||
// fetchClusterRoleBindings returns a list of all cluster roles in the cluster.
|
||||
|
||||
@@ -135,6 +135,7 @@ func parsePersistentVolumeClaim(volume *corev1.PersistentVolumeClaim) models.K8s
|
||||
VolumeMode: volume.Spec.VolumeMode,
|
||||
OwningApplications: nil,
|
||||
Phase: volume.Status.Phase,
|
||||
Labels: volume.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1568,10 +1568,6 @@ type (
|
||||
TunnelAddr(endpoint *Endpoint) (string, error)
|
||||
UpdateLastActivity(endpointID EndpointID)
|
||||
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
|
||||
EdgeJobs(endpointId EndpointID) []EdgeJob
|
||||
AddEdgeJob(endpoint *Endpoint, edgeJob *EdgeJob)
|
||||
RemoveEdgeJob(edgeJobID EdgeJobID)
|
||||
RemoveEdgeJobFromEndpoint(endpointID EndpointID, edgeJobID EdgeJobID)
|
||||
}
|
||||
|
||||
// Server defines the interface to serve the API
|
||||
@@ -1599,7 +1595,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.22.0"
|
||||
APIVersion = "2.23.0"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
@@ -3,7 +3,6 @@ package scheduler
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -59,7 +58,7 @@ func Test_JobShouldStop_UponPermError(t *testing.T) {
|
||||
s.StartJobEvery(jobInterval, func() error {
|
||||
acc++
|
||||
close(ch)
|
||||
return NewPermanentError(fmt.Errorf("failed"))
|
||||
return NewPermanentError(errors.New("failed"))
|
||||
})
|
||||
|
||||
<-time.After(3 * jobInterval)
|
||||
@@ -76,7 +75,7 @@ func Test_JobShouldNotStop_UponError(t *testing.T) {
|
||||
s.StartJobEvery(jobInterval, func() error {
|
||||
if acc.Add(1) == 2 {
|
||||
close(ch)
|
||||
return NewPermanentError(fmt.Errorf("failed"))
|
||||
return NewPermanentError(errors.New("failed"))
|
||||
}
|
||||
|
||||
return errors.New("non-permanent error")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package deployments
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -43,21 +44,19 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
|
||||
// Webhook
|
||||
if stack.AutoUpdate != nil && stack.AutoUpdate.Webhook != "" {
|
||||
return redeployWhenChanged(stack, deployer, datastore, gitService)
|
||||
return redeployWhenChanged(stack, deployer, datastore, gitService, true)
|
||||
}
|
||||
|
||||
// Polling
|
||||
_, err, _ = singleflightGroup.Do(strconv.Itoa(int(stackID)), func() (any, error) {
|
||||
return nil, redeployWhenChanged(stack, deployer, datastore, gitService)
|
||||
return nil, redeployWhenChanged(stack, deployer, datastore, gitService, false)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService) error {
|
||||
stackID := stack.ID
|
||||
|
||||
log.Debug().Int("stack_id", int(stackID)).Msg("redeploying stack")
|
||||
func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datastore dataservices.DataStore, gitService portainer.GitService, webhook bool) error {
|
||||
log.Debug().Int("stack_id", int(stack.ID)).Msg("redeploying stack")
|
||||
|
||||
if stack.GitConfig == nil {
|
||||
return nil // do nothing if it isn't a git-based stack
|
||||
@@ -76,17 +75,14 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
|
||||
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
|
||||
}
|
||||
|
||||
author := stack.UpdatedBy
|
||||
if author == "" {
|
||||
author = stack.CreatedBy
|
||||
}
|
||||
author := cmp.Or(stack.UpdatedBy, stack.CreatedBy)
|
||||
|
||||
user, err := datastore.User().UserByUsername(author)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Int("stack_id", int(stackID)).
|
||||
Str("author", author).
|
||||
Int("stack_id", int(stack.ID)).
|
||||
Str("stack", stack.Name).
|
||||
Str("author", author).
|
||||
Int("endpoint_id", int(stack.EndpointID)).
|
||||
Msg("cannot auto update a stack, stack author user is missing")
|
||||
|
||||
@@ -97,9 +93,36 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
|
||||
return nil
|
||||
}
|
||||
|
||||
if webhook {
|
||||
go func() {
|
||||
if err := redeployWhenChangedSecondStage(stack, deployer, datastore, gitService, user, endpoint); err != nil {
|
||||
log.Error().Err(err).
|
||||
Int("stack_id", int(stack.ID)).
|
||||
Str("stack", stack.Name).
|
||||
Str("author", author).
|
||||
Int("endpoint_id", int(stack.EndpointID)).
|
||||
Msg("webhook failed to redeploy a stack")
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return redeployWhenChangedSecondStage(stack, deployer, datastore, gitService, user, endpoint)
|
||||
}
|
||||
|
||||
func redeployWhenChangedSecondStage(
|
||||
stack *portainer.Stack,
|
||||
deployer StackDeployer,
|
||||
datastore dataservices.DataStore,
|
||||
gitService portainer.GitService,
|
||||
user *portainer.User,
|
||||
endpoint *portainer.Endpoint,
|
||||
) error {
|
||||
var gitCommitChangedOrForceUpdate bool
|
||||
|
||||
if !stack.FromAppTemplate {
|
||||
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, false, false, stack.ProjectPath)
|
||||
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stack.ID), stack.GitConfig, false, false, stack.ProjectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,7 +147,6 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
err = deployer.DeployRemoteComposeStack(stack, endpoint, registries, true, false)
|
||||
} else {
|
||||
@@ -132,7 +154,7 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stack.ID)
|
||||
}
|
||||
case portainer.DockerSwarmStack:
|
||||
if stackutils.IsRelativePathStack(stack) {
|
||||
@@ -141,16 +163,13 @@ func redeployWhenChanged(stack *portainer.Stack, deployer StackDeployer, datasto
|
||||
err = deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
|
||||
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stack.ID)
|
||||
}
|
||||
case portainer.KubernetesStack:
|
||||
log.Debug().
|
||||
Int("stack_id", int(stackID)).
|
||||
Msg("deploying a kube app")
|
||||
log.Debug().Int("stack_id", int(stack.ID)).Msg("deploying a kube app")
|
||||
|
||||
err := deployer.DeployKubernetesStack(stack, endpoint, user)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stackID)
|
||||
if err := deployer.DeployKubernetesStack(stack, endpoint, user); err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stack.ID)
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
||||
|
||||
@@ -31,9 +31,7 @@ import {
|
||||
KubernetesPodNodeAffinityPayload,
|
||||
KubernetesPreferredSchedulingTermPayload,
|
||||
} from 'Kubernetes/pod/payloads/affinities';
|
||||
|
||||
export const PodKubernetesInstanceLabel = 'app.kubernetes.io/instance';
|
||||
export const PodManagedByLabel = 'app.kubernetes.io/managed-by';
|
||||
import { PodKubernetesInstanceLabel, PodManagedByLabel } from '@/react/kubernetes/applications/constants';
|
||||
|
||||
class KubernetesApplicationHelper {
|
||||
/* #region UTILITY FUNCTIONS */
|
||||
|
||||
@@ -15,8 +15,6 @@ export const applicationsModule = angular
|
||||
'namespaces',
|
||||
'onNamespaceChange',
|
||||
'onRefresh',
|
||||
'showSystem',
|
||||
'onShowSystemChange',
|
||||
'onRemove',
|
||||
'hideStacks',
|
||||
])
|
||||
|
||||
@@ -215,8 +215,6 @@ export const ngModule = angular
|
||||
'namespace',
|
||||
'namespaces',
|
||||
'onNamespaceChange',
|
||||
'showSystem',
|
||||
'setSystemResources',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
show-system="ctrl.state.isSystemResources"
|
||||
on-show-system-change="(ctrl.setSystemResources)"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
@@ -50,8 +48,6 @@
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
show-system="ctrl.state.isSystemResources"
|
||||
on-show-system-change="(ctrl.setSystemResources)"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
|
||||
@@ -42,7 +42,6 @@ class KubernetesApplicationsController {
|
||||
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
||||
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
||||
this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
|
||||
this.setSystemResources = this.setSystemResources.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
@@ -133,12 +132,6 @@ class KubernetesApplicationsController {
|
||||
});
|
||||
}
|
||||
|
||||
setSystemResources(flag) {
|
||||
return this.$scope.$applyAsync(() => {
|
||||
this.state.isSystemResources = flag;
|
||||
});
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: this.LocalStorage.getActiveTab('applications'),
|
||||
@@ -150,7 +143,6 @@ class KubernetesApplicationsController {
|
||||
ports: [],
|
||||
namespaces: [],
|
||||
namespaceName: '',
|
||||
isSystemResources: undefined,
|
||||
};
|
||||
|
||||
this.deploymentOptions = await getDeploymentOptions();
|
||||
|
||||
@@ -363,7 +363,9 @@ class KubernetesNodeController {
|
||||
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
||||
};
|
||||
|
||||
await Promise.allSettled([this.getNodes(), this.getEvents(), this.getEndpoints(), this.getNodeUsage()]);
|
||||
// getEvents depends on nodes, so get nodes first
|
||||
await this.getNodes();
|
||||
await Promise.allSettled([this.getEvents(), this.getEndpoints(), this.getNodeUsage()]);
|
||||
|
||||
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
||||
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
||||
@@ -371,7 +373,6 @@ class KubernetesNodeController {
|
||||
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||
|
||||
this.resourceReservation = await getTotalResourcesForAllApplications(this.$state.params.endpointId, this.node.Name);
|
||||
this.resourceReservation.CpuRequest = Math.round(this.resourceReservation.CpuRequest / 1000);
|
||||
this.resourceReservation.MemoryRequest = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.MemoryRequest);
|
||||
this.node.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { confirmRedeploy } from '@/react/kubernetes/volumes/ItemView/ConfirmRedeployModal';
|
||||
import { isVolumeUsed, isVolumeExternal } from '@/react/kubernetes/volumes/utils';
|
||||
import { isVolumeUsed } from '@/react/kubernetes/volumes/utils';
|
||||
|
||||
class KubernetesVolumeController {
|
||||
/* @ngInject */
|
||||
@@ -50,7 +50,7 @@ class KubernetesVolumeController {
|
||||
}
|
||||
|
||||
isExternalVolume() {
|
||||
return isVolumeExternal(this.volume);
|
||||
return !this.volume.PersistentVolumeClaim.ApplicationOwner;
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
/* @ngInject */
|
||||
export default function CodeEditorController($scope) {
|
||||
this.type = '';
|
||||
|
||||
this.handleChange = (value) => {
|
||||
$scope.$evalAsync(() => {
|
||||
this.onChange(value);
|
||||
});
|
||||
};
|
||||
|
||||
this.$onInit = () => {
|
||||
this.type = getType({ yaml: this.yml, dockerFile: this.dockerFile, shell: this.shell });
|
||||
};
|
||||
}
|
||||
|
||||
function getType({ yaml, dockerFile, shell }) {
|
||||
if (yaml) {
|
||||
return 'yaml';
|
||||
}
|
||||
if (dockerFile) {
|
||||
return 'dockerfile';
|
||||
}
|
||||
if (shell) {
|
||||
return 'shell';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<react-code-editor
|
||||
id="$ctrl.identifier"
|
||||
placeholder="$ctrl.placeholder"
|
||||
yaml="$ctrl.yml"
|
||||
docker-file="$ctrl.dockerFile"
|
||||
shell="$ctrl.shell"
|
||||
type="$ctrl.type"
|
||||
readonly="$ctrl.readOnly"
|
||||
on-change="($ctrl.handleChange)"
|
||||
value="$ctrl.value"
|
||||
|
||||
@@ -223,9 +223,7 @@ export const ngModule = angular
|
||||
r2a(CodeEditor, [
|
||||
'id',
|
||||
'placeholder',
|
||||
'yaml',
|
||||
'dockerFile',
|
||||
'shell',
|
||||
'type',
|
||||
'readonly',
|
||||
'onChange',
|
||||
'value',
|
||||
|
||||
@@ -15,12 +15,11 @@ import styles from './CodeEditor.module.css';
|
||||
import { TextTip } from './Tip/TextTip';
|
||||
import { StackVersionSelector } from './StackVersionSelector';
|
||||
|
||||
type Type = 'yaml' | 'shell' | 'dockerfile';
|
||||
interface Props extends AutomationTestingProps {
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
yaml?: boolean;
|
||||
dockerFile?: boolean;
|
||||
shell?: boolean;
|
||||
type?: Type;
|
||||
readonly?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
@@ -62,6 +61,12 @@ const dockerFileLanguage = new LanguageSupport(
|
||||
);
|
||||
const shellLanguage = new LanguageSupport(StreamLanguage.define(shell));
|
||||
|
||||
const docTypeExtensionMap: Record<Type, LanguageSupport> = {
|
||||
yaml: yamlLanguage,
|
||||
dockerfile: dockerFileLanguage,
|
||||
shell: shellLanguage,
|
||||
};
|
||||
|
||||
export function CodeEditor({
|
||||
id,
|
||||
onChange,
|
||||
@@ -71,26 +76,18 @@ export function CodeEditor({
|
||||
versions,
|
||||
onVersionChange,
|
||||
height = '500px',
|
||||
yaml: isYaml,
|
||||
dockerFile: isDockerFile,
|
||||
shell: isShell,
|
||||
type,
|
||||
'data-cy': dataCy,
|
||||
}: Props) {
|
||||
const [isRollback, setIsRollback] = useState(false);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const extensions = [];
|
||||
if (isYaml) {
|
||||
extensions.push(yamlLanguage);
|
||||
}
|
||||
if (isDockerFile) {
|
||||
extensions.push(dockerFileLanguage);
|
||||
}
|
||||
if (isShell) {
|
||||
extensions.push(shellLanguage);
|
||||
if (type && docTypeExtensionMap[type]) {
|
||||
extensions.push(docTypeExtensionMap[type]);
|
||||
}
|
||||
return extensions;
|
||||
}, [isYaml, isDockerFile, isShell]);
|
||||
}, [type]);
|
||||
|
||||
function handleVersionChange(version: number) {
|
||||
if (versions && versions.length > 1) {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { PropsWithChildren, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
ComponentProps,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useTransitionHook } from '@uirouter/react';
|
||||
|
||||
import { BROWSER_OS_PLATFORM } from '@/react/constants';
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { CodeEditor } from '@@/CodeEditor';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
@@ -52,39 +57,21 @@ export const editorConfig = {
|
||||
win: otherEditorConfig,
|
||||
} as const;
|
||||
|
||||
interface Props extends AutomationTestingProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
type CodeEditorProps = ComponentProps<typeof CodeEditor>;
|
||||
|
||||
id: string;
|
||||
placeholder?: string;
|
||||
yaml?: boolean;
|
||||
shell?: boolean;
|
||||
readonly?: boolean;
|
||||
titleContent?: React.ReactNode;
|
||||
interface Props extends CodeEditorProps {
|
||||
titleContent?: ReactNode;
|
||||
hideTitle?: boolean;
|
||||
error?: string;
|
||||
versions?: number[];
|
||||
onVersionChange?: (version: number) => void;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function WebEditorForm({
|
||||
id,
|
||||
onChange,
|
||||
placeholder,
|
||||
value,
|
||||
titleContent = '',
|
||||
hideTitle,
|
||||
readonly,
|
||||
yaml,
|
||||
shell,
|
||||
children,
|
||||
error,
|
||||
versions,
|
||||
onVersionChange,
|
||||
height,
|
||||
'data-cy': dataCy,
|
||||
...props
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div>
|
||||
@@ -107,16 +94,8 @@ export function WebEditorForm({
|
||||
<div className="col-sm-12 col-lg-12">
|
||||
<CodeEditor
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
yaml={yaml}
|
||||
shell={shell}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
versions={versions}
|
||||
onVersionChange={(v) => onVersionChange && onVersionChange(v)}
|
||||
height={height}
|
||||
data-cy={dataCy}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ function InnerForm({ isLoading }: { isLoading: boolean }) {
|
||||
onChange={(value) => setFieldValue('fileContent', value)}
|
||||
value={values.fileContent}
|
||||
placeholder="Define or paste the content of your script file here"
|
||||
shell
|
||||
type="shell"
|
||||
error={errors.fileContent}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -83,7 +83,7 @@ function InnerForm({ isLoading }: { isLoading: boolean }) {
|
||||
onChange={(value) => setFieldValue('fileContent', value)}
|
||||
value={values.fileContent}
|
||||
placeholder="Define or paste the content of your script file here"
|
||||
shell
|
||||
type="shell"
|
||||
error={errors.fileContent}
|
||||
/>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function DockerContentField({
|
||||
id="stack-creation-editor"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
yaml
|
||||
type="yaml"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
error={error}
|
||||
readonly={readonly}
|
||||
|
||||
@@ -72,7 +72,7 @@ export function KubeManifestForm({
|
||||
id="stack-creation-editor"
|
||||
value={values.fileContent}
|
||||
onChange={(value) => handleChange({ fileContent: value })}
|
||||
yaml
|
||||
type="yaml"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
error={errors?.fileContent}
|
||||
data-cy="stack-creation-editor"
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ComposeForm({
|
||||
<WebEditorForm
|
||||
data-cy="compose-editor"
|
||||
value={values.content}
|
||||
yaml
|
||||
type="yaml"
|
||||
id="compose-editor"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
onChange={(value) => handleContentChange(DeploymentType.Compose, value)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user