Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 515ef880c0 | |||
| b89f1d314f | |||
| be3cb0690c | |||
| 835a7e41e6 | |||
| 5aae1cd991 | |||
| 34532deccb | |||
| 80c8e483c9 | |||
| 9421e9d452 | |||
| 55cda8c78e | |||
| 4190fc1b4e | |||
| ac5491e864 | |||
| 8cbd23c059 | |||
| 3800a958da |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
@@ -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{}{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function SwarmViewModel(data) {
|
||||
this.Id = data.ID;
|
||||
}
|
||||
+13
-11
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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 },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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 },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Vendored
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user