Compare commits

...

13 Commits

Author SHA1 Message Date
Oscar Zhou 515ef880c0 version: bump version to 2.21.5 (#254) 2024-12-20 08:51:16 +13:00
Steven Kang b89f1d314f fix: security - CVE-2024-45337 - portainer-suite release 2.21 (#249) 2024-12-18 19:05:20 -03:00
andres-portainer be3cb0690c fix: 2.21.5 backported fixes (#251)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-12-17 19:16:41 -03:00
Yajith Dayarathna 835a7e41e6 update ci trigger paths for portainer-ee - release/2.21 (#69) 2024-10-29 12:23:44 +13:00
LP B 5aae1cd991 fix(app): deploy container app template (#59)
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-10-24 16:09:34 +13:00
andres-portainer 34532deccb fix(swarm): fix service updates BE-11219 (#58) 2024-10-23 18:23:28 -03:00
Oscar Zhou 80c8e483c9 version: bump version to 2.21.4 (#37) 2024-10-22 13:33:45 +13:00
andres-portainer 9421e9d452 fix(security): add initial support for HSTS and CSP BE-11311 (#52) 2024-10-21 13:52:21 -03:00
Oscar Zhou 55cda8c78e fix(edge): backport agent id/name into edge api response [BE-10988] (#36) 2024-10-18 16:40:39 +13:00
Yajith Dayarathna 4190fc1b4e required changes to enable monorepo.
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
2024-10-09 08:40:52 +13:00
LP B ac5491e864 feat(app): limit the docker API version supported by the frontend (#12295) 2024-10-08 17:13:14 +02:00
Oscar Zhou 8cbd23c059 version: bump version to 2.21.3 (#12300) 2024-10-08 08:12:46 +13:00
andres-portainer 3800a958da fix(endpoints): optimize the search performance BE-11267 (#12264) 2024-10-01 15:14:04 -03:00
265 changed files with 5087 additions and 4589 deletions
-166
View File
@@ -1,166 +0,0 @@
name: ci
on:
workflow_dispatch:
push:
branches:
- 'develop'
- 'release/*'
pull_request:
branches:
- 'develop'
- 'release/*'
- 'feat/*'
- 'fix/*'
- 'refactor/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.21.11
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: ${{ env.GO_VERSION }}
- 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 --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 --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 --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 --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 --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
-15
View File
@@ -1,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 10
WAIT_MS: 60000
-55
View File
@@ -1,55 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.21.9
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.55.2
args: --timeout=10m -c .golangci.yaml
-252
View File
@@ -1,252 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
env:
GO_VERSION: 1.21.9
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 portainerci/portainer:develop
- 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: portainerci/portainer:develop
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
View File
@@ -1,298 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'go.mod'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.21.9
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
-19
View File
@@ -1,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-28
View File
@@ -1,28 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request
-76
View File
@@ -1,76 +0,0 @@
name: Test
env:
GO_VERSION: 1.21.9
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
@@ -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.21.9
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
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
cd $(dirname -- "$0") && yarn lint-staged
@@ -941,6 +941,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.21.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.21.5\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}
@@ -2,6 +2,7 @@ package edgestacks
import (
"errors"
"fmt"
"net/http"
"time"
@@ -63,9 +64,8 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
}
var payload updateStatusPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", fmt.Errorf("edge polling error: %w. Environment ID: %d", err, payload.EndpointID))
}
var stack *portainer.EdgeStack
@@ -98,17 +98,16 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *ht
return nil, nil
}
return nil, err
return nil, fmt.Errorf("unable to retrieve Edge stack from the database: %w. Environment ID: %d", err, payload.EndpointID)
}
endpoint, err := tx.Endpoint().Endpoint(payload.EndpointID)
if err != nil {
return nil, handler.handlerDBErr(err, "Unable to find an environment with the specified identifier inside the database")
return nil, handler.handlerDBErr(fmt.Errorf("unable to find the environment from the database: %w. Environment ID: %d", err, payload.EndpointID), "unable to find the environment")
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return nil, httperror.Forbidden("Permission denied to access environment", err)
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return nil, httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
status := *payload.Status
@@ -126,9 +125,8 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *ht
updateEnvStatus(payload.EndpointID, stack, deploymentStatus)
err = tx.EdgeStack().UpdateEdgeStack(stackID, stack)
if err != nil {
return nil, handler.handlerDBErr(err, "Unable to persist the stack changes inside the database")
if err := tx.EdgeStack().UpdateEdgeStack(stackID, stack); err != nil {
return nil, handler.handlerDBErr(fmt.Errorf("unable to update Edge stack to the database: %w. Environment name: %s", err, endpoint.Name), "unable to update Edge stack")
}
return stack, nil
@@ -2,6 +2,7 @@ package endpointedge
import (
"errors"
"fmt"
"net/http"
"strconv"
@@ -39,32 +40,30 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
return httperror.BadRequest("Unable to find an environment on request context", err)
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID")
if err != nil {
return httperror.BadRequest("Invalid edge job identifier route variable", err)
return httperror.BadRequest("Invalid edge job identifier route variable", fmt.Errorf("invalid Edge job route variable: %w. Environment name: %s", err, endpoint.Name))
}
var payload logsPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", fmt.Errorf("invalid Edge job request payload: %w. Environment name: %s", err, endpoint.Name))
}
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return handler.getEdgeJobLobs(tx, endpoint.ID, portainer.EdgeJobID(edgeJobID), payload)
})
if err != nil {
}); err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment name: %s", httpErr.Err, endpoint.Name)
return httpErr
}
return httperror.InternalServerError("Unexpected error", err)
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment name: %s", err, endpoint.Name))
}
return response.JSON(w, nil)
@@ -1,7 +1,7 @@
package endpointedge
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@@ -33,27 +33,26 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
return httperror.BadRequest("Unable to find an environment on request context", err)
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
if err != nil {
return httperror.BadRequest("Invalid edge stack identifier route variable", err)
return httperror.BadRequest("Invalid edge stack identifier route variable", fmt.Errorf("invalid Edge stack route variable: %w. Environment name: %s", err, endpoint.Name))
}
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", err)
return httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("unable to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
} else if err != nil {
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", err)
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("failed to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
}
fileName := edgeStack.EntryPoint
if endpointutils.IsDockerEndpoint(endpoint) {
if fileName == "" {
return httperror.BadRequest("Docker is not supported by this stack", errors.New("Docker is not supported by this stack"))
return httperror.BadRequest("Docker is not supported by this stack", fmt.Errorf("no filename is provided for the Docker endpoint. Environment name: %s", endpoint.Name))
}
}
@@ -66,18 +65,18 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
fileName = edgeStack.ManifestPath
if fileName == "" {
return httperror.BadRequest("Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack"))
return httperror.BadRequest("Kubernetes is not supported by this stack", fmt.Errorf("no filename is provided for the Kubernetes endpoint. Environment name: %s", endpoint.Name))
}
}
dirEntries, err := filesystem.LoadDir(edgeStack.ProjectPath)
if err != nil {
return httperror.InternalServerError("Unable to load repository", err)
return httperror.InternalServerError("Unable to load repository", fmt.Errorf("failed to load project directory: %w. Environment name: %s", err, endpoint.Name))
}
fileContent, err := filesystem.FilterDirForCompatibility(dirEntries, fileName, endpoint.Agent.Version)
if err != nil {
return httperror.InternalServerError("File not found", err)
return httperror.InternalServerError("File not found", fmt.Errorf("unable to find file: %w. Environment name: %s", err, endpoint.Name))
}
dirEntries = filesystem.FilterDirForEntryFile(dirEntries, fileName)
@@ -86,27 +86,27 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
// EE-5190
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("unable to retrieve endpoint heartbeat. Environment ID: %d", endpointID))
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err != nil {
// EE-5190
return httperror.Forbidden("Permission denied to access environment", errors.New("the device has not been trusted yet"))
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("unable to retrieve endpoint from database: %w. Environment ID: %d", err, endpointID))
}
firstConn := endpoint.LastCheckInDate == 0
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("unauthorized Edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
handler.DataStore.Endpoint().UpdateHeartbeat(endpoint.ID)
err = handler.requestBouncer.TrustedEdgeEnvironmentAccess(handler.DataStore, endpoint)
if err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("untrusted Edge environment access: %w. Environment name: %s", err, endpoint.Name))
}
var statusResponse *endpointEdgeStatusInspectResponse
@@ -117,10 +117,11 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
if err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment name: %s", httpErr.Err, endpoint.Name)
return httpErr
}
return httperror.InternalServerError("Unexpected error", err)
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment name: %s", err, endpoint.Name))
}
return cacheResponse(w, endpoint.ID, *statusResponse)
@@ -265,6 +266,7 @@ func cacheResponse(w http.ResponseWriter, endpointID portainer.EndpointID, statu
httpErr := response.JSON(rr, statusResponse)
if httpErr != nil {
httpErr.Err = fmt.Errorf("failed to cache response: %w. Environment ID: %d", httpErr.Err, endpointID)
return httpErr
}
@@ -96,8 +96,8 @@ func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpoin
payloadTagSet := tag.Set(payload.TagIDs)
endpointGroupTagSet := tag.Set((endpointGroup.TagIDs))
union := tag.Union(payloadTagSet, endpointGroupTagSet)
intersection := tag.Intersection(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > len(intersection)
intersection := tag.IntersectionCount(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > intersection
if tagsChanged {
removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet)
+17 -28
View File
@@ -193,7 +193,7 @@ func (handler *Handler) filterEndpointsByQuery(
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
tagsMap := make(map[portainer.TagID]string, len(tags))
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
@@ -302,8 +302,7 @@ func filterEndpointsBySearchCriteria(
) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
if endpointMatchSearchCriteria(&endpoint, tagsMap, searchCriteria) {
endpoints[n] = endpoint
n++
@@ -317,7 +316,7 @@ func filterEndpointsBySearchCriteria(
continue
}
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpoints, endpointGroups) {
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpointGroups) {
endpoints[n] = endpoint
n++
@@ -363,7 +362,7 @@ func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portai
return endpoints[:n]
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
@@ -378,8 +377,8 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
for _, tagID := range endpoint.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
@@ -389,16 +388,17 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
if group.ID != endpoint.GroupID {
continue
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tagID := range group.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
}
@@ -411,11 +411,10 @@ func edgeGroupMatchSearchCriteria(
endpoint *portainer.Endpoint,
edgeGroups []portainer.EdgeGroup,
searchCriteria string,
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
) bool {
for _, edgeGroup := range edgeGroups {
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, endpoints, endpointGroups)
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, []portainer.Endpoint{*endpoint}, endpointGroups)
for _, endpointID := range relatedEndpointIDs {
if endpointID == endpoint.ID {
@@ -446,16 +445,6 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []port
return endpoints[:n]
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0, len(tagIDs))
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
+98
View File
@@ -1,6 +1,7 @@
package endpoints
import (
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -148,6 +149,103 @@ func Test_Filter_excludeIDs(t *testing.T) {
runTests(tests, t, handler, environments)
}
func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := 0; i < n; i++ {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := 0; i < 1000; i++ {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1, 2, 3},
PartialMatch: true,
})
}
tagsMap := map[portainer.TagID]string{}
for i := 0; i < 10; i++ {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for i := 0; i < b.N; i++ {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := 0; i < n; i++ {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1, 2, 3},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := 0; i < 1000; i++ {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1},
})
}
tagsMap := map[portainer.TagID]string{}
for i := 0; i < 10; i++ {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for i := 0; i < b.N; i++ {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
+7 -4
View File
@@ -4,6 +4,9 @@ import (
"net/http"
"strings"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/gorilla/handlers"
)
@@ -16,8 +19,10 @@ type Handler struct {
// NewHandler creates a handler to serve static files.
func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler {
h := &Handler{
Handler: handlers.CompressHandler(
http.FileServer(http.Dir(assetPublicPath)),
Handler: security.MWSecureHeaders(
handlers.CompressHandler(http.FileServer(http.Dir(assetPublicPath))),
featureflags.IsEnabled("hsts"),
featureflags.IsEnabled("csp"),
),
wasInstanceDisabled: wasInstanceDisabled,
}
@@ -53,7 +58,5 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
w.Header().Add("X-XSS-Protection", "1; mode=block")
w.Header().Add("X-Content-Type-Options", "nosniff")
handler.Handler.ServeHTTP(w, r)
}
+1 -1
View File
@@ -85,7 +85,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.21.2
// @version 2.21.5
// @description.markdown api-description.md
// @termsOfService
@@ -122,6 +122,7 @@ func (handler *Handler) executeServiceWebhook(
_ = rc.Close()
}(rc)
}
_, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, serviceUpdateOptions)
if err != nil {
+3 -1
View File
@@ -45,7 +45,9 @@ func (o *OfflineGate) WaitingMiddleware(timeout time.Duration, next http.Handler
httperror.WriteError(w, http.StatusRequestTimeout, "Timeout waiting for the offline gate to signal", http.ErrHandlerTimeout)
return
}
defer o.lock.RUnlock()
next.ServeHTTP(w, r)
o.lock.RUnlock()
})
}
+5
View File
@@ -20,11 +20,16 @@ type postDockerfileRequest struct {
}
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
//
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
// will extract the file content from the request body, tar it, and rewrite the body.
// !! THIS IS ONLY TRUE WHEN THE UPLOADED DOCKERFILE FILE HAS NO EXTENSION (the generated file.type in the frontend will be empty)
// If the Dockerfile is named like Dockerfile.yaml or has an internal type, a non-empty Content-Type header will be generated
//
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
// rewrite the body of the request.
//
// In any other case, it will leave the request unaltered.
func buildOperation(request *http.Request) error {
contentTypeHeader := request.Header.Get("Content-Type")
+69 -52
View File
@@ -84,11 +84,28 @@ func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, er
return transport.ProxyDockerRequest(request)
}
var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*http.Response, error){
"configs": (*Transport).proxyConfigRequest,
"containers": (*Transport).proxyContainerRequest,
"services": (*Transport).proxyServiceRequest,
"volumes": (*Transport).proxyVolumeRequest,
"networks": (*Transport).proxyNetworkRequest,
"secrets": (*Transport).proxySecretRequest,
"swarm": (*Transport).proxySwarmRequest,
"nodes": (*Transport).proxyNodeRequest,
"tasks": (*Transport).proxyTaskRequest,
"build": (*Transport).proxyBuildRequest,
"images": (*Transport).proxyImageRequest,
"v2": (*Transport).proxyAgentRequest,
}
// ProxyDockerRequest intercepts a Docker API request and apply logic based
// on the requested operation.
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
request.URL.Path = requestPath
// from : /v1.41/containers/{id}/json
// or : /containers/{id}/json
// to : /containers/{id}/json
unversionedPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
if transport.endpoint.Type == portainer.AgentOnDockerEnvironment || transport.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@@ -100,34 +117,16 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
}
switch {
case strings.HasPrefix(requestPath, "/configs"):
return transport.proxyConfigRequest(request)
case strings.HasPrefix(requestPath, "/containers"):
return transport.proxyContainerRequest(request)
case strings.HasPrefix(requestPath, "/services"):
return transport.proxyServiceRequest(request)
case strings.HasPrefix(requestPath, "/volumes"):
return transport.proxyVolumeRequest(request)
case strings.HasPrefix(requestPath, "/networks"):
return transport.proxyNetworkRequest(request)
case strings.HasPrefix(requestPath, "/secrets"):
return transport.proxySecretRequest(request)
case strings.HasPrefix(requestPath, "/swarm"):
return transport.proxySwarmRequest(request)
case strings.HasPrefix(requestPath, "/nodes"):
return transport.proxyNodeRequest(request)
case strings.HasPrefix(requestPath, "/tasks"):
return transport.proxyTaskRequest(request)
case strings.HasPrefix(requestPath, "/build"):
return transport.proxyBuildRequest(request)
case strings.HasPrefix(requestPath, "/images"):
return transport.proxyImageRequest(request)
case strings.HasPrefix(requestPath, "/v2"):
return transport.proxyAgentRequest(request)
default:
return transport.executeDockerRequest(request)
// from : /containers/{id}/json
// trim to : containers/{id}/json
// pick : [ containers, {id}, json ][0]
// prefix : containers
prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0]
if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil {
return proxyFunc(transport, request, unversionedPath)
}
return transport.executeDockerRequest(request)
}
func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) {
@@ -144,8 +143,8 @@ func (transport *Transport) executeDockerRequest(request *http.Request) (*http.R
return response, err
}
func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := strings.TrimPrefix(unversionedPath, "/v2")
switch {
case strings.HasPrefix(requestPath, "/browse"):
@@ -203,8 +202,10 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
return transport.executeDockerRequest(r)
}
func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyConfigRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/configs/create":
return transport.decorateGenericResourceCreationOperation(request, configObjectIdentifier, portainer.ConfigResourceControl)
@@ -225,8 +226,10 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyContainerRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/containers/create":
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
@@ -261,8 +264,10 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
}
}
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyServiceRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/services/create":
return transport.decorateServiceCreationOperation(request)
@@ -292,8 +297,10 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
}
}
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyVolumeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/volumes/create":
return transport.decorateVolumeResourceCreationOperation(request, portainer.VolumeResourceControl)
@@ -309,8 +316,10 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyNetworkRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/networks/create":
return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl)
@@ -330,8 +339,10 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Re
}
}
func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxySecretRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/secrets/create":
return transport.decorateGenericResourceCreationOperation(request, secretObjectIdentifier, portainer.SecretResourceControl)
@@ -351,8 +362,8 @@ func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Res
}
}
func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
requestPath := request.URL.Path
func (transport *Transport) proxyNodeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
// assume /nodes/{id}
if path.Base(requestPath) != "nodes" {
@@ -362,8 +373,10 @@ func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Respo
return transport.executeDockerRequest(request)
}
func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxySwarmRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/swarm":
return transport.rewriteOperation(request, swarmInspectOperation)
default:
@@ -372,8 +385,10 @@ func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Resp
}
}
func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyTaskRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/tasks":
return transport.rewriteOperation(request, transport.taskListOperation)
default:
@@ -382,7 +397,7 @@ func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Respo
}
}
func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) {
err := transport.updateDefaultGitBranch(request)
if err != nil {
return nil, err
@@ -408,8 +423,10 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
return nil
}
func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
func (transport *Transport) proxyImageRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/images/create":
return transport.replaceRegistryAuthenticationHeader(request)
default:
+19 -6
View File
@@ -11,10 +11,11 @@ import (
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/featureflags"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/rs/zerolog/log"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const apiKeyHeader = "X-API-KEY"
@@ -43,6 +44,8 @@ type (
jwtService portainer.JWTService
apiKeyService apikey.APIKeyService
revokedJWT sync.Map
hsts bool
csp bool
}
// RestrictedRequestContext is a data structure containing information
@@ -69,6 +72,8 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
dataStore: dataStore,
jwtService: jwtService,
apiKeyService: apiKeyService,
hsts: featureflags.IsEnabled("hsts"),
csp: featureflags.IsEnabled("csp"),
}
go b.cleanUpExpiredJWT()
@@ -79,7 +84,7 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
// PublicAccess defines a security check for public API endpoints.
// No authentication is required to access these endpoints.
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
return mwSecureHeaders(h)
return MWSecureHeaders(h, bouncer.hsts, bouncer.csp)
}
// AdminAccess defines a security check for API endpoints that require an authorization check.
@@ -208,7 +213,8 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler
bouncer.CookieAuthLookup,
bouncer.JWTAuthLookup,
}, h)
h = mwSecureHeaders(h)
h = MWSecureHeaders(h, bouncer.hsts, bouncer.csp)
return h
}
@@ -506,10 +512,17 @@ func extractAPIKey(r *http.Request) (string, bool) {
return "", false
}
// mwSecureHeaders provides secure headers middleware for handlers.
func mwSecureHeaders(next http.Handler) http.Handler {
// MWSecureHeaders provides secure headers middleware for handlers.
func MWSecureHeaders(next http.Handler, hsts, csp bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-XSS-Protection", "1; mode=block")
if hsts {
w.Header().Set("Strict-Transport-Security", "max-age=31536000") // 365 days
}
if csp {
w.Header().Set("Content-Security-Policy", "script-src 'self' cdn.matomo.cloud")
}
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
+3 -4
View File
@@ -77,6 +77,7 @@ func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portai
return true
}
}
return false
}
@@ -84,12 +85,10 @@ func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portai
if endpointGroup.TagIDs != nil {
endpointTags = tag.Union(endpointTags, tag.Set(endpointGroup.TagIDs))
}
edgeGroupTags := tag.Set(edgeGroup.TagIDs)
if edgeGroup.PartialMatch {
intersection := tag.Intersection(endpointTags, edgeGroupTags)
return len(intersection) != 0
return tag.PartialMatch(edgeGroup.TagIDs, endpointTags)
}
return tag.FullMatch(edgeGroupTags, endpointTags)
return tag.FullMatch(edgeGroup.TagIDs, endpointTags)
}
+26 -27
View File
@@ -1,64 +1,63 @@
package tag
import portainer "github.com/portainer/portainer/api"
import (
portainer "github.com/portainer/portainer/api"
)
type tagSet map[portainer.TagID]bool
type tagSet map[portainer.TagID]struct{}
// Set converts an array of ids to a set
func Set(tagIDs []portainer.TagID) tagSet {
set := map[portainer.TagID]bool{}
set := map[portainer.TagID]struct{}{}
for _, tagID := range tagIDs {
set[tagID] = true
set[tagID] = struct{}{}
}
return set
}
// Intersection returns a set intersection of the provided sets
func Intersection(sets ...tagSet) tagSet {
intersection := tagSet{}
if len(sets) == 0 {
return intersection
// IntersectionCount returns the element count of the intersection of the sets
func IntersectionCount(setA, setB tagSet) int {
if len(setA) > len(setB) {
setA, setB = setB, setA
}
setA := sets[0]
count := 0
for tag := range setA {
inAll := true
for _, setB := range sets {
if !setB[tag] {
inAll = false
break
}
}
if inAll {
intersection[tag] = true
if _, ok := setB[tag]; ok {
count++
}
}
return intersection
return count
}
// Union returns a set union of provided sets
func Union(sets ...tagSet) tagSet {
union := tagSet{}
for _, set := range sets {
for tag := range set {
union[tag] = true
union[tag] = struct{}{}
}
}
return union
}
// Contains return true if setA contains setB
func Contains(setA tagSet, setB tagSet) bool {
func Contains(setA tagSet, setB []portainer.TagID) bool {
if len(setA) == 0 || len(setB) == 0 {
return false
}
for tag := range setB {
if !setA[tag] {
for _, tag := range setB {
if _, ok := setA[tag]; !ok {
return false
}
}
return true
}
@@ -67,8 +66,8 @@ func Difference(setA tagSet, setB tagSet) tagSet {
set := tagSet{}
for tag := range setA {
if !setB[tag] {
set[tag] = true
if _, ok := setB[tag]; !ok {
set[tag] = struct{}{}
}
}
+11 -3
View File
@@ -1,11 +1,19 @@
package tag
import portainer "github.com/portainer/portainer/api"
// FullMatch returns true if environment tags matches all edge group tags
func FullMatch(edgeGroupTags tagSet, environmentTags tagSet) bool {
func FullMatch(edgeGroupTags []portainer.TagID, environmentTags tagSet) bool {
return Contains(environmentTags, edgeGroupTags)
}
// PartialMatch returns true if environment tags matches at least one edge group tag
func PartialMatch(edgeGroupTags tagSet, environmentTags tagSet) bool {
return len(Intersection(edgeGroupTags, environmentTags)) != 0
func PartialMatch(edgeGroupTags []portainer.TagID, environmentTags tagSet) bool {
for _, tagID := range edgeGroupTags {
if _, ok := environmentTags[tagID]; ok {
return true
}
}
return false
}
+17 -17
View File
@@ -9,49 +9,49 @@ import (
func TestFullMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
edgeGroupTags []portainer.TagID
environmentTag tagSet
expected bool
}{
{
name: "environment tag partially match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group tags equal to environment tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags fully match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2, 3}),
expected: true,
},
{
name: "environment tags do not match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{3, 4}),
expected: false,
},
{
name: "edge group has no tags and environment has tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group has tags and environment has no tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
@@ -70,55 +70,55 @@ func TestFullMatch(t *testing.T) {
func TestPartialMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
edgeGroupTags []portainer.TagID
environmentTag tagSet
expected bool
}{
{
name: "environment tags partially match edge group tags 1",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags partially match edge group tags 2",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 4, 5}),
expected: true,
},
{
name: "edge group tags equal to environment tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags fully match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2, 3}),
expected: true,
},
{
name: "environment tags do not match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{3, 4}),
expected: false,
},
{
name: "edge group has no tags and environment has tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group has tags and environment has no tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
+17 -17
View File
@@ -7,49 +7,49 @@ import (
portainer "github.com/portainer/portainer/api"
)
func TestIntersection(t *testing.T) {
func TestIntersectionCount(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected tagSet
expected int
}{
{
name: "positive numbers set intersection",
setA: Set([]portainer.TagID{1, 2, 3, 4, 5}),
setB: Set([]portainer.TagID{4, 5, 6, 7}),
expected: Set([]portainer.TagID{4, 5}),
expected: 2,
},
{
name: "empty setA intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "empty setB intersection",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "no common elements sets intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{4, 5, 6}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "equal sets intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{1, 2, 3}),
expected: 3,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Intersection(tc.setA, tc.setB)
if !reflect.DeepEqual(result, tc.expected) {
result := IntersectionCount(tc.setA, tc.setB)
if result != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
@@ -109,49 +109,49 @@ func TestContains(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
setB []portainer.TagID
expected bool
}{
{
name: "setA contains setB",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: true,
},
{
name: "setA equals to setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: true,
},
{
name: "setA contains parts of setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{1, 2, 3}),
setB: []portainer.TagID{1, 2, 3},
expected: false,
},
{
name: "setA does not contain setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{3, 4}),
setB: []portainer.TagID{3, 4},
expected: false,
},
{
name: "setA is empty and setB is not empty",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: false,
},
{
name: "setA is not empty and setB is empty",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{}),
setB: []portainer.TagID{},
expected: false,
},
{
name: "setA is empty and setB is empty",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{}),
setB: []portainer.TagID{},
expected: false,
},
}
+6 -2
View File
@@ -1601,7 +1601,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.21.2"
APIVersion = "2.21.5"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
@@ -1653,11 +1653,15 @@ const (
// List of supported features
const (
FeatureFdo = "fdo"
FeatureFdo = "fdo"
FeatureHSTS = "hsts"
FeatureCSP = "csp"
)
var SupportedFeatureFlags = []featureflags.Feature{
FeatureFdo,
FeatureHSTS,
FeatureCSP,
}
const (
-15
View File
@@ -5,7 +5,6 @@ function ImageHelperFactory() {
return {
isValidTag,
createImageConfigForContainer,
getImagesNamesForDownload,
removeDigestFromRepository,
imageContainsURL,
};
@@ -14,20 +13,6 @@ function ImageHelperFactory() {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
}
/**
*
* @param {Array<{tags: Array<string>; id: string;}>} images
* @returns {{names: string[]}}}
*/
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.tags[0] !== '<none>:<none>' ? image.tags[0] : image.id;
});
return {
names,
};
}
/**
*
* @param {PorImageRegistryModel} registry
-8
View File
@@ -3,14 +3,6 @@ angular.module('portainer.docker').factory('VolumeHelper', [
'use strict';
var helper = {};
helper.createDriverOptions = function (optionArray) {
var options = {};
optionArray.forEach(function (option) {
options[option.name] = option.value;
});
return options;
};
helper.isVolumeUsedByAService = function (volume, services) {
for (var i = 0; i < services.length; i++) {
var service = services[i];
+33
View File
@@ -0,0 +1,33 @@
type Data = {
stream: string;
errorDetail: { message: string };
};
export class ImageBuildModel {
hasError: boolean = false;
buildLogs: string[];
constructor(data: Data[]) {
const buildLogs: string[] = [];
data.forEach((line) => {
if (line.stream) {
// convert unicode chars to readable chars
const logLine = line.stream.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
buildLogs.push(logLine);
}
if (line.errorDetail) {
buildLogs.push(line.errorDetail.message);
this.hasError = true;
}
});
this.buildLogs = buildLogs;
}
}
-30
View File
@@ -1,30 +0,0 @@
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
function b64DecodeUnicode(str) {
try {
return decodeURIComponent(
atob(str)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
} catch (err) {
return atob(str);
}
}
export function ConfigViewModel(data) {
this.Id = data.ID;
this.CreatedAt = data.CreatedAt;
this.UpdatedAt = data.UpdatedAt;
this.Version = data.Version.Index;
this.Name = data.Spec.Name;
this.Labels = data.Spec.Labels;
this.Data = b64DecodeUnicode(data.Spec.Data);
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}
+54
View File
@@ -0,0 +1,54 @@
import { Config } from 'docker-types/generated/1.41';
import { IResource } from '@/react/docker/components/datatables/createOwnershipColumn';
import { PortainerResponse } from '@/react/docker/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export class ConfigViewModel implements IResource {
Id: string;
CreatedAt: string;
UpdatedAt: string;
Version: number;
Name: string;
Labels: Record<string, string>;
Data: string;
ResourceControl?: ResourceControlViewModel;
constructor(data: PortainerResponse<Config>) {
this.Id = data.ID || '';
this.CreatedAt = data.CreatedAt || '';
this.UpdatedAt = data.UpdatedAt || '';
this.Version = data.Version?.Index || 0;
this.Name = data.Spec?.Name || '';
this.Labels = data.Spec?.Labels || {};
this.Data = b64DecodeUnicode(data.Spec?.Data || '');
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(
data.Portainer.ResourceControl
);
}
}
}
function b64DecodeUnicode(str: string) {
try {
return decodeURIComponent(
window
.atob(str)
.toString()
.split('')
.map((c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
.join('')
);
} catch (err) {
return window.atob(str);
}
}
-145
View File
@@ -1,145 +0,0 @@
import _ from 'lodash-es';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export function createStatus(statusText) {
var status = _.toLower(statusText);
if (status.indexOf('paused') > -1) {
return 'paused';
} else if (status.indexOf('dead') > -1) {
return 'dead';
} else if (status.indexOf('created') > -1) {
return 'created';
} else if (status.indexOf('exited') > -1) {
return 'stopped';
} else if (status.indexOf('(healthy)') > -1) {
return 'healthy';
} else if (status.indexOf('(unhealthy)') > -1) {
return 'unhealthy';
} else if (status.indexOf('(health: starting)') > -1) {
return 'starting';
}
return 'running';
}
export function ContainerViewModel(data) {
this.Id = data.Id;
this.Status = createStatus(data.Status);
this.State = data.State;
this.Created = data.Created;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
}
this.NetworkSettings = data.NetworkSettings;
this.Image = data.Image;
this.ImageID = data.ImageID;
this.Command = data.Command;
this.Checked = false;
this.Labels = data.Labels;
if (this.Labels && this.Labels['com.docker.compose.project']) {
this.StackName = this.Labels['com.docker.compose.project'];
} else if (this.Labels && this.Labels['com.docker.stack.namespace']) {
this.StackName = this.Labels['com.docker.stack.namespace'];
}
this.Mounts = data.Mounts;
this.IsPortainer = data.IsPortainer;
this.Ports = [];
if (data.Ports) {
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
if (data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
}
}
export function ContainerStatsViewModel(data) {
this.read = data.read;
this.preread = data.preread;
if (data.memory_stats.privateworkingset !== undefined) {
// Windows
this.MemoryUsage = data.memory_stats.privateworkingset;
this.MemoryCache = 0;
this.NumProcs = data.num_procs;
this.isWindows = true;
} else {
// Linux
if (data.memory_stats.stats === undefined || data.memory_stats.usage === undefined) {
this.MemoryUsage = this.MemoryCache = 0;
} else {
this.MemoryCache = 0;
if (data.memory_stats.stats.cache !== undefined) {
// cgroups v1
this.MemoryCache = data.memory_stats.stats.cache;
}
this.MemoryUsage = data.memory_stats.usage - this.MemoryCache;
}
}
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
this.CPUCores = 1;
if (data.cpu_stats.cpu_usage.percpu_usage) {
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
} else {
if (data.cpu_stats.online_cpus !== undefined) {
this.CPUCores = data.cpu_stats.online_cpus;
}
}
this.Networks = _.values(data.networks);
if (data.blkio_stats !== undefined && data.blkio_stats.io_service_bytes_recursive !== null) {
//TODO: take care of multiple block devices
var readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Read');
if (readData === undefined) {
// try the cgroups v2 version
readData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'read');
}
if (readData !== undefined) {
this.BytesRead = readData.value;
}
var writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'Write');
if (writeData === undefined) {
// try the cgroups v2 version
writeData = data.blkio_stats.io_service_bytes_recursive.find((d) => d.op === 'write');
}
if (writeData !== undefined) {
this.BytesWrite = writeData.value;
}
} else {
//no IO related data is available
this.noIOdata = true;
}
}
export function ContainerDetailsViewModel(data) {
this.Model = data;
this.Id = data.Id;
this.State = data.State;
this.Created = data.Created;
this.Name = data.Name;
this.NetworkSettings = data.NetworkSettings;
this.Args = data.Args;
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
this.Mounts = data.Mounts;
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
this.IsPortainer = data.IsPortainer;
}
+56
View File
@@ -0,0 +1,56 @@
import { IResource } from '@/react/docker/components/datatables/createOwnershipColumn';
import { ContainerDetailsResponse } from '@/react/docker/containers/queries/useContainer';
import { PortainerResponse } from '@/react/docker/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export class ContainerDetailsViewModel
implements IResource, Pick<PortainerResponse<unknown>, 'IsPortainer'>
{
Model: ContainerDetailsResponse;
Id: ContainerDetailsResponse['Id'];
State: ContainerDetailsResponse['State'];
Created: ContainerDetailsResponse['Created'];
Name: ContainerDetailsResponse['Name'];
NetworkSettings: ContainerDetailsResponse['NetworkSettings'];
Args: ContainerDetailsResponse['Args'];
Image: ContainerDetailsResponse['Image'];
Config: ContainerDetailsResponse['Config'];
HostConfig: ContainerDetailsResponse['HostConfig'];
Mounts: ContainerDetailsResponse['Mounts'];
// IResource
ResourceControl?: ResourceControlViewModel;
// PortainerResponse
IsPortainer?: ContainerDetailsResponse['IsPortainer'];
constructor(data: ContainerDetailsResponse) {
this.Model = data;
this.Id = data.Id;
this.State = data.State;
this.Created = data.Created;
this.Name = data.Name;
this.NetworkSettings = data.NetworkSettings;
this.Args = data.Args;
this.Image = data.Image;
this.Config = data.Config;
this.HostConfig = data.HostConfig;
this.Mounts = data.Mounts;
if (data.Portainer && data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(
data.Portainer.ResourceControl
);
}
this.IsPortainer = data.IsPortainer;
}
}
+113
View File
@@ -0,0 +1,113 @@
import { values } from 'lodash';
import { ContainerStats } from '@/react/docker/containers/queries/useContainerStats';
import { ValueOf } from '@/types';
/**
* This type is arbitrary and only defined based on what we use / observed from the API responses.
*/
export class ContainerStatsViewModel {
read: string;
preread: string;
MemoryUsage: number;
MemoryCache: number = 0;
NumProcs: number = 0;
isWindows: boolean = false;
PreviousCPUTotalUsage: number;
PreviousCPUSystemUsage: number;
CurrentCPUTotalUsage: number;
CurrentCPUSystemUsage: number;
CPUCores: number;
Networks: ValueOf<NonNullable<ContainerStats['networks']>>[];
BytesRead: number = 0;
BytesWrite: number = 0;
noIOdata: boolean = false;
constructor(data: ContainerStats) {
this.read = data.read || '';
this.preread = data.preread || '';
if (data?.memory_stats?.privateworkingset !== undefined) {
// Windows
this.MemoryUsage = data?.memory_stats?.privateworkingset;
this.MemoryCache = 0;
this.NumProcs = data.num_procs || 0;
this.isWindows = true;
}
// Linux
else if (
data?.memory_stats?.stats === undefined ||
data?.memory_stats?.usage === undefined
) {
this.MemoryUsage = 0;
this.MemoryCache = 0;
} else {
this.MemoryCache = 0;
if (data?.memory_stats?.stats?.cache !== undefined) {
// cgroups v1
this.MemoryCache = data.memory_stats.stats.cache;
}
this.MemoryUsage = data.memory_stats.usage - this.MemoryCache;
}
this.PreviousCPUTotalUsage =
data?.precpu_stats?.cpu_usage?.total_usage || 0;
this.PreviousCPUSystemUsage = data?.precpu_stats?.system_cpu_usage || 0;
this.CurrentCPUTotalUsage = data?.cpu_stats?.cpu_usage?.total_usage || 0;
this.CurrentCPUSystemUsage = data?.cpu_stats?.system_cpu_usage || 0;
this.CPUCores = 1;
this.CPUCores =
data?.cpu_stats?.cpu_usage?.percpu_usage?.length ??
data?.cpu_stats?.online_cpus ??
1;
this.Networks = values(data.networks);
if (
data.blkio_stats !== undefined &&
data.blkio_stats.io_service_bytes_recursive !== null
) {
// TODO: take care of multiple block devices
let readData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'Read'
);
if (readData === undefined) {
// try the cgroups v2 version
readData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'read'
);
}
if (readData !== undefined) {
this.BytesRead = readData.value;
}
let writeData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'Write'
);
if (writeData === undefined) {
// try the cgroups v2 version
writeData = data?.blkio_stats?.io_service_bytes_recursive?.find(
(d) => d.op === 'write'
);
}
if (writeData !== undefined) {
this.BytesWrite = writeData.value;
}
} else {
// no IO related data is available
this.noIOdata = true;
}
}
}
-174
View File
@@ -1,174 +0,0 @@
function createEventDetails(event) {
var eventAttr = event.Actor.Attributes;
var details = '';
var action = event.Action;
var extra = '';
var hasColon = action.indexOf(':');
if (hasColon != -1) {
extra = action.substring(hasColon);
action = action.substring(0, hasColon);
}
switch (event.Type) {
case 'container':
switch (action) {
case 'stop':
details = 'Container ' + eventAttr.name + ' stopped';
break;
case 'destroy':
details = 'Container ' + eventAttr.name + ' deleted';
break;
case 'create':
details = 'Container ' + eventAttr.name + ' created';
break;
case 'start':
details = 'Container ' + eventAttr.name + ' started';
break;
case 'kill':
details = 'Container ' + eventAttr.name + ' killed';
break;
case 'die':
details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode;
break;
case 'commit':
details = 'Container ' + eventAttr.name + ' committed';
break;
case 'restart':
details = 'Container ' + eventAttr.name + ' restarted';
break;
case 'pause':
details = 'Container ' + eventAttr.name + ' paused';
break;
case 'unpause':
details = 'Container ' + eventAttr.name + ' unpaused';
break;
case 'attach':
details = 'Container ' + eventAttr.name + ' attached';
break;
case 'detach':
details = 'Container ' + eventAttr.name + ' detached';
break;
case 'copy':
details = 'Container ' + eventAttr.name + ' copied';
break;
case 'export':
details = 'Container ' + eventAttr.name + ' exported';
break;
case 'health_status':
details = 'Container ' + eventAttr.name + ' executed health status';
break;
case 'oom':
details = 'Container ' + eventAttr.name + ' goes in out of memory';
break;
case 'rename':
details = 'Container ' + eventAttr.name + ' renamed';
break;
case 'resize':
details = 'Container ' + eventAttr.name + ' resized';
break;
case 'top':
details = 'Showed running processes for container ' + eventAttr.name;
break;
case 'update':
details = 'Container ' + eventAttr.name + ' updated';
break;
case 'exec_create':
details = 'Exec instance created';
break;
case 'exec_start':
details = 'Exec instance started';
break;
case 'exec_die':
details = 'Exec instance exited';
break;
default:
details = 'Unsupported event';
}
break;
case 'image':
switch (action) {
case 'delete':
details = 'Image deleted';
break;
case 'import':
details = 'Image ' + event.Actor.ID + ' imported';
break;
case 'load':
details = 'Image ' + event.Actor.ID + ' loaded';
break;
case 'tag':
details = 'New tag created for ' + eventAttr.name;
break;
case 'untag':
details = 'Image untagged';
break;
case 'save':
details = 'Image ' + event.Actor.ID + ' saved';
break;
case 'pull':
details = 'Image ' + event.Actor.ID + ' pulled';
break;
case 'push':
details = 'Image ' + event.Actor.ID + ' pushed';
break;
default:
details = 'Unsupported event';
}
break;
case 'network':
switch (action) {
case 'create':
details = 'Network ' + eventAttr.name + ' created';
break;
case 'destroy':
details = 'Network ' + eventAttr.name + ' deleted';
break;
case 'remove':
details = 'Network ' + eventAttr.name + ' removed';
break;
case 'connect':
details = 'Container connected to ' + eventAttr.name + ' network';
break;
case 'disconnect':
details = 'Container disconnected from ' + eventAttr.name + ' network';
break;
default:
details = 'Unsupported event';
}
break;
case 'volume':
switch (action) {
case 'create':
details = 'Volume ' + event.Actor.ID + ' created';
break;
case 'destroy':
details = 'Volume ' + event.Actor.ID + ' deleted';
break;
case 'mount':
details = 'Volume ' + event.Actor.ID + ' mounted';
break;
case 'unmount':
details = 'Volume ' + event.Actor.ID + ' unmounted';
break;
default:
details = 'Unsupported event';
}
break;
default:
details = 'Unsupported event';
}
return details + extra;
}
export function EventViewModel(data) {
// Type, Action, Actor unavailable in Docker < 1.10
this.Time = data.time;
if (data.Type) {
this.Type = data.Type;
this.Details = createEventDetails(data);
} else {
this.Type = data.status;
this.Details = data.from;
}
}
+134
View File
@@ -0,0 +1,134 @@
import { EventMessage } from 'docker-types/generated/1.41';
type EventType = NonNullable<EventMessage['Type']>;
type Action = string;
type Attributes = {
id: string;
name: string;
exitCode: string;
};
type EventToTemplateMap = Record<EventType, ActionToTemplateMap>;
type ActionToTemplateMap = Record<Action, TemplateBuilder>;
type TemplateBuilder = (attr: Attributes) => string;
/**
* {
* [EventType]: {
* [Action]: TemplateBuilder,
* [Action]: TemplateBuilder
* },
* [EventType]: {
* [Action]: TemplateBuilder,
* }
* }
*
* EventType are known and defined by Docker specs
* Action are unknown and specific for each EventType
*/
const templates: EventToTemplateMap = {
builder: {},
config: {},
container: {
stop: ({ name }) => `Container ${name} stopped`,
destroy: ({ name }) => `Container ${name} deleted`,
create: ({ name }) => `Container ${name} created`,
start: ({ name }) => `Container ${name} started`,
kill: ({ name }) => `Container ${name} killed`,
die: ({ name, exitCode }) =>
`Container ${name} exited with status code ${exitCode}`,
commit: ({ name }) => `Container ${name} committed`,
restart: ({ name }) => `Container ${name} restarted`,
pause: ({ name }) => `Container ${name} paused`,
unpause: ({ name }) => `Container ${name} unpaused`,
attach: ({ name }) => `Container ${name} attached`,
detach: ({ name }) => `Container ${name} detached`,
copy: ({ name }) => `Container ${name} copied`,
export: ({ name }) => `Container ${name} exported`,
health_status: ({ name }) => `Container ${name} executed health status`,
oom: ({ name }) => `Container ${name} goes in out of memory`,
rename: ({ name }) => `Container ${name} renamed`,
resize: ({ name }) => `Container ${name} resized`,
top: ({ name }) => `Showed running processes for container ${name}`,
update: ({ name }) => `Container ${name} updated`,
exec_create: () => `Exec instance created`,
exec_start: () => `Exec instance started`,
exec_die: () => `Exec instance exited`,
},
daemon: {},
image: {
delete: () => `Image deleted`,
import: ({ id }) => `Image ${id} imported`,
load: ({ id }) => `Image ${id} loaded`,
tag: ({ name }) => `New tag created for ${name}`,
untag: () => `Image untagged`,
save: ({ id }) => `Image ${id} saved`,
pull: ({ id }) => `Image ${id} pulled`,
push: ({ id }) => `Image ${id} pushed`,
},
network: {
create: ({ name }) => `Network ${name} created`,
destroy: ({ name }) => `Network ${name} deleted`,
remove: ({ name }) => `Network ${name} removed`,
connect: ({ name }) => `Container connected to ${name} network`,
disconnect: ({ name }) => `Container disconnected from ${name} network`,
prune: () => `Networks pruned`,
},
node: {},
plugin: {},
secret: {},
service: {},
volume: {
create: ({ id }) => `Volume ${id} created`,
destroy: ({ id }) => `Volume ${id} deleted`,
mount: ({ id }) => `Volume ${id} mounted`,
unmount: ({ id }) => `Volume ${id} unmounted`,
},
};
function createEventDetails(event: EventMessage) {
const eventType = event.Type ?? '';
// An action can be `action:extra`
// For example `docker exec -it CONTAINER sh`
// Generates the action `exec_create: sh`
let extra = '';
let action = event.Action ?? '';
const hasColon = action?.indexOf(':') ?? -1;
if (hasColon !== -1) {
extra = action?.substring(hasColon) ?? '';
action = action?.substring(0, hasColon);
}
const attr: Attributes = {
id: event.Actor?.ID || '',
name: event.Actor?.Attributes?.name || '',
exitCode: event.Actor?.Attributes?.exitCode || '',
};
// Event types are defined by the docker API specs
// Each event has it own set of actions, which a unknown/not defined by specs
// If the received event or action has no builder associated to it
// We consider the event unsupported and we provide the raw data
const detailsBuilder = templates[eventType as EventType]?.[action];
const details = detailsBuilder
? detailsBuilder(attr)
: `Unsupported event: ${eventType} / ${action}`;
return details + extra;
}
export class EventViewModel {
Time: EventMessage['time'];
Type: EventMessage['Type'];
Details: string;
constructor(data: EventMessage) {
this.Time = data.time;
this.Type = data.Type;
this.Details = createEventDetails(data);
}
}
-45
View File
@@ -1,45 +0,0 @@
export function ImageViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
if ((!this.RepoTags || this.RepoTags.length === 0) && data.RepoDigests) {
this.RepoTags = [];
for (var i = 0; i < data.RepoDigests.length; i++) {
var digest = data.RepoDigests[i];
var repository = digest.substring(0, digest.indexOf('@'));
this.RepoTags.push(repository + ':<none>');
}
}
this.Size = data.Size;
this.Used = data.Used;
if (data.Portainer && data.Portainer.Agent && data.Portainer.Agent.NodeName) {
this.NodeName = data.Portainer.Agent.NodeName;
}
this.Labels = data.Labels;
}
export function ImageBuildModel(data) {
this.hasError = false;
var buildLogs = [];
for (var i = 0; i < data.length; i++) {
var line = data[i];
if (line.stream) {
line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
buildLogs.push(line);
}
if (line.errorDetail) {
buildLogs.push(line.errorDetail.message);
this.hasError = true;
}
}
this.buildLogs = buildLogs;
}
+47
View File
@@ -0,0 +1,47 @@
import { ImageSummary } from 'docker-types/generated/1.41';
import { PortainerResponse } from '@/react/docker/types';
export type ImageId = ImageSummary['Id'];
export type ImageName = string;
/**
* Partial copy of ImageSummary
*/
export class ImageViewModel {
Id: ImageId;
Created: ImageSummary['Created'];
RepoTags: ImageSummary['RepoTags'];
Size: ImageSummary['Size'];
Labels: ImageSummary['Labels'];
// internal
NodeName: string;
Used: boolean = false;
constructor(data: PortainerResponse<ImageSummary>, used: boolean = false) {
this.Id = data.Id;
// this.Tag = data.Tag; // doesn't seem to be used?
// this.Repository = data.Repository; // doesn't seem to be used?
this.Created = data.Created;
this.RepoTags = data.RepoTags;
if ((!this.RepoTags || this.RepoTags.length === 0) && data.RepoDigests) {
this.RepoTags = [];
data.RepoDigests.forEach((digest) => {
const repository = digest.substring(0, digest.indexOf('@'));
this.RepoTags.push(`${repository}:<none>`);
});
}
this.Size = data.Size;
this.NodeName = data.Portainer?.Agent?.NodeName || '';
this.Labels = data.Labels;
this.Used = used;
}
}
-27
View File
@@ -1,27 +0,0 @@
export function ImageDetailsViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Parent = data.Parent;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.Size = data.Size;
this.DockerVersion = data.DockerVersion;
this.Os = data.Os;
this.Architecture = data.Architecture;
this.Author = data.Author;
this.Command = data.Config.Cmd;
let config = {};
if (data.Config) {
config = data.Config; // this is part of OCI images-spec
} else if (data.ContainerConfig != null) {
config = data.ContainerConfig; // not OCI ; has been removed in Docker 26 (API v1.45) along with .Container
}
this.Entrypoint = config.Entrypoint ? config.Entrypoint : '';
this.ExposedPorts = config.ExposedPorts ? Object.keys(config.ExposedPorts) : [];
this.Volumes = config.Volumes ? Object.keys(config.Volumes) : [];
this.Env = config.Env ? config.Env : [];
this.Labels = config.Labels;
}
+70
View File
@@ -0,0 +1,70 @@
import { ImageInspect } from 'docker-types/generated/1.41';
type ImageInspectConfig = NonNullable<ImageInspect['Config']>;
export class ImageDetailsViewModel {
Id: ImageInspect['Id'];
Parent: ImageInspect['Parent'];
Created: ImageInspect['Created'];
RepoTags: ImageInspect['RepoTags'];
Size: ImageInspect['Size'];
DockerVersion: ImageInspect['DockerVersion'];
Os: ImageInspect['Os'];
Architecture: ImageInspect['Architecture'];
Author: ImageInspect['Author'];
// Config sub fields
Command: ImageInspectConfig['Cmd'];
Entrypoint: Required<ImageInspectConfig['Entrypoint']>;
ExposedPorts: Required<ImageInspectConfig['ExposedPorts']>;
Volumes: Required<ImageInspectConfig>['Volumes'];
Env: Required<ImageInspectConfig>['Env'];
Labels: ImageInspectConfig['Labels'];
// computed fields
Used: boolean = false;
constructor(data: ImageInspect) {
this.Id = data.Id;
// this.Tag = data.Tag; // doesn't seem to be used?
this.Parent = data.Parent;
this.Created = data.Created;
// this.Repository = data.Repository; // doesn't seem to be used?
this.RepoTags = data.RepoTags;
this.Size = data.Size;
this.DockerVersion = data.DockerVersion;
this.Os = data.Os;
this.Architecture = data.Architecture;
this.Author = data.Author;
this.Command = data.Config?.Cmd;
let config: ImageInspect['Config'] = {};
if (data.Config) {
config = data.Config; // this is part of OCI images-spec
} else if (data.ContainerConfig) {
config = data.ContainerConfig; // not OCI ; has been removed in Docker 26 (API v1.45) along with .Container
}
this.Entrypoint = config.Entrypoint ?? [''];
this.ExposedPorts = config.ExposedPorts
? Object.keys(config.ExposedPorts)
: [];
this.Volumes = config.Volumes ? Object.keys(config.Volumes) : [];
this.Env = config.Env ?? [];
this.Labels = config.Labels;
}
}
-9
View File
@@ -1,9 +0,0 @@
export function ImageLayerViewModel(order, data) {
this.Order = order;
this.Id = data.Id;
this.Created = data.Created;
this.CreatedBy = data.CreatedBy;
this.Size = data.Size;
this.Comment = data.Comment;
this.Tags = data.Tags;
}
+27
View File
@@ -0,0 +1,27 @@
import { ImageLayer } from '@/react/docker/proxy/queries/images/useImageHistory';
export class ImageLayerViewModel implements ImageLayer {
Id: ImageLayer['Id'];
Created: ImageLayer['Created'];
CreatedBy: ImageLayer['CreatedBy'];
Size: ImageLayer['Size'];
Comment: ImageLayer['Comment'];
Tags: ImageLayer['Tags'];
constructor(
public Order: number,
data: ImageLayer
) {
this.Id = data.Id;
this.Created = data.Created;
this.CreatedBy = data.CreatedBy;
this.Size = data.Size;
this.Comment = data.Comment;
this.Tags = data.Tags;
}
}
+16 -4
View File
@@ -1,8 +1,21 @@
import { IPAM, Network, NetworkContainer } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerMetadata } from '@/react/docker/types';
import { IResource } from '@/react/docker/components/datatables/createOwnershipColumn';
import { PortainerResponse } from '@/react/docker/types';
// TODO later: aggregate NetworkViewModel and DockerNetwork types
//
// type MacvlanNetwork = {
// ConfigFrom?: { Network: string };
// ConfigOnly?: boolean;
// };
//
// type NetworkViewModel = Network & {
// StackName?: string;
// NodeName?: string;
// ResourceControl?: ResourceControlViewModel;
// } & MacvlanNetwork;
export class NetworkViewModel implements IResource {
Id: string;
@@ -38,8 +51,7 @@ export class NetworkViewModel implements IResource {
ResourceControl?: ResourceControlViewModel;
constructor(
data: Network & {
Portainer?: PortainerMetadata;
data: PortainerResponse<Network> & {
ConfigFrom?: { Network: string };
ConfigOnly?: boolean;
}
+1 -3
View File
@@ -10,8 +10,6 @@ import {
ResourceObject,
} from 'docker-types/generated/1.41';
import { WithRequiredProperty } from '@/types';
export class NodeViewModel {
Model: Node;
@@ -55,7 +53,7 @@ export class NodeViewModel {
Status: NodeStatus['State'];
Addr: WithRequiredProperty<NodeStatus, 'Addr'>['Addr'] = '';
Addr: Required<NodeStatus>['Addr'] = '';
Leader: ManagerStatus['Leader'];
-9
View File
@@ -1,9 +0,0 @@
// This model is based on https://github.com/moby/moby/blob/0ac25dfc751fa4304ab45afd5cd8705c2235d101/api/types/plugin.go#L8-L31
// instead of the official documentation.
// See: https://github.com/moby/moby/issues/34241
export function PluginViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Enabled = data.Enabled;
this.Config = data.Config;
}
+3 -3
View File
@@ -1,8 +1,8 @@
import { Secret } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { PortainerMetadata } from '@/react/docker/types';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerResponse } from '@/react/docker/types';
import { IResource } from '@/react/docker/components/datatables/createOwnershipColumn';
export class SecretViewModel implements IResource {
Id: string;
@@ -19,7 +19,7 @@ export class SecretViewModel implements IResource {
ResourceControl?: ResourceControlViewModel;
constructor(data: Secret & { Portainer?: PortainerMetadata }) {
constructor(data: PortainerResponse<Secret>) {
this.Id = data.ID || '';
this.CreatedAt = data.CreatedAt || '';
this.UpdatedAt = data.UpdatedAt || '';
+5 -7
View File
@@ -9,15 +9,13 @@ import {
} from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { PortainerMetadata } from '@/react/docker/types';
import { WithRequiredProperty } from '@/types';
import { PortainerResponse } from '@/react/docker/types';
import { TaskViewModel } from './task';
type ContainerSpec = WithRequiredProperty<
TaskSpec,
'ContainerSpec'
>['ContainerSpec'];
type ContainerSpec = Required<TaskSpec>['ContainerSpec'];
export type ServiceId = string;
export class ServiceViewModel {
Model: Service;
@@ -140,7 +138,7 @@ export class ServiceViewModel {
ResourceControl?: ResourceControlViewModel;
constructor(data: Service & { Portainer?: PortainerMetadata }) {
constructor(data: PortainerResponse<Service>) {
this.Model = data;
this.Id = data.ID || '';
this.Tasks = [];
-3
View File
@@ -1,3 +0,0 @@
export function SwarmViewModel(data) {
this.Id = data.ID;
}
+13 -11
View File
@@ -1,25 +1,27 @@
import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41';
import { Task } from 'docker-types/generated/1.41';
import { DeepPick } from '@/types/deepPick';
export class TaskViewModel {
Id: string;
Id: NonNullable<Task['ID']>;
Created: string;
Created: NonNullable<Task['CreatedAt']>;
Updated: string;
Updated: NonNullable<Task['UpdatedAt']>;
Slot: number;
Slot: NonNullable<Task['Slot']>;
Spec?: TaskSpec;
Spec?: Task['Spec'];
Status: Task['Status'];
Status?: Task['Status'];
DesiredState: TaskState;
DesiredState: NonNullable<Task['DesiredState']>;
ServiceId: string;
ServiceId: NonNullable<Task['ServiceID']>;
NodeId: string;
NodeId: NonNullable<Task['NodeID']>;
ContainerId: string = '';
ContainerId: DeepPick<Task, 'Status.ContainerStatus.ContainerID'>;
constructor(data: Task) {
this.Id = data.ID || '';
+12 -12
View File
@@ -1,33 +1,33 @@
import { Volume } from 'docker-types/generated/1.41';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
import { PortainerMetadata } from '@/react/docker/types';
import { IResource } from '@/react/docker/components/datatables/createOwnershipColumn';
import { PortainerResponse } from '@/react/docker/types';
export class VolumeViewModel implements IResource {
Id: string;
Id: Volume['Name'];
CreatedAt: string | undefined;
CreatedAt?: Volume['CreatedAt'];
Driver: string;
Driver: Volume['Driver'];
Options: Record<string, string>;
Options: Volume['Options'];
Labels: Record<string, string>;
Labels: Volume['Labels'];
StackName?: string;
Mountpoint: Volume['Mountpoint'];
Mountpoint: string;
// Portainer properties
ResourceId?: string;
NodeName?: string;
StackName?: string;
ResourceControl?: ResourceControlViewModel;
constructor(
data: Volume & { Portainer?: PortainerMetadata; ResourceID?: string }
) {
constructor(data: PortainerResponse<Volume> & { ResourceID?: string }) {
this.Id = data.Name;
this.CreatedAt = data.CreatedAt;
this.Driver = data.Driver;
-28
View File
@@ -1,28 +0,0 @@
import { API_ENDPOINT_ENDPOINTS } from '@/constants';
import { jsonObjectsToArrayHandler } from './response/handlers';
angular.module('portainer.docker').factory('Build', [
'$resource',
function BuildFactory($resource) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build',
{},
{
buildImage: {
method: 'POST',
ignoreLoadingBar: true,
transformResponse: jsonObjectsToArrayHandler,
isArray: true,
headers: { 'Content-Type': 'application/x-tar' },
},
buildImageOverride: {
method: 'POST',
ignoreLoadingBar: true,
transformResponse: jsonObjectsToArrayHandler,
isArray: true,
},
}
);
},
]);
-14
View File
@@ -1,14 +0,0 @@
angular.module('portainer.docker').factory('Commit', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function CommitFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/commit',
{},
{
commitContainer: { method: 'POST', params: { container: '@id', repo: '@repo' }, ignoreLoadingBar: true },
}
);
},
]);
-19
View File
@@ -1,19 +0,0 @@
angular.module('portainer.docker').factory('Config', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function ConfigFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/configs/:id/:action',
{
environmentId: '@environmentId',
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);
-73
View File
@@ -1,73 +0,0 @@
import { genericHandler, logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Container', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:environmentId/docker/containers/:id/:action',
{
name: '@name',
environmentId: '@environmentId',
},
{
query: {
method: 'GET',
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
},
get: {
method: 'GET',
params: { action: 'json' },
},
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
ignoreLoadingBar: true,
},
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
exec: {
method: 'POST',
params: { id: '@id', action: 'exec' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
inspect: {
method: 'GET',
params: { id: '@id', action: 'json' },
},
update: {
method: 'POST',
params: { id: '@id', action: 'update' },
},
prune: {
method: 'POST',
params: { action: 'prune', filters: '@filters' },
},
resize: {
method: 'POST',
params: { id: '@id', action: 'resize', h: '@height', w: '@width' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
}
);
},
]);
-24
View File
@@ -1,24 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Exec', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/exec/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
resize: {
method: 'POST',
params: { id: '@id', action: 'resize', h: '@height', w: '@width' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
}
);
},
]);
-57
View File
@@ -1,57 +0,0 @@
import { deleteImageHandler, jsonObjectsToArrayHandler } from './response/handlers';
import { imageGetResponse } from './response/image';
angular.module('portainer.docker').factory('Image', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'HttpRequestHelper',
function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },
insert: { method: 'POST', params: { id: '@id', action: 'insert' } },
tag: { method: 'POST', params: { id: '@id', action: 'tag', force: 0, repo: '@repo' }, ignoreLoadingBar: true },
inspect: { method: 'GET', params: { id: '@id', action: 'json' } },
push: {
method: 'POST',
params: { action: 'push', id: '@imageName' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader },
ignoreLoadingBar: true,
},
create: {
method: 'POST',
params: { action: 'create', fromImage: '@fromImage' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader },
ignoreLoadingBar: true,
},
download: {
method: 'GET',
params: { action: 'get', names: '@names' },
transformResponse: imageGetResponse,
responseType: 'blob',
ignoreLoadingBar: true,
},
remove: {
method: 'DELETE',
params: { id: '@id', force: '@force' },
isArray: true,
transformResponse: deleteImageHandler,
},
}
);
},
]);
-44
View File
@@ -1,44 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Network', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action',
{
id: '@id',
endpointId: EndpointProvider.endpointID,
},
{
query: {
method: 'GET',
isArray: true,
},
get: {
method: 'GET',
},
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
},
remove: {
method: 'DELETE',
transformResponse: genericHandler,
},
connect: {
method: 'POST',
params: { action: 'connect' },
},
disconnect: {
method: 'POST',
params: { action: 'disconnect' },
},
}
);
},
]);
-20
View File
@@ -1,20 +0,0 @@
angular.module('portainer.docker').factory('Node', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function NodeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/nodes/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'POST', params: { id: '@id', action: 'update', version: '@version' } },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);
-17
View File
@@ -1,17 +0,0 @@
angular.module('portainer.docker').factory('Plugin', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function PluginFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/plugins/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', isArray: true },
}
);
},
]);
-81
View File
@@ -1,81 +0,0 @@
function isJSONArray(jsonString) {
return Object.prototype.toString.call(jsonString) === '[object Array]';
}
function isJSON(jsonString) {
try {
var o = JSON.parse(jsonString);
if (o && typeof o === 'object') {
return o;
}
} catch (e) {
//empty
}
return false;
}
// The Docker API often returns a list of JSON object.
// This handler wrap the JSON objects in an array.
// Used by the API in: Image push, Image create, Events query.
export function jsonObjectsToArrayHandler(data) {
// catching empty data helps the function not to fail and prevents unwanted error message to user.
if (!data) {
return [];
}
var str = '[' + data.replace(/\n/g, ' ').replace(/\}\s*\{/g, '}, {') + ']';
return angular.fromJson(str);
}
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12)
// This handler ensure a valid JSON object is returned in any case.
// Used by the API in: container deletion, network deletion, network creation, volume creation,
// container exec, exec resize.
export function genericHandler(data) {
var response = {};
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
if (!data) {
return response;
}
// A string is returned on failure (Docker < 1.12)
else if (!isJSON(data)) {
response.message = data;
}
// Docker 1.12 returns a valid JSON object when an error occurs
else {
response = angular.fromJson(data);
}
return response;
}
// The Docker API returns the logs as a single string.
// This handler wraps the data in a JSON object under the "logs" property.
export function logsHandler(data) {
return {
logs: data,
};
}
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
// This handler returns the original array on success or a newly created array containing
// only one JSON object with the field message filled with the error message on failure.
export function deleteImageHandler(data) {
// A string is returned on failure (Docker < 1.12)
var response = [];
if (!isJSON(data)) {
response.push({ message: data });
}
// A JSON object is returned on failure (Docker = 1.12)
else if (!isJSONArray(data)) {
var json = angular.fromJson(data);
response.push(json);
}
// An array is returned on success (Docker 1.9 -> 1.12)
else {
response = angular.fromJson(data);
}
return response;
}
-9
View File
@@ -1,9 +0,0 @@
// The get action of the Image service returns a file.
// ngResource will transform it as an array of chars.
// This functions simply creates a response object and assign
// the data to a field.
export function imageGetResponse(data) {
var response = {};
response.file = data;
return response;
}
-20
View File
@@ -1,20 +0,0 @@
angular.module('portainer.docker').factory('Secret', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SecretFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/secrets/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: { action: 'create' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id' } },
}
);
},
]);
-45
View File
@@ -1,45 +0,0 @@
import { logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Service', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'HttpRequestHelper',
function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true, params: { filters: '@filters' } },
create: {
method: 'POST',
params: { action: 'create' },
headers: {
'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader,
version: '1.29',
},
ignoreLoadingBar: true,
},
update: {
method: 'POST',
params: { id: '@id', action: 'update', version: '@version', rollback: '@rollback' },
headers: {
'X-Registry-Auth': (config) => btoa(JSON.stringify({ registryId: config.data.registryId })),
version: '1.29',
},
},
remove: { method: 'DELETE', params: { id: '@id' } },
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
}
);
},
]);
-17
View File
@@ -1,17 +0,0 @@
angular.module('portainer.docker').factory('Swarm', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SwarmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/swarm',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET' },
}
);
},
]);
-32
View File
@@ -1,32 +0,0 @@
import { jsonObjectsToArrayHandler } from './response/handlers';
angular.module('portainer.docker').factory('System', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction',
{
name: '@name',
endpointId: EndpointProvider.endpointID,
},
{
info: {
method: 'GET',
params: { action: 'info' },
},
version: { method: 'GET', params: { action: 'version' } },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },
isArray: true,
transformResponse: jsonObjectsToArrayHandler,
},
auth: { method: 'POST', params: { action: 'auth' } },
dataUsage: { method: 'GET', params: { action: 'system', subAction: 'df' } },
}
);
},
]);
-26
View File
@@ -1,26 +0,0 @@
import { logsHandler } from './response/handlers';
angular.module('portainer.docker').factory('Task', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true, params: { filters: '@filters' } },
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
}
);
},
]);
-37
View File
@@ -1,37 +0,0 @@
import { genericHandler } from './response/handlers';
angular.module('portainer.docker').factory('Volume', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
function addVolumeNameToHeader(config) {
return config.data.Name || '';
}
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET' },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',
params: { action: 'create' },
transformResponse: genericHandler,
ignoreLoadingBar: true,
headers: { 'X-Portainer-VolumeName': addVolumeNameToHeader },
},
remove: {
method: 'DELETE',
transformResponse: genericHandler,
params: { id: '@id' },
},
}
);
},
]);
+57 -83
View File
@@ -1,91 +1,65 @@
import { ImageBuildModel } from '../models/image';
import {
buildImageFromDockerfileContent,
buildImageFromDockerfileContentAndFiles,
buildImageFromURL,
buildImageFromUpload,
} from '@/react/docker/images/queries/useBuildImageMutation';
angular.module('portainer.docker').factory('BuildService', [
'$q',
'Build',
'FileUploadService',
function BuildServiceFactory($q, Build, FileUploadService) {
'use strict';
var service = {};
import { ImageBuildModel } from '../models/build';
service.buildImageFromUpload = function (endpointID, names, file, path) {
var deferred = $q.defer();
angular.module('portainer.docker').factory('BuildService', BuildServiceFactory);
FileUploadService.buildImage(endpointID, names, file, path)
.then(function success(response) {
var model = new ImageBuildModel(response.data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
/* @ngInject */
function BuildServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
return deferred.promise;
};
return {
buildImageFromUpload: useAxios(buildImageFromUploadAngularJS), // build image
buildImageFromURL: useAxios(buildImageFromURLAngularJS), // build image
buildImageFromDockerfileContent: useAxios(buildImageFromDockerfileContentAngularJS), // build image
buildImageFromDockerfileContentAndFiles: useAxios(buildImageFromDockerfileContentAndFilesAngularJS), // build image
};
service.buildImageFromURL = function (endpointId, names, url, path) {
var params = {
endpointId,
t: names,
remote: url,
dockerfile: path,
};
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {File} file
* @param {string} path
*/
async function buildImageFromUploadAngularJS(environmentId, names, file, path) {
const data = await buildImageFromUpload(environmentId, names, file, path);
return new ImageBuildModel(data);
}
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} url
* @param {string} path
*/
async function buildImageFromURLAngularJS(environmentId, names, url, path) {
const data = await buildImageFromURL(environmentId, names, url, path);
return new ImageBuildModel(data);
}
Build.buildImage(params, {})
.$promise.then(function success(data) {
var model = new ImageBuildModel(data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} content
*/
async function buildImageFromDockerfileContentAngularJS(environmentId, names, content) {
const data = await buildImageFromDockerfileContent(environmentId, names, content);
return new ImageBuildModel(data);
}
return deferred.promise;
};
service.buildImageFromDockerfileContent = function (endpointId, names, content) {
var params = {
endpointId,
t: names,
};
var payload = {
content: content,
};
var deferred = $q.defer();
Build.buildImageOverride(params, payload)
.$promise.then(function success(data) {
var model = new ImageBuildModel(data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.buildImageFromDockerfileContentAndFiles = function (endpointID, names, content, files) {
var dockerfile = new Blob([content], { type: 'text/plain' });
var uploadFiles = [dockerfile].concat(files);
var deferred = $q.defer();
FileUploadService.buildImageFromFiles(endpointID, names, uploadFiles)
.then(function success(response) {
var model = new ImageBuildModel(response.data);
deferred.resolve(model);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {string[]} names
* @param {string} content
* @param {File[]} files
*/
async function buildImageFromDockerfileContentAndFilesAngularJS(environmentId, names, content, files) {
const data = await buildImageFromDockerfileContentAndFiles(environmentId, names, content, files);
return new ImageBuildModel(data);
}
}
+31 -60
View File
@@ -1,66 +1,37 @@
import { getConfig } from '@/react/docker/configs/queries/useConfig';
import { getConfigs } from '@/react/docker/configs/queries/useConfigs';
import { deleteConfig } from '@/react/docker/configs/queries/useDeleteConfigMutation';
import { createConfig } from '@/react/docker/configs/queries/useCreateConfigMutation';
import { ConfigViewModel } from '../models/config';
angular.module('portainer.docker').factory('ConfigService', [
'$q',
'Config',
function ConfigServiceFactory($q, Config) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ConfigService', ConfigServiceFactory);
service.config = function (environmentId, configId) {
var deferred = $q.defer();
/* @ngInspect */
function ConfigServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
Config.get({ id: configId, environmentId })
.$promise.then(function success(data) {
var config = new ConfigViewModel(data);
deferred.resolve(config);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve config details', err: err });
});
return {
configs: useAxios(listConfigsAngularJS), // config list + service create + service edit
config: useAxios(getConfigAngularJS), // config create + config edit
remove: useAxios(deleteConfig), // config list + config edit
create: useAxios(createConfig), // config create
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId
*/
async function listConfigsAngularJS(environmentId) {
const data = await getConfigs(environmentId);
return data.map((c) => new ConfigViewModel(c));
}
service.configs = function (environmentId) {
var deferred = $q.defer();
Config.query({ environmentId })
.$promise.then(function success(data) {
var configs = data.map(function (item) {
return new ConfigViewModel(item);
});
deferred.resolve(configs);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve configs', err: err });
});
return deferred.promise;
};
service.remove = function (environmentId, configId) {
var deferred = $q.defer();
Config.remove({ environmentId, id: configId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove config', err: err });
});
return deferred.promise;
};
service.create = function (environmentId, config) {
return Config.create({ environmentId }, config).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {ConfigId} configId
*/
async function getConfigAngularJS(environmentId, configId) {
const data = await getConfig(environmentId, configId);
return new ConfigViewModel(data);
}
}
+111 -178
View File
@@ -9,196 +9,129 @@ import {
startContainer,
stopContainer,
recreateContainer,
getContainerLogs,
} from '@/react/docker/containers/containers.service';
import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container';
import { getContainers } from '@/react/docker/containers/queries/useContainers';
import { getContainer } from '@/react/docker/containers/queries/useContainer';
import { resizeTTY } from '@/react/docker/containers/queries/useContainerResizeTTYMutation';
import { updateContainer } from '@/react/docker/containers/queries/useUpdateContainer';
import { createExec } from '@/react/docker/containers/queries/useCreateExecMutation';
import { containerStats } from '@/react/docker/containers/queries/useContainerStats';
import { containerTop } from '@/react/docker/containers/queries/useContainerTop';
import { createOrReplace } from '@/react/docker/containers/CreateView/useCreateMutation';
import { toReactAccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { ContainerDetailsViewModel } from '../models/containerDetails';
import { ContainerStatsViewModel } from '../models/containerStats';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('ContainerService', ContainerServiceFactory);
/* @ngInject */
function ContainerServiceFactory($q, Container, $timeout) {
const service = {
killContainer,
pauseContainer,
renameContainer,
restartContainer,
resumeContainer,
startContainer,
stopContainer,
recreateContainer,
remove: removeContainer,
updateRestartPolicy,
updateLimits,
function ContainerServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
return {
killContainer: useAxios(killContainer), // container edit
pauseContainer: useAxios(pauseContainer), // container edit
renameContainer: useAxios(renameContainer), // container edit
restartContainer: useAxios(restartContainer), // container edit
resumeContainer: useAxios(resumeContainer), // container edit
startContainer: useAxios(startContainer), // container edit
stopContainer: useAxios(stopContainer), // container edit
recreateContainer: useAxios(recreateContainer), // container edit
remove: useAxios(removeContainer), // container edit
container: useAxios(getContainerAngularJS), // container console + container edit + container stats
containers: useAxios(getContainers), // dashboard + services list + service edit + voluem edit + stackservice + stack create + stack edit
resizeTTY: useAxios(resizeTTYAngularJS), // container console
updateRestartPolicy: useAxios(updateRestartPolicyAngularJS), // container edit
createExec: useAxios(createExec), // container console
containerStats: useAxios(containerStatsAngularJS), // container stats
containerTop: useAxios(containerTop), // container stats
inspect: useAxios(getContainer), // container inspect
createAndStartContainer: useAxios(createAndStartContainer), // templates
logs: useAxios(containerLogsAngularJS), // container logs
};
service.container = function (environmentId, id) {
var deferred = $q.defer();
Container.get({ environmentId, id })
.$promise.then(function success(data) {
var container = new ContainerDetailsViewModel(data);
deferred.resolve(container);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve container information', err: err });
});
return deferred.promise;
};
service.containers = function (environmentId, all, filters) {
var deferred = $q.defer();
Container.query({ environmentId, all, filters })
.$promise.then(function success(data) {
var containers = data.map(function (item) {
return new ContainerViewModel(item);
});
deferred.resolve(containers);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve containers', err: err });
});
return deferred.promise;
};
service.resizeTTY = function (environmentId, id, width, height, timeout) {
var deferred = $q.defer();
$timeout(function () {
Container.resize({}, { environmentId, id, width, height })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to resize tty of container ' + id, err: err });
});
}, timeout);
return deferred.promise;
};
function updateRestartPolicy(environmentId, id, restartPolicy, maximumRetryCounts) {
return Container.update({ environmentId, id }, { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } }).$promise;
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
* @param {*} param2
*/
async function getContainerAngularJS(environmentId, id, { nodeName } = {}) {
const data = await getContainer(environmentId, id, { nodeName });
return new ContainerDetailsViewModel(data);
}
function updateLimits(environmentId, id, config) {
return Container.update(
{ environmentId, id },
{
// MemorySwap: must be set
// -1: non limits, 0: treated as unset(cause update error).
MemoryReservation: config.HostConfig.MemoryReservation,
Memory: config.HostConfig.Memory,
MemorySwap: -1,
NanoCpus: config.HostConfig.NanoCpus,
}
).$promise;
/**
* @param {EnvironmentId} environmentId
* @param {string} containerId
* @param {number} width
* @param {number} height
* @param timeout DEPRECATED: Previously used in pure AJS implementation
*/
async function resizeTTYAngularJS(environmentId, containerId, width, height) {
return resizeTTY(environmentId, containerId, { width, height });
}
service.createContainer = function (environmentId, configuration) {
var deferred = $q.defer();
Container.create({ environmentId }, configuration)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create container', err: err });
});
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
* @param {RestartPolicy['Name']} restartPolicy
* @param {RestartPolicy['MaximumRetryCount']} maximumRetryCounts
*/
async function updateRestartPolicyAngularJS(environmentId, id, restartPolicy, maximumRetryCounts) {
return updateContainer(environmentId, id, {
RestartPolicy: {
Name: restartPolicy,
MaximumRetryCount: maximumRetryCounts,
},
});
}
service.createAndStartContainer = function (environmentId, configuration) {
var deferred = $q.defer();
var container;
service
.createContainer(environmentId, configuration)
.then(function success(data) {
container = data;
return service.startContainer(environmentId, container.Id);
})
.then(function success() {
deferred.resolve(container);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
/**
* @param {Environment} environment
* @param {*} configuration
* @param {AccessControlFormData} accessControlFormData
*/
async function createAndStartContainer(environment, configuration, accessControlFormData) {
return createOrReplace({
config: configuration,
environment,
values: {
name: configuration.name,
imageName: configuration.Image,
accessControl: toReactAccessControlFormData(accessControlFormData),
},
});
}
service.createExec = function (environmentId, execConfig) {
var deferred = $q.defer();
Container.exec({ environmentId }, execConfig)
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.logs = function (environmentId, id, stdout, stderr, timestamps, since, tail, stripHeaders) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
environmentId,
};
Container.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.containerStats = function (environmentId, id) {
var deferred = $q.defer();
Container.stats({ environmentId, id })
.$promise.then(function success(data) {
var containerStats = new ContainerStatsViewModel(data);
deferred.resolve(containerStats);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.containerTop = function (environmentId, id) {
return Container.top({ environmentId, id }).$promise;
};
service.inspect = function (environmentId, id) {
return Container.inspect({ environmentId, id }).$promise;
};
service.prune = function (environmentId, filters) {
return Container.prune({ environmentId, filters }).$promise;
};
return service;
/**
* @param {EnvironmentId} environmentId
* @param {ContainerId} id
*/
async function containerStatsAngularJS(environmentId, id) {
const data = await containerStats(environmentId, id);
return new ContainerStatsViewModel(data);
}
/**
* @param {EnvironmentId} environmentId
* @param {Containerid} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
* @param {boolean?} stripHeaders
*/
async function containerLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all', stripHeaders) {
const data = await getContainerLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders, withTimestamps: !!timestamps });
}
}
+19 -27
View File
@@ -1,31 +1,23 @@
angular.module('portainer.docker').factory('ExecService', [
'$q',
'$timeout',
'Exec',
function ExecServiceFactory($q, $timeout, Exec) {
'use strict';
var service = {};
import { resizeTTY } from '@/react/docker/proxy/queries/useExecResizeTTYMutation';
service.resizeTTY = function (execId, width, height, timeout) {
var deferred = $q.defer();
angular.module('portainer.docker').factory('ExecService', ExecServiceFactory);
$timeout(function () {
Exec.resize({}, { id: execId, height: height, width: width })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: 'Unable to resize tty of exec', err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to resize tty of exec', err: err });
});
}, timeout);
/* @ngInject */
function ExecServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
return deferred.promise;
};
return {
resizeTTY: useAxios(injectEnvironmentId(resizeTTYAngularJS)),
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} execId
* @param {number} width
* @param {number} height
* @param timeout DEPRECATED: Previously used in pure AJS implementation
*/
async function resizeTTYAngularJS(environmentId, execId, width, height) {
return resizeTTY(environmentId, execId, { width, height });
}
}
+76 -193
View File
@@ -1,208 +1,91 @@
import _ from 'lodash';
import { groupBy } from 'lodash';
import { getUniqueTagListFromImages } from '@/react/docker/images/utils';
import { getImage } from '@/react/docker/proxy/queries/images/useImage';
import { parseAxiosError } from '@/portainer/services/axios';
import { getImages } from '@/react/docker/proxy/queries/images/useImages';
import { getContainers } from '@/react/docker/containers/queries/useContainers';
import { getImageHistory } from '@/react/docker/proxy/queries/images/useImageHistory';
import { pullImage } from '@/react/docker/images/queries/usePullImageMutation';
import { pushImage } from '@/react/docker/images/queries/usePushImageMutation';
import { removeImage } from '@/react/docker/proxy/queries/images/useRemoveImageMutation';
import { tagImage } from '@/react/docker/proxy/queries/images/useTagImageMutation';
import { downloadImages } from '@/react/docker/proxy/queries/images/useDownloadImages';
import { uploadImages } from '@/react/docker/proxy/queries/images/useUploadImageMutation';
import { ImageViewModel } from '../models/image';
import { ImageDetailsViewModel } from '../models/imageDetails';
import { ImageLayerViewModel } from '../models/imageLayer';
angular.module('portainer.docker').factory('ImageService', [
'$q',
'Image',
'ImageHelper',
'RegistryService',
'HttpRequestHelper',
'ContainerService',
'FileUploadService',
function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService, FileUploadService) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ImageService', ImageServiceFactory);
service.image = function (imageId) {
var deferred = $q.defer();
Image.get({ id: imageId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var image = new ImageDetailsViewModel(data);
deferred.resolve(image);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
/* @ngInject */
function ImageServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
service.images = function ({ environmentId, withUsage } = {}) {
var deferred = $q.defer();
return {
image: useAxios(injectEnvironmentId(imageAngularJS)), // container console + image edit
images: useAxios(injectEnvironmentId(imagesAngularJS)), // por image registry controller + dashboard + service edit
history: useAxios(injectEnvironmentId(historyAngularJS)), // image edit
pushImage: useAxios(injectEnvironmentId(pushImageAngularJS)), // image edit
pullImage: useAxios(injectEnvironmentId(pullImageAngularJS)), // images list + image edit + templates list
tagImage: useAxios(injectEnvironmentId(tagImage)), // image edit + image import
downloadImages: useAxios(injectEnvironmentId(downloadImages)), // image list + image edit
uploadImage: useAxios(injectEnvironmentId(uploadImages)), // image import
deleteImage: useAxios(injectEnvironmentId(removeImage)), // image list + image edit
getUniqueTagListFromImages, // por image registry controller + service edit
};
$q.all({
containers: withUsage ? ContainerService.containers(environmentId, 1) : [],
images: Image.query({}).$promise,
})
.then(function success(data) {
var containers = data.containers;
const containerByImageId = _.groupBy(containers, 'ImageID');
async function imageAngularJS(environmentId, imageId) {
const image = await getImage(environmentId, imageId);
return new ImageDetailsViewModel(image);
}
var images = data.images.map(function (item) {
item.Used = !!containerByImageId[item.Id] && containerByImageId[item.Id].length > 0;
return new ImageViewModel(item);
});
deferred.resolve(images);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve images', err: err });
});
return deferred.promise;
};
service.history = function (imageId) {
var deferred = $q.defer();
Image.history({ id: imageId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var layers = [];
var order = data.length;
angular.forEach(data, function (imageLayer) {
layers.push(new ImageLayerViewModel(order, imageLayer));
order--;
});
deferred.resolve(layers);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
service.pushImage = pushImage;
/**
*
* @param {PorImageRegistryModel} registryModel
*/
function pushImage(registryModel) {
var deferred = $q.defer();
var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
const imageConfiguration = ImageHelper.createImageConfigForContainer(registryModel);
Image.push({ imageName: imageConfiguration.fromImage })
.$promise.then(function success(data) {
if (data[data.length - 1].error) {
deferred.reject({ msg: data[data.length - 1].error });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to push image tag', err: err });
});
return deferred.promise;
async function imagesAngularJS(environmentId, withUsage) {
try {
const [containers, images] = await Promise.all([withUsage ? getContainers(environmentId) : [], getImages(environmentId)]);
const containerByImageId = groupBy(containers, 'ImageID');
return images.map((item) => new ImageViewModel(item, !!containerByImageId[item.Id] && containerByImageId[item.Id].length > 0));
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve images');
}
}
/**
* PULL IMAGE
*/
function pullImageAndIgnoreErrors(imageConfiguration) {
var deferred = $q.defer();
Image.create({}, imageConfiguration)
.$promise.catch(() => {
// left empty to ignore errors
})
.finally(function final() {
deferred.resolve();
});
return deferred.promise;
async function historyAngularJS(environmentId, imageId) {
try {
const layers = await getImageHistory(environmentId, imageId);
return layers.reverse().map((layer, idx) => new ImageLayerViewModel(idx, layer));
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve image history');
}
}
function pullImageAndAcknowledgeErrors(imageConfiguration) {
var deferred = $q.defer();
/**
* type PorImageRegistryModel = {
* UseRegistry: bool;
* Registry?: Registry;
* Image: string;
* }
*/
Image.create({}, imageConfiguration)
.$promise.then(function success(data) {
var err = data.length > 0 && data[data.length - 1].message;
if (err) {
var detail = data[data.length - 1];
deferred.reject({ msg: detail.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to pull image', err: err });
});
/**
* @param {EnvironmentId} environmentId Autofilled by AngularToReact
* @param {PorImageRegistryModel} registryModel
*/
async function pushImageAngularJS(environmentId, registryModel) {
const { UseRegistry, Registry, Image } = registryModel;
const registry = UseRegistry ? Registry : undefined;
return pushImage({ environmentId, image: Image, registry });
}
return deferred.promise;
}
service.pullImage = pullImage;
/**
*
* @param {PorImageRegistryModel} registry
* @param {bool} ignoreErrors
*/
function pullImage(registry, ignoreErrors) {
var authenticationDetails = registry.Registry.Authentication ? RegistryService.encodedCredentials(registry.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
var imageConfiguration = ImageHelper.createImageConfigForContainer(registry);
if (ignoreErrors) {
return pullImageAndIgnoreErrors(imageConfiguration);
}
return pullImageAndAcknowledgeErrors(imageConfiguration);
}
/**
* ! PULL IMAGE
*/
service.tagImage = function (id, image) {
return Image.tag({ id: id, repo: image }).$promise;
};
/**
*
* @param {Array<{tags: Array<string>; id: string;}>} images
* @returns {Promise<unknown>}
*/
service.downloadImages = function (images) {
var names = ImageHelper.getImagesNamesForDownload(images);
return Image.download(names).$promise;
};
service.uploadImage = function (file) {
return FileUploadService.loadImages(file);
};
service.deleteImage = function (id, forceRemoval) {
var deferred = $q.defer();
Image.remove({ id: id, force: forceRemoval })
.$promise.then(function success(data) {
if (data[0].message) {
deferred.reject({ msg: data[0].message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove image', err: err });
});
return deferred.promise;
};
service.getUniqueTagListFromImages = getUniqueTagListFromImages;
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Autofilled by AngularToReact
* @param {PorImageRegistryModel} registryModel
* @param {string?} nodeName
*/
async function pullImageAngularJS(environmentId, registryModel, nodeName) {
const { UseRegistry, Registry, Image } = registryModel;
const registry = UseRegistry ? Registry : undefined;
return pullImage({ environmentId, image: Image, nodeName, registry });
}
}
+50 -68
View File
@@ -1,75 +1,57 @@
import { createNetwork } from '@/react/docker/networks/queries/useCreateNetworkMutation';
import { getNetwork } from '@/react/docker/networks/queries/useNetwork';
import { getNetworks } from '@/react/docker/networks/queries/useNetworks';
import { deleteNetwork } from '@/react/docker/networks/queries/useDeleteNetworkMutation';
import { disconnectContainer } from '@/react/docker/networks/queries/useDisconnectContainerMutation';
import { connectContainer } from '@/react/docker/networks/queries/useConnectContainerMutation';
import { NetworkViewModel } from '../models/network';
angular.module('portainer.docker').factory('NetworkService', [
'$q',
'Network',
function NetworkServiceFactory($q, Network) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('NetworkService', NetworkServiceFactory);
service.create = function (networkConfiguration) {
var deferred = $q.defer();
/* @ngInject */
function NetworkServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Network.create(networkConfiguration)
.$promise.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create network', err: err });
});
return deferred.promise;
};
return {
create: useAxios(injectEnvironmentId(createNetwork)), // create network
network: useAxios(injectEnvironmentId(networkAngularJS)), // service edit
networks: useAxios(injectEnvironmentId(networksAngularJS)), // macvlan form + container edit + dashboard + service create + service edit + custom templates list + templates list
remove: useAxios(injectEnvironmentId(deleteNetwork)), // networks list
disconnectContainer: useAxios(injectEnvironmentId(disconnectContainer)), // container edit
connectContainer: useAxios(injectEnvironmentId(connectContainerAngularJS)), // container edit
};
service.network = function (id) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {NetworkId} networkId
* @param {string?} nodeName
* @returns NetworkViewModel
*/
async function networkAngularJS(environmentId, networkId, nodeName) {
const data = await getNetwork(environmentId, networkId, { nodeName });
return new NetworkViewModel(data);
}
Network.get({ id: id })
.$promise.then(function success(data) {
var network = new NetworkViewModel(data);
deferred.resolve(network);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve network details', err: err });
});
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {boolean?} localNetworks
* @param {boolean?} swarmNetworks
* @param {boolean?} swarmAttachableNetworks
* @param {*} filters
* @returns NetworkViewModel[]
*/
async function networksAngularJS(environmentId, local, swarm, swarmAttachable, filters) {
const data = await getNetworks(environmentId, { local, swarm, swarmAttachable, filters });
return data.map((n) => new NetworkViewModel(n));
}
return deferred.promise;
};
service.networks = function (localNetworks, swarmNetworks, swarmAttachableNetworks, filters) {
var deferred = $q.defer();
Network.query({ filters: filters })
.$promise.then(function success(data) {
var networks = data;
var filteredNetworks = networks
.filter(function (network) {
if (localNetworks && network.Scope === 'local') {
return network;
}
if (swarmNetworks && network.Scope === 'swarm') {
return network;
}
if (swarmAttachableNetworks && network.Scope === 'swarm' && network.Attachable === true) {
return network;
}
})
.map(function (item) {
return new NetworkViewModel(item);
});
deferred.resolve(filteredNetworks);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve networks', err: err });
});
return deferred.promise;
};
service.remove = function (id) {
return Network.remove({ id: id }).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId filled by AngularToReact
* @param {NetworkId} networkId
* @param {ContainerId} containerId
*/
async function connectContainerAngularJS(environmentId, networkId, containerId) {
return connectContainer({ environmentId, containerId, networkId });
}
}
+36 -66
View File
@@ -1,73 +1,43 @@
import { getNode } from '@/react/docker/proxy/queries/nodes/useNode';
import { getNodes } from '@/react/docker/proxy/queries/nodes/useNodes';
import { updateNode } from '@/react/docker/proxy/queries/nodes/useUpdateNodeMutation';
import { NodeViewModel } from '../models/node';
angular.module('portainer.docker').factory('NodeService', [
'$q',
'Node',
function NodeServiceFactory($q, Node) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('NodeService', NodeServiceFactory);
service.nodes = nodes;
service.node = node;
service.updateNode = updateNode;
service.getActiveManager = getActiveManager;
/* @ngInject */
function NodeServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
function node(id) {
var deferred = $q.defer();
Node.get({ id: id })
.$promise.then(function onNodeLoaded(rawNode) {
var node = new NodeViewModel(rawNode);
return deferred.resolve(node);
})
.catch(function onFailed(err) {
deferred.reject({ msg: 'Unable to retrieve node', err: err });
});
return {
nodes: useAxios(injectEnvironmentId(nodesAngularJS)), // macvlan form + services list + service create + service edit + swarm visualizer + stack edit
node: useAxios(injectEnvironmentId(nodeAngularJS)), // node browser + node details
updateNode: useAxios(injectEnvironmentId(updateNodeAngularJS)), // swarm node details panel
};
return deferred.promise;
}
/**
* @param {EnvironmentId} environmentId
* @param {NodeId} id
*/
async function nodeAngularJS(environmentId, id) {
const data = await getNode(environmentId, id);
return new NodeViewModel(data);
}
function nodes() {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId
*/
async function nodesAngularJS(environmentId) {
const data = await getNodes(environmentId);
return data.map((n) => new NodeViewModel(n));
}
Node.query({})
.$promise.then(function success(data) {
var nodes = data.map(function (item) {
return new NodeViewModel(item);
});
deferred.resolve(nodes);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
});
return deferred.promise;
}
function updateNode(node) {
return Node.update({ id: node.Id, version: node.Version }, node).$promise;
}
function getActiveManager() {
var deferred = $q.defer();
service
.nodes()
.then(function success(data) {
for (var i = 0; i < data.length; ++i) {
var node = data[i];
if (node.Role === 'manager' && node.Availability === 'active' && node.Status === 'ready' && node.Addr !== '0.0.0.0') {
deferred.resolve(node);
break;
}
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
});
return deferred.promise;
}
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {NodeSpec & { Id: string; Version: number }} nodeConfig
*/
async function updateNodeAngularJS(environmentId, nodeConfig) {
return updateNode(environmentId, nodeConfig.Id, nodeConfig, nodeConfig.Version);
}
}
+43 -68
View File
@@ -1,76 +1,51 @@
import _ from 'lodash-es';
import { PluginViewModel } from '../models/plugin';
import { isFulfilled } from '@/portainer/helpers/promise-utils';
import { getInfo } from '@/react/docker/proxy/queries/useInfo';
import { aggregateData, getPlugins } from '@/react/docker/proxy/queries/useServicePlugins';
angular.module('portainer.docker').factory('PluginService', [
'$q',
'Plugin',
'SystemService',
function PluginServiceFactory($q, Plugin, SystemService) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('PluginService', PluginServiceFactory);
service.plugins = function () {
var deferred = $q.defer();
var plugins = [];
/* @ngInject */
function PluginServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Plugin.query({})
.$promise.then(function success(data) {
for (var i = 0; i < data.length; i++) {
var plugin = new PluginViewModel(data[i]);
plugins.push(plugin);
}
})
.finally(function final() {
deferred.resolve(plugins);
});
return {
volumePlugins: useAxios(injectEnvironmentId(volumePlugins)), // volume create
networkPlugins: useAxios(injectEnvironmentId(networksPlugins)), // network create
loggingPlugins: useAxios(injectEnvironmentId(loggingPlugins)), // service create + service edit
};
}
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function volumePlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Volume');
}
function servicePlugins(systemOnly, pluginType, pluginVersion) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function networksPlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Network');
}
$q.all({
system: SystemService.plugins(),
plugins: systemOnly ? [] : service.plugins(),
})
.then(function success(data) {
var aggregatedPlugins = [];
var systemPlugins = data.system;
var plugins = data.plugins;
/**
* @param {EnvironmentId} environmentId Injected
* @param {boolean} systemOnly
*/
async function loggingPlugins(environmentId, systemOnly) {
const { systemPluginsData, pluginsData } = await getAllPlugins(environmentId);
return aggregateData(systemPluginsData, pluginsData, systemOnly, 'Log');
}
if (systemPlugins[pluginType]) {
aggregatedPlugins = aggregatedPlugins.concat(systemPlugins[pluginType]);
}
async function getAllPlugins(environmentId) {
const [system, plugins] = await Promise.allSettled([getInfo(environmentId), getPlugins(environmentId)]);
const systemPluginsData = isFulfilled(system) ? system.value.Plugins : undefined;
const pluginsData = isFulfilled(plugins) ? plugins.value : undefined;
for (var i = 0; i < plugins.length; i++) {
var plugin = plugins[i];
if (plugin.Enabled && _.includes(plugin.Config.Interface.Types, pluginVersion)) {
aggregatedPlugins.push(plugin.Name);
}
}
deferred.resolve(aggregatedPlugins);
})
.catch(function error(err) {
deferred.reject({ msg: err.msg, err: err });
});
return deferred.promise;
}
service.volumePlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Volume', 'docker.volumedriver/1.0');
};
service.networkPlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Network', 'docker.networkdriver/1.0');
};
service.loggingPlugins = function (systemOnly) {
return servicePlugins(systemOnly, 'Log', 'docker.logdriver/1.0');
};
return service;
},
]);
return { systemPluginsData, pluginsData };
}
+31 -60
View File
@@ -1,66 +1,37 @@
import { getSecret } from '@/react/docker/proxy/queries/secrets/useSecret';
import { getSecrets } from '@/react/docker/proxy/queries/secrets/useSecrets';
import { removeSecret } from '@/react/docker/proxy/queries/secrets/useRemoveSecretMutation';
import { createSecret } from '@/react/docker/proxy/queries/secrets/useCreateSecretMutation';
import { SecretViewModel } from '../models/secret';
angular.module('portainer.docker').factory('SecretService', [
'$q',
'Secret',
function SecretServiceFactory($q, Secret) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SecretService', SecretServiceFactory);
service.secret = function (secretId) {
var deferred = $q.defer();
/* @ngInject */
function SecretServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Secret.get({ id: secretId })
.$promise.then(function success(data) {
var secret = new SecretViewModel(data);
deferred.resolve(secret);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve secret details', err: err });
});
return {
secret: useAxios(injectEnvironmentId(secretAngularJS)), // secret edit
secrets: useAxios(injectEnvironmentId(secretsAngularJS)), // secret list + service create + service edit
remove: useAxios(injectEnvironmentId(removeSecret)), // secret list + secret edit
create: useAxios(injectEnvironmentId(createSecret)), // secret create
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {SecretId} id
*/
async function secretAngularJS(environmentId, id) {
const data = await getSecret(environmentId, id);
return new SecretViewModel(data);
}
service.secrets = function () {
var deferred = $q.defer();
Secret.query({})
.$promise.then(function success(data) {
var secrets = data.map(function (item) {
return new SecretViewModel(item);
});
deferred.resolve(secrets);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve secrets', err: err });
});
return deferred.promise;
};
service.remove = function (secretId) {
var deferred = $q.defer();
Secret.remove({ id: secretId })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove secret', err: err });
});
return deferred.promise;
};
service.create = function (secretConfig) {
return Secret.create(secretConfig).$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
*/
async function secretsAngularJS(environmentId) {
const data = await getSecrets(environmentId);
return data.map((s) => new SecretViewModel(s));
}
}
+88 -90
View File
@@ -1,100 +1,98 @@
import { formatLogs } from '../helpers/logHelper';
import { removeService } from '@/react/docker/services/ListView/ServicesDatatable/useRemoveServicesMutation';
import { createService } from '@/react/docker/services/queries/useCreateServiceMutation';
import { getService } from '@/react/docker/services/queries/useService';
import { getServices } from '@/react/docker/services/queries/useServices';
import { updateService } from '@/react/docker/services/queries/useUpdateServiceMutation';
import { getServiceLogs } from '@/react/docker/services/queries/useServiceLogs';
import { ServiceViewModel } from '../models/service';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('ServiceService', [
'$q',
'Service',
function ServiceServiceFactory($q, Service) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('ServiceService', ServiceServiceFactory);
service.services = function (filters) {
var deferred = $q.defer();
/* @ngInject */
function ServiceServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Service.query({ filters: filters ? filters : {} })
.$promise.then(function success(data) {
var services = data.map(function (item) {
return new ServiceViewModel(item);
});
deferred.resolve(services);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve services', err: err });
});
return {
services: useAxios(injectEnvironmentId(getServicesAngularJS)), // dashboard + service list + swarm visualizer + volume list + stackservice + stack edit
service: useAxios(injectEnvironmentId(getServiceAngularJS)), // service edit + task edit
remove: useAxios(injectEnvironmentId(removeServiceAngularJS)), // service edit
update: useAxios(injectEnvironmentId(updateServiceAngularJS)), // service edit
create: useAxios(injectEnvironmentId(createServiceAngularJS)), // service create
logs: useAxios(injectEnvironmentId(serviceLogsAngularJS)), // service logs
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {*} filters
*/
async function getServicesAngularJS(environmentId, filters) {
const data = await getServices(environmentId, filters);
return data.map((s) => new ServiceViewModel(s));
}
service.service = function (id) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceId} serviceId
*/
async function getServiceAngularJS(environmentId, serviceId) {
const data = await getService(environmentId, serviceId);
return new ServiceViewModel(data);
}
Service.get({ id: id })
.$promise.then(function success(data) {
var service = new ServiceViewModel(data);
deferred.resolve(service);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve service details', err: err });
});
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceViewModel} service
*/
async function removeServiceAngularJS(environmentId, service) {
return removeService(environmentId, service.Id);
}
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceViewModel} service
* @param {ServiceUpdateConfig} config
* @param {string?} rollback
*/
async function updateServiceAngularJS(environmentId, service, config, rollback) {
const data = await getServiceAngularJS(environmentId, service.Id);
return updateService({
environmentId,
config,
serviceId: service.Id,
version: data.Version,
registryId: config.registryId,
rollback,
});
}
service.remove = function (service) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {Service} config
* @param {RegistryId} registryId
*/
async function createServiceAngularJS(environmentId, config, registryId) {
return createService({ environmentId, config, registryId });
}
Service.remove({ id: service.Id })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove service', err: err });
});
return deferred.promise;
};
service.update = function (serv, config, rollback) {
return service.service(serv.Id).then((data) => {
const params = {
id: serv.Id,
version: data.Version,
};
if (rollback) {
params.rollback = rollback;
}
return Service.update(params, config).$promise;
});
};
service.logs = function (id, stdout, stderr, timestamps, since, tail) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
};
Service.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {ServiceId} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
*/
async function serviceLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all') {
const data = await getServiceLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders: true, withTimestamps: !!timestamps });
}
}
+9 -24
View File
@@ -1,27 +1,12 @@
import { SwarmViewModel } from '../models/swarm';
import { getSwarm } from '@/react/docker/proxy/queries/useSwarm';
angular.module('portainer.docker').factory('SwarmService', [
'$q',
'Swarm',
function SwarmServiceFactory($q, Swarm) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SwarmService', SwarmServiceFactory);
service.swarm = function (endpointId) {
var deferred = $q.defer();
/* @ngInject */
function SwarmServiceFactory(AngularToReact) {
const { useAxios } = AngularToReact;
Swarm.get(endpointId ? { endpointId } : undefined)
.$promise.then(function success(data) {
var swarm = new SwarmViewModel(data);
deferred.resolve(swarm);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve Swarm details', err: err });
});
return deferred.promise;
};
return service;
},
]);
return {
swarm: useAxios(getSwarm), // stack service
};
}
+23 -54
View File
@@ -1,59 +1,28 @@
import { ping } from '@/react/docker/proxy/queries/usePing';
import { getInfo } from '@/react/docker/proxy/queries/useInfo';
import { getVersion } from '@/react/docker/proxy/queries/useVersion';
import { getEvents } from '@/react/docker/proxy/queries/useEvents';
import { EventViewModel } from '../models/event';
import { ping } from './ping';
angular.module('portainer.docker').factory('SystemService', [
'$q',
'System',
function SystemServiceFactory($q, System) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('SystemService', SystemServiceFactory);
service.plugins = function () {
var deferred = $q.defer();
System.info({})
.$promise.then(function success(data) {
var plugins = data.Plugins;
deferred.resolve(plugins);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve plugins information from system', err: err });
});
return deferred.promise;
};
/* @ngInject */
function SystemServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
service.info = function () {
return System.info({}).$promise;
};
return {
info: useAxios(injectEnvironmentId(getInfo)), // dashboard + docker host view + docker host browser + swarm inspect views + stateManager (update endpoint state)
ping: useAxios(ping), // docker/__module onEnter abstract /docker subpath
version: useAxios(injectEnvironmentId(getVersion)), // docker host view + swarm inspect view + stateManager (update endpoint state)
events: useAxios(injectEnvironmentId(eventsAngularJS)), // events list
};
service.ping = function (endpointId) {
return ping(endpointId);
};
service.version = function () {
return System.version({}).$promise;
};
service.events = function (from, to) {
var deferred = $q.defer();
System.events({ since: from, until: to })
.$promise.then(function success(data) {
var events = data.map(function (item) {
return new EventViewModel(item);
});
deferred.resolve(events);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve engine events', err: err });
});
return deferred.promise;
};
service.dataUsage = function () {
return System.dataUsage().$promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId Injected
* @param {{since: string; until: string;}} param1
*/
async function eventsAngularJS(environmentId, { since, until }) {
const data = await getEvents(environmentId, { since, until });
return data.map((e) => new EventViewModel(e));
}
}
+50 -62
View File
@@ -1,69 +1,57 @@
import { formatLogs } from '../helpers/logHelper';
import { getTask } from '@/react/docker/tasks/queries/useTask';
import { getTasks } from '@/react/docker/proxy/queries/tasks/useTasks';
import { getTaskLogs } from '@/react/docker/tasks/queries/useTaskLogs';
import { TaskViewModel } from '../models/task';
import { formatLogs } from '../helpers/logHelper';
angular.module('portainer.docker').factory('TaskService', [
'$q',
'Task',
function TaskServiceFactory($q, Task) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('TaskService', TaskServiceFactory);
service.task = function (id) {
var deferred = $q.defer();
/* @ngInject */
function TaskServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
Task.get({ id: id })
.$promise.then(function success(data) {
var task = new TaskViewModel(data);
deferred.resolve(task);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve task details', err: err });
});
return {
task: useAxios(injectEnvironmentId(taskAngularJS)), // task edit
tasks: useAxios(injectEnvironmentId(tasksAngularJS)), // services list + service edit + swarm visualizer + stack edit
logs: useAxios(injectEnvironmentId(taskLogsAngularJS)), // task logs
};
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {TaskId} id
*/
async function taskAngularJS(environmentId, id) {
const data = await getTask(environmentId, id);
return new TaskViewModel(data);
}
service.tasks = function (filters) {
var deferred = $q.defer();
/**
* @param {EnvironmentId} environmentId Injected
* @param {*} filters
*/
async function tasksAngularJS(environmentId, filters) {
const data = await getTasks(environmentId, filters);
return data.map((t) => new TaskViewModel(t));
}
Task.query({ filters: filters ? filters : {} })
.$promise.then(function success(data) {
var tasks = data.map(function (item) {
return new TaskViewModel(item);
});
deferred.resolve(tasks);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve tasks', err: err });
});
return deferred.promise;
};
service.logs = function (id, stdout, stderr, timestamps, since, tail) {
var deferred = $q.defer();
var parameters = {
id: id,
stdout: stdout || 0,
stderr: stderr || 0,
timestamps: timestamps || 0,
since: since || 0,
tail: tail || 'all',
};
Task.logs(parameters)
.$promise.then(function success(data) {
var logs = formatLogs(data.logs, { stripHeaders: true, withTimestamps: !!timestamps });
deferred.resolve(logs);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {TaskId} id
* @param {boolean?} stdout
* @param {boolean?} stderr
* @param {boolean?} timestamps
* @param {number?} since
* @param {number?} tail
*/
async function taskLogsAngularJS(environmentId, id, stdout = false, stderr = false, timestamps = false, since = 0, tail = 'all') {
const data = await getTaskLogs(environmentId, id, {
since,
stderr,
stdout,
tail,
timestamps,
});
return formatLogs(data, { stripHeaders: true, withTimestamps: !!timestamps });
}
}
+83 -99
View File
@@ -1,105 +1,89 @@
import { getVolumes } from '@/react/docker/volumes/queries/useVolumes';
import { getVolume } from '@/react/docker/volumes/queries/useVolume';
import { removeVolume } from '@/react/docker/volumes/queries/useRemoveVolumeMutation';
import { createVolume } from '@/react/docker/volumes/queries/useCreateVolumeMutation';
import { VolumeViewModel } from '../models/volume';
angular.module('portainer.docker').factory('VolumeService', [
'$q',
'Volume',
'VolumeHelper',
function VolumeServiceFactory($q, Volume, VolumeHelper) {
'use strict';
var service = {};
angular.module('portainer.docker').factory('VolumeService', VolumeServiceFactory);
service.volumes = function (params) {
var deferred = $q.defer();
Volume.query(params)
.$promise.then(function success(data) {
var volumes = data.Volumes || [];
volumes = volumes.map(function (item) {
return new VolumeViewModel(item);
});
deferred.resolve(volumes);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve volumes', err: err });
});
return deferred.promise;
/* @ngInject */
function VolumeServiceFactory(AngularToReact) {
const { useAxios, injectEnvironmentId } = AngularToReact;
return {
volumes: useAxios(injectEnvironmentId(volumesAngularJS)), // dashboard + service create + service edit + volume list
volume: useAxios(injectEnvironmentId(volumeAngularJS)), // volume edit
getVolumes: useAxios(injectEnvironmentId(getVolumesAngularJS)), // template list
remove: useAxios(injectEnvironmentId(removeAngularJS)), // volume list + volume edit
createVolume: useAxios(injectEnvironmentId(createAngularJS)), // volume create
createVolumeConfiguration, // volume create
createXAutoGeneratedLocalVolumes: useAxios(injectEnvironmentId(createXAutoGeneratedLocalVolumes)), // templates list
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {Filters} filters
*/
async function volumesAngularJS(environmentId, filters) {
const data = await getVolumes(environmentId, filters);
return data.map((v) => new VolumeViewModel(v));
}
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} id
*/
async function volumeAngularJS(environmentId, id) {
const data = await getVolume(environmentId, id);
return new VolumeViewModel(data);
}
/**
* @param {EnvironmentId} environmentId Injected
*/
async function getVolumesAngularJS(environmentId) {
return getVolumes(environmentId);
}
/**
* @param {EnvironmentId} environmentId Injected
* @param {string} name
* @param {string?} nodeName
*/
async function removeAngularJS(environmentId, name, nodeName) {
return removeVolume(environmentId, name, { nodeName });
}
/**
* @param {string} name
* @param {string} driver
* @param {{name: string; value: string;}[]} driverOptions
*/
function createVolumeConfiguration(name, driver, driverOptions) {
return {
Name: name,
Driver: driver,
DriverOpts: driverOptions.reduce((res, { name, value }) => ({ ...res, [name]: value }), {}),
};
}
service.volume = function (id) {
var deferred = $q.defer();
Volume.get({ id: id })
.$promise.then(function success(data) {
var volume = new VolumeViewModel(data);
deferred.resolve(volume);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve volume details', err: err });
});
return deferred.promise;
};
/**
* @param {EnvironmentId} environmentId Injected
* @param {VolumeConfiguration} volumeConfiguration
* @param {string?} nodeName
*/
async function createAngularJS(environmentId, volumeConfiguration, nodeName) {
const data = await createVolume(environmentId, volumeConfiguration, { nodeName });
return new VolumeViewModel(data);
}
service.getVolumes = function () {
return Volume.query({}).$promise;
};
service.remove = function (volume) {
var deferred = $q.defer();
Volume.remove({ id: volume.Id })
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove volume', err: err });
});
return deferred.promise;
};
service.createVolumeConfiguration = function (name, driver, driverOptions) {
var volumeConfiguration = {
Name: name,
Driver: driver,
DriverOpts: VolumeHelper.createDriverOptions(driverOptions),
};
return volumeConfiguration;
};
service.createVolume = function (volumeConfiguration) {
var deferred = $q.defer();
Volume.create(volumeConfiguration)
.$promise.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var volume = new VolumeViewModel(data);
deferred.resolve(volume);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create volume', err: err });
});
return deferred.promise;
};
service.createVolumes = function (volumeConfigurations) {
var createVolumeQueries = volumeConfigurations.map(function (volumeConfiguration) {
return service.createVolume(volumeConfiguration);
});
return $q.all(createVolumeQueries);
};
service.createXAutoGeneratedLocalVolumes = function (x) {
var createVolumeQueries = [];
for (var i = 0; i < x; i++) {
createVolumeQueries.push(service.createVolume({ Driver: 'local' }));
}
return $q.all(createVolumeQueries);
};
return service;
},
]);
/**
* @param {EnvironmentId} environmentId
* @param {number} count
*/
async function createXAutoGeneratedLocalVolumes(environmentId, count) {
const promises = Array.from({ length: count }).map(() => createAngularJS(environmentId, { Driver: 'local' }));
return Promise.all(promises);
}
}
@@ -9,28 +9,12 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
'ContainerService',
'ImageService',
'Notifications',
'ContainerHelper',
'ExecService',
'HttpRequestHelper',
'LocalStorage',
'CONSOLE_COMMANDS_LABEL_PREFIX',
'SidebarService',
'endpoint',
function (
$scope,
$state,
$transition$,
ContainerService,
ImageService,
Notifications,
ContainerHelper,
ExecService,
HttpRequestHelper,
LocalStorage,
CONSOLE_COMMANDS_LABEL_PREFIX,
SidebarService,
endpoint
) {
function ($scope, $state, $transition$, ContainerService, ImageService, Notifications, ExecService, HttpRequestHelper, CONSOLE_COMMANDS_LABEL_PREFIX, SidebarService, endpoint) {
var socket, term;
let states = Object.freeze({
@@ -97,7 +81,6 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
$scope.state = states.connecting;
var command = $scope.formValues.isCustomCommand ? $scope.formValues.customCommand : $scope.formValues.command;
var execConfig = {
id: $transition$.params().id,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
@@ -106,7 +89,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
Cmd: commandStringToArray(command),
};
ContainerService.createExec(endpoint.Id, execConfig)
ContainerService.createExec(endpoint.Id, $transition$.params().id, execConfig)
.then(function success(data) {
const params = {
endpointId: $state.params.endpointId,
@@ -5,6 +5,7 @@ import { confirmContainerDeletion } from '@/react/docker/containers/common/confi
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { ResourceControlType } from '@/react/portainer/access-control/types';
import { confirmContainerRecreation } from '@/react/docker/containers/ItemView/ConfirmRecreationModal';
import { commitContainer } from '@/react/docker/proxy/queries/useCommitContainerMutation';
angular.module('portainer.docker').controller('ContainerController', [
'$q',
@@ -13,14 +14,13 @@ angular.module('portainer.docker').controller('ContainerController', [
'$transition$',
'$filter',
'$async',
'Commit',
'ContainerService',
'ImageHelper',
'Notifications',
'HttpRequestHelper',
'Authentication',
'endpoint',
function ($q, $scope, $state, $transition$, $filter, $async, Commit, ContainerService, ImageHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
function ($q, $scope, $state, $transition$, $filter, $async, ContainerService, ImageHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
$scope.resourceType = ResourceControlType.Container;
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
@@ -204,7 +204,7 @@ angular.module('portainer.docker').controller('ContainerController', [
const registryModel = $scope.config.RegistryModel;
const imageConfig = ImageHelper.createImageConfigForContainer(registryModel);
try {
await Commit.commitContainer({ environmentId: endpoint.Id }, { id: $transition$.params().id, repo: imageConfig.fromImage }).$promise;
await commitContainer(endpoint.Id, { container: $transition$.params().id, repo: imageConfig.fromImage });
Notifications.success('Image created', $transition$.params().id);
$state.reload();
} catch (err) {
+3 -3
View File
@@ -6,10 +6,10 @@ angular.module('portainer.docker').controller('EventsController', [
'SystemService',
function ($scope, Notifications, SystemService) {
function initView() {
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
const since = moment().subtract(24, 'hour').unix();
const until = moment().unix();
SystemService.events(from, to)
SystemService.events({ since, until })
.then(function success(data) {
$scope.events = data;
})
+3 -8
View File
@@ -35,15 +35,10 @@ angular.module('portainer.docker').controller('ImagesController', [
const registryModel = $scope.formValues.RegistryModel;
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
$scope.state.actionInProgress = true;
ImageService.pullImage(registryModel, false)
.then(function success(data) {
var err = data[data.length - 1].errorDetail;
if (err) {
return Notifications.error('Failure', err, 'Unable to pull image');
}
ImageService.pullImage(registryModel, nodeName)
.then(function success() {
Notifications.success('Image successfully pulled', registryModel.Image);
$state.reload();
})
@@ -122,7 +117,7 @@ angular.module('portainer.docker').controller('ImagesController', [
$scope.state.exportInProgress = true;
ImageService.downloadImages(images)
.then(function success(data) {
var downloadData = new Blob([data.file], { type: 'application/x-tar' });
var downloadData = new Blob([data], { type: 'application/x-tar' });
FileSaver.saveAs(downloadData, 'images.tar');
Notifications.success('Success', 'Image(s) successfully downloaded');
})
@@ -230,11 +230,8 @@ angular.module('portainer.docker').controller('CreateNetworkController', [
}
function createNetwork(context) {
HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName);
HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation);
$scope.state.actionInProgress = true;
NetworkService.create(context.networkConfiguration)
NetworkService.create(context.networkConfiguration, { nodeName: context.nodeName, agentManagerOperation: context.managerOperation })
.then(function success(data) {
const userId = context.userDetails.ID;
const accessControlData = context.accessControlData;
@@ -14,7 +14,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'$scope',
'$state',
'$timeout',
'Service',
'ServiceService',
'ServiceHelper',
'ConfigService',
'ConfigHelper',
@@ -29,8 +29,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
'Notifications',
'FormValidator',
'PluginService',
'RegistryService',
'HttpRequestHelper',
'NodeService',
'WebhookService',
'endpoint',
@@ -39,7 +37,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
$scope,
$state,
$timeout,
Service,
ServiceService,
ServiceHelper,
ConfigService,
ConfigHelper,
@@ -54,8 +52,6 @@ angular.module('portainer.docker').controller('CreateServiceController', [
Notifications,
FormValidator,
PluginService,
RegistryService,
HttpRequestHelper,
NodeService,
WebhookService,
endpoint
@@ -523,11 +519,9 @@ angular.module('portainer.docker').controller('CreateServiceController', [
function createNewService(config, accessControlData) {
const registryModel = $scope.formValues.RegistryModel;
var authenticationDetails = registryModel.Registry.Authentication ? RegistryService.encodedCredentials(registryModel.Registry) : '';
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
Service.create(config)
.$promise.then(function success(data) {
ServiceService.create(config, registryModel.Registry.Authentication ? registryModel.Registry.Id : 0)
.then(function success(data) {
const serviceId = data.ID;
const resourceControl = data.Portainer.ResourceControl;
const userId = Authentication.getUserDetails().ID;
@@ -3,7 +3,6 @@ import { VolumesNFSFormData } from '../../../components/volumesNFSForm/volumesNF
import { VolumesCIFSFormData } from '../../../components/volumesCIFSForm/volumesCifsFormModel';
angular.module('portainer.docker').controller('CreateVolumeController', [
'$q',
'$scope',
'$state',
'VolumeService',
@@ -12,9 +11,8 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
'Authentication',
'Notifications',
'FormValidator',
'HttpRequestHelper',
'endpoint',
function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, HttpRequestHelper, endpoint) {
function ($scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, endpoint) {
$scope.endpoint = endpoint;
$scope.formValues = {
@@ -126,10 +124,9 @@ angular.module('portainer.docker').controller('CreateVolumeController', [
}
var nodeName = $scope.formValues.NodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
$scope.state.actionInProgress = true;
VolumeService.createVolume(volumeConfiguration)
VolumeService.createVolume(volumeConfiguration, nodeName)
.then(function success(data) {
const userId = userDetails.ID;
const resourceControl = data.ResourceControl;
@@ -21,7 +21,7 @@ angular.module('portainer.docker').controller('VolumeController', [
$scope.removeVolume = function removeVolume() {
confirmDelete('Do you want to remove this volume?').then((confirmed) => {
if (confirmed) {
VolumeService.remove($scope.volume)
VolumeService.remove($scope.volume.Id)
.then(function success() {
Notifications.success('Volume successfully removed', $transition$.params().id);
$state.go('docker.volumes', {});
@@ -10,15 +10,13 @@ angular.module('portainer.docker').controller('VolumesController', [
'ServiceService',
'VolumeHelper',
'Notifications',
'HttpRequestHelper',
'Authentication',
'endpoint',
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, Authentication, endpoint) {
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, Authentication, endpoint) {
$scope.removeAction = function (selectedItems) {
confirmDelete('Do you want to remove the selected volume(s)?').then(async (confirmed) => {
async function doRemove(volume) {
HttpRequestHelper.setPortainerAgentTargetHeader(volume.NodeName);
return VolumeService.remove(volume)
return VolumeService.remove(volume.Id, volume.NodeName)
.then(function success() {
Notifications.success('Volume successfully removed', volume.Id);
var index = $scope.volumes.indexOf(volume);
@@ -42,8 +40,8 @@ angular.module('portainer.docker').controller('VolumesController', [
var endpointRole = $scope.applicationState.endpoint.mode.role;
$q.all({
attached: VolumeService.volumes({ filters: { dangling: ['false'] } }),
dangling: VolumeService.volumes({ filters: { dangling: ['true'] } }),
attached: VolumeService.volumes({ dangling: ['false'] }),
dangling: VolumeService.volumes({ dangling: ['true'] }),
services: endpointProvider === 'DOCKER_SWARM_MODE' && endpointRole === 'MANAGER' ? ServiceService.services() : [],
})
.then(function success(data) {
+24
View File
@@ -0,0 +1,24 @@
import {
MaxDockerAPIVersionKey,
MaxDockerAPIVersionType,
} from './portainer/services/dockerMaxApiVersion';
export * from 'axios';
declare module 'axios' {
interface CreateAxiosDefaults {
/**
* require to define a default max Docker API Version when creating an axios instance
*/
[MaxDockerAPIVersionKey]: MaxDockerAPIVersionType;
}
interface AxiosRequestConfig {
/**
* represents the maximum Docker API version supported for the request
*
* the default will be used when not specified in the request config
*/
[MaxDockerAPIVersionKey]?: MaxDockerAPIVersionType;
}
}
-13
View File
@@ -10,19 +10,6 @@
<meta http-equiv="pragma" content="no-cache" />
<meta name="robots" content="noindex" />
<base id="base" />
<script>
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
if (window.origin == 'http://localhost:49000') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:49000/';
window.ddExtension = true;
} else {
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
}
</script>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
+12
View File
@@ -21,6 +21,18 @@ import { onStartupAngular } from './app';
import { configApp } from './config';
import { constantsModule } from './ng-constants';
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
if (window.origin == 'http://localhost:49000') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:49000/';
window.ddExtension = true;
} else {
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
}
initFeatureService(Edition[process.env.PORTAINER_EDITION]);
angular
+8 -1
View File
@@ -1,5 +1,4 @@
import { rawResponse } from 'Kubernetes/rest/response/transform';
import { logsHandler } from 'Docker/rest/response/handlers';
angular.module('portainer.kubernetes').factory('KubernetesPods', [
'$resource',
@@ -48,3 +47,11 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
};
},
]);
// The Docker API returns the logs as a single string.
// This handler wraps the data in a JSON object under the "logs" property.
function logsHandler(data) {
return {
logs: data,
};
}
@@ -9,3 +9,16 @@ export function AccessControlFormData() {
this.AuthorizedUsers = [];
this.AuthorizedTeams = [];
}
/**
* Transform AngularJS UAC FormData model to React UAC FormData model
* @param {AccessControlFormData} uac AngularJS format (see above)
* @returns {AccessControlFormData} React format (see at @/react/portainer/access-control/types)
*/
export function toReactAccessControlFormData({ Ownership, AuthorizedTeams, AuthorizedUsers }) {
return {
ownership: Ownership, // type: ResourceControlOwnership;
authorizedUsers: AuthorizedUsers, // type: UserId[];
authorizedTeams: AuthorizedTeams, // type: TeamId[];
};
}

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