Compare commits
46 Commits
release/2.
...
fix/dev-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a626324c | ||
|
|
5fd36ee986 | ||
|
|
7c2fcb67eb | ||
|
|
2eb4453487 | ||
|
|
535e499cc5 | ||
|
|
fee315b07e | ||
|
|
d1166b5294 | ||
|
|
e3b727a636 | ||
|
|
d56ea05218 | ||
|
|
8e724e3fbe | ||
|
|
33b141bcd3 | ||
|
|
ded8ce48a8 | ||
|
|
e60635bf32 | ||
|
|
6fb4951949 | ||
|
|
c429b29216 | ||
|
|
8ab490f224 | ||
|
|
78b83420bf | ||
|
|
b4dbc341cc | ||
|
|
3118c639f6 | ||
|
|
5d7ab85473 | ||
|
|
99331a81d4 | ||
|
|
ab1a8c1d6a | ||
|
|
e063cba81b | ||
|
|
23e6a982b9 | ||
|
|
0bf75ae113 | ||
|
|
72b41dde01 | ||
|
|
36b122ca21 | ||
|
|
649799069b | ||
|
|
0ca56ddbb1 | ||
|
|
3a30c8ed1e | ||
|
|
151db6bfe7 | ||
|
|
106c719a34 | ||
|
|
1cfd031db1 | ||
|
|
fbc1a2d44d | ||
|
|
47478efd1e | ||
|
|
50940b7fba | ||
|
|
7468d5637b | ||
|
|
6edc210ae7 | ||
|
|
f859876cb6 | ||
|
|
5e434a82ed | ||
|
|
d9f6471a00 | ||
|
|
a7d1a20dfb | ||
|
|
17517d7521 | ||
|
|
c609f6912f | ||
|
|
346fe9e3f1 | ||
|
|
69f14e569b |
@@ -1,44 +0,0 @@
|
||||
version: "2"
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
complex-logic:
|
||||
enabled: false
|
||||
file-lines:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: false
|
||||
method-lines:
|
||||
enabled: false
|
||||
nested-control-flow:
|
||||
enabled: false
|
||||
return-statements:
|
||||
enabled: false
|
||||
similar-code:
|
||||
enabled: false
|
||||
identical-code:
|
||||
enabled: false
|
||||
plugins:
|
||||
gofmt:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: "eslint-5"
|
||||
config:
|
||||
config: .eslintrc.yml
|
||||
exclude_patterns:
|
||||
- assets/
|
||||
- build/
|
||||
- dist/
|
||||
- distribution/
|
||||
- node_modules
|
||||
- test/
|
||||
- webpack/
|
||||
- gruntfile.js
|
||||
- webpack.config.js
|
||||
- api/
|
||||
- "!app/kubernetes/**"
|
||||
- .github/
|
||||
- .tmp/
|
||||
43
.github/workflows/nightly-security-scan.yml
vendored
43
.github/workflows/nightly-security-scan.yml
vendored
@@ -1,16 +1,16 @@
|
||||
name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
@@ -24,14 +24,14 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
- name: Upload js security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Analyse the js result
|
||||
id: set-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=js_result::${result}"
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
name: Server dependency check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
@@ -77,8 +77,8 @@ jobs:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Analyse the go result
|
||||
id: set-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=go_result::${result}"
|
||||
|
||||
@@ -114,8 +114,11 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: build
|
||||
run: make build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
@@ -134,9 +137,9 @@ jobs:
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
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 trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -144,8 +147,8 @@ jobs:
|
||||
name: image-security-scan-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
@@ -156,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Analyse the trivy result
|
||||
id: set-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
||||
echo "::set-output name=image_result::${result}"
|
||||
|
||||
@@ -167,7 +170,7 @@ jobs:
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||
@@ -182,7 +185,7 @@ jobs:
|
||||
echo ${{ matrix.image.summary }}
|
||||
|
||||
- name: Send Slack message
|
||||
if: >-
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
|
||||
36
.github/workflows/pr-security.yml
vendored
36
.github/workflows/pr-security.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
@@ -51,8 +51,8 @@ jobs:
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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 js result html file
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Analyse the js diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=js_diff_result::${result}"
|
||||
|
||||
@@ -111,8 +111,8 @@ jobs:
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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 go result html file
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Analyse the go diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=go_diff_result::${result}"
|
||||
|
||||
@@ -149,8 +149,11 @@ jobs:
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: build
|
||||
run: make build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
@@ -169,9 +172,9 @@ jobs:
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
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 trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -191,8 +194,8 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 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-result")
|
||||
|
||||
- name: Upload image result html file
|
||||
@@ -203,7 +206,7 @@ jobs:
|
||||
|
||||
- name: Analyse the image diff result
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=image_diff_result::${result}"
|
||||
|
||||
@@ -215,17 +218,16 @@ jobs:
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
strategy:
|
||||
matrix:
|
||||
matrix:
|
||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
||||
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
||||
steps:
|
||||
|
||||
- name: Check job status of diff result
|
||||
if: >-
|
||||
matrix.jsdiff.status == 'failure' ||
|
||||
matrix.godiff.status == 'failure' ||
|
||||
matrix.imagediff.status == 'failure'
|
||||
matrix.imagediff.status == 'failure'
|
||||
run: |
|
||||
echo ${{ matrix.jsdiff.status }}
|
||||
echo ${{ matrix.godiff.status }}
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -8,12 +8,12 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
run: yarn jest --maxWorkers=2
|
||||
# test-server:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
|
||||
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Validate OpenAPI specs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Download golang modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Validate OpenAPI Spec
|
||||
run: make docs-validate
|
||||
53
.github/workflows/validate-openapi-spec.yml
vendored
53
.github/workflows/validate-openapi-spec.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup Go v1.17.3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17.3'
|
||||
|
||||
- name: Prebuild docs
|
||||
run: yarn prebuild:docs
|
||||
|
||||
- name: Build OpenAPI 2.0 Spec
|
||||
run: yarn build:docs
|
||||
|
||||
# Install dependencies globally to bypass installing all frontend deps
|
||||
- name: Install swagger2openapi and swagger-cli
|
||||
run: yarn global add swagger2openapi @apidevtools/swagger-cli
|
||||
|
||||
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
|
||||
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
|
||||
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
|
||||
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
|
||||
run: yarn validate:docs
|
||||
122
Makefile
Normal file
122
Makefile
Normal file
@@ -0,0 +1,122 @@
|
||||
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
|
||||
# For a list of valid GOOS and GOARCH values
|
||||
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
|
||||
PLATFORM=$(shell go env GOOS)
|
||||
ARCH=$(shell go env GOARCH)
|
||||
|
||||
TAG=latest
|
||||
SWAG_VERSION=v1.8.11
|
||||
|
||||
# build target, can be one of "production", "testing", "development"
|
||||
ENV=development
|
||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help build-storybook build-client devops download-binaries tidy clean client-deps
|
||||
|
||||
##@ Building
|
||||
|
||||
init-dist:
|
||||
@mkdir -p dist
|
||||
|
||||
build-storybook:
|
||||
yarn storybook:build
|
||||
|
||||
build: build-server build-client ## Build the server and client
|
||||
|
||||
build-client: init-dist client-deps ## Build the client
|
||||
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
|
||||
|
||||
build-server: init-dist ## Build the server binary
|
||||
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
build-image: build ## Build the Portainer image
|
||||
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
||||
|
||||
devops: clean init-dist download-binaries build-client ## Build the server binary for CI
|
||||
echo "Building the devops binary..."
|
||||
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
##@ Dependencies
|
||||
|
||||
download-binaries: ## Download dependant binaries
|
||||
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||
|
||||
tidy: ## Tidy up the go.mod file
|
||||
cd api && go mod tidy
|
||||
|
||||
client-deps: ## Install client dependencies
|
||||
yarn
|
||||
|
||||
##@ Cleanup
|
||||
|
||||
clean: ## Remove all build and download artifacts
|
||||
@echo "Clearing the dist directory..."
|
||||
@rm -rf dist/*
|
||||
|
||||
##@ Testing
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test
|
||||
|
||||
test-server: ## Run server tests
|
||||
cd api && go test -v ./...
|
||||
|
||||
test: test-client test-server ## Run all tests
|
||||
|
||||
##@ Dev
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
|
||||
dev-server: build-image ## Run the server in development mode
|
||||
@./dev/run_container.sh
|
||||
|
||||
|
||||
##@ Format
|
||||
|
||||
format-client: ## Format client code
|
||||
yarn format
|
||||
|
||||
format-server: ## Format server code
|
||||
cd api && go fmt ./...
|
||||
|
||||
format: format-client format-server ## Format all code
|
||||
|
||||
##@ Lint
|
||||
|
||||
lint: lint-client lint-server ## Lint all code
|
||||
|
||||
lint-client: ## Lint client code
|
||||
yarn lint
|
||||
|
||||
lint-server: ## Lint server code
|
||||
cd api && go vet ./...
|
||||
|
||||
##@ Extension
|
||||
|
||||
dev-extension: build-server build-client ## Run the extension in development mode
|
||||
make local -f build/docker-extension/Makefile
|
||||
|
||||
##@ Docs
|
||||
|
||||
docs-deps: ## Install docs dependencies
|
||||
go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
|
||||
docs-build: docs-deps ## Build docs
|
||||
cd api && swag init -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
|
||||
|
||||
docs-validate: docs-build ## Validate docs
|
||||
yarn swagger2openapi --warnOnly api/docs/swagger.yaml -o api/docs/openapi.yaml
|
||||
yarn swagger-cli validate api/docs/openapi.yaml
|
||||
|
||||
docs-clean: ## Clean docs
|
||||
rm -rf api/docs
|
||||
|
||||
docs-validate-clean: docs-validate docs-clean ## Validate and clean docs
|
||||
|
||||
##@ Helpers
|
||||
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
@@ -3,8 +3,10 @@ package backup
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -43,6 +45,12 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
||||
return errors.Wrap(err, "Failed to stop db")
|
||||
}
|
||||
|
||||
// At some point, backups were created containing a subdirectory, now we need to handle both
|
||||
restorePath, err = getRestoreSourcePath(restorePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
|
||||
}
|
||||
|
||||
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||
return errors.Wrap(err, "failed to restore the system state")
|
||||
}
|
||||
@@ -59,6 +67,26 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
|
||||
return archive.ExtractTarGz(r, destinationDirPath)
|
||||
}
|
||||
|
||||
func getRestoreSourcePath(dir string) (string, error) {
|
||||
// find portainer.db or portainer.edb file. Return the parent directory
|
||||
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
|
||||
|
||||
backupDirPath := dir
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if portainerdbRegex.MatchString(d.Name()) {
|
||||
backupDirPath = filepath.Dir(path)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return backupDirPath, err
|
||||
}
|
||||
|
||||
func restoreFiles(srcDir string, destinationDir string) error {
|
||||
for _, filename := range filesToRestore {
|
||||
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"EdgeKey": "",
|
||||
"Extensions": [],
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"Name": "local",
|
||||
"PublicURL": "",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"EnableGPUManagement": false,
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
"Kubernetes": {
|
||||
@@ -64,6 +65,7 @@
|
||||
"UseServerMetrics": false
|
||||
},
|
||||
"Flags": {
|
||||
"IsServerIngressClassDetected": false,
|
||||
"IsServerMetricsDetected": false,
|
||||
"IsServerStorageDetected": false
|
||||
},
|
||||
@@ -905,8 +907,7 @@
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
@@ -936,8 +937,7 @@
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": "",
|
||||
"subtleUpgradeButton": false
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UserTheme": "",
|
||||
@@ -945,6 +945,6 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.18.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
err = manager.deployer.Deploy(ctx, filePaths, libstack.DeployOptions{
|
||||
Options: libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
@@ -82,9 +82,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
}
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
err = manager.deployer.Pull(ctx, filePaths, libstack.Options{
|
||||
WorkingDir: stack.ProjectPath,
|
||||
EnvFilePath: envFilePath,
|
||||
|
||||
@@ -90,7 +90,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack, false)
|
||||
filePaths := stackutils.GetStackFilePaths(stack, true)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/client"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -51,21 +49,22 @@ type azureItem struct {
|
||||
}
|
||||
|
||||
type azureClient struct {
|
||||
client *http.Client
|
||||
baseUrl string
|
||||
}
|
||||
|
||||
func NewAzureClient() *azureClient {
|
||||
httpsCli := newHttpClientForAzure()
|
||||
return &azureClient{
|
||||
client: httpsCli,
|
||||
baseUrl: "https://dev.azure.com",
|
||||
}
|
||||
}
|
||||
|
||||
func newHttpClientForAzure() *http.Client {
|
||||
func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
|
||||
tlsConfig := crypto.CreateTLSConfiguration()
|
||||
|
||||
if insecureSkipVerify {
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
httpsCli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
@@ -74,7 +73,6 @@ func newHttpClientForAzure() *http.Client {
|
||||
Timeout: 300 * time.Second,
|
||||
}
|
||||
|
||||
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
||||
return httpsCli
|
||||
}
|
||||
|
||||
@@ -106,6 +104,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to create temp file")
|
||||
}
|
||||
|
||||
defer zipFile.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
|
||||
@@ -119,10 +118,14 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
|
||||
return "", errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
res, err := a.client.Do(req)
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
@@ -166,7 +169,10 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
@@ -399,7 +405,10 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
@@ -456,7 +465,10 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
|
||||
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
client := newHttpClientForAzure(opt.tlsSkipVerify)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to make an HTTP request")
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
|
||||
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "")
|
||||
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
})
|
||||
@@ -74,7 +74,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
|
||||
|
||||
dst := t.TempDir()
|
||||
|
||||
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat)
|
||||
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
|
||||
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat)
|
||||
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func TestService_ListRefs_Azure(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
}
|
||||
@@ -109,8 +109,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
|
||||
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -248,7 +248,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions)
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
|
||||
if tt.expect.shouldFail {
|
||||
assert.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
@@ -271,8 +271,8 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
|
||||
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
@@ -292,7 +292,6 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
a := &azureClient{
|
||||
client: server.Client(),
|
||||
baseUrl: server.URL,
|
||||
}
|
||||
|
||||
@@ -329,7 +328,6 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
a := &azureClient{
|
||||
client: server.Client(),
|
||||
baseUrl: server.URL,
|
||||
}
|
||||
|
||||
@@ -442,6 +440,7 @@ func Test_listRefs_azure(t *testing.T) {
|
||||
|
||||
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
|
||||
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args baseOption
|
||||
|
||||
@@ -20,6 +20,8 @@ type CloneOptions struct {
|
||||
ReferenceName string
|
||||
Username string
|
||||
Password string
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
|
||||
@@ -43,7 +45,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
|
||||
|
||||
cleanUp = true
|
||||
|
||||
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password)
|
||||
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
|
||||
if err != nil {
|
||||
cleanUp = false
|
||||
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
|
||||
|
||||
@@ -28,9 +28,10 @@ func NewGitClient(preserveGitDir bool) *gitClient {
|
||||
|
||||
func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) error {
|
||||
gitOptions := git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
}
|
||||
|
||||
if opt.referenceName != "" {
|
||||
@@ -60,7 +61,8 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
}
|
||||
|
||||
refs, err := remote.List(listOptions)
|
||||
@@ -110,7 +112,8 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
})
|
||||
|
||||
listOptions := &git.ListOptions{
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
}
|
||||
|
||||
refs, err := rem.List(listOptions)
|
||||
@@ -132,12 +135,13 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
// listFiles list all filenames under the specific repository
|
||||
func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, error) {
|
||||
cloneOption := &git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(opt.referenceName),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
URL: opt.repositoryUrl,
|
||||
NoCheckout: true,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName(opt.referenceName),
|
||||
Auth: getAuth(opt.username, opt.password),
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
}
|
||||
|
||||
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken)
|
||||
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, filepath.Join(dst, "README.md"))
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 0, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken)
|
||||
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func TestService_ListRefs_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 0, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
}
|
||||
@@ -63,8 +63,8 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
go service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func TestService_ListFiles_GitHub(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions)
|
||||
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
|
||||
if tt.expect.shouldFail {
|
||||
assert.Error(t, err)
|
||||
if tt.expect.err != nil {
|
||||
@@ -226,8 +226,8 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
|
||||
|
||||
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
@@ -240,8 +240,8 @@ func TestService_purgeCache_Github(t *testing.T) {
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
service := NewService(context.TODO())
|
||||
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
@@ -261,8 +261,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
|
||||
// 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
|
||||
service := newService(context.TODO(), 2, 40*timeout)
|
||||
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
@@ -293,12 +293,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 2, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
}
|
||||
@@ -311,26 +311,26 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
|
||||
service := newService(context.TODO(), 2, 0)
|
||||
|
||||
repositoryUrl := privateGitRepoURL
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false)
|
||||
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(refs), 1)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{})
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, []string{}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 2, service.repoFileCache.Len())
|
||||
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", false)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
|
||||
refs, err = service.ListRefs(repositoryUrl, username, "fake-token", true)
|
||||
_, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, service.repoRefCache.Len())
|
||||
// The relevant file caches should be removed too
|
||||
@@ -344,12 +344,12 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
|
||||
accessToken := getRequiredValue(t, "GITHUB_PAT")
|
||||
username := getRequiredValue(t, "GITHUB_USERNAME")
|
||||
repositoryUrl := privateGitRepoURL
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{})
|
||||
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, []string{}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(files), 1)
|
||||
assert.Equal(t, 1, service.repoFileCache.Len())
|
||||
|
||||
files, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{})
|
||||
_, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", true, []string{}, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, service.repoFileCache.Len())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "")
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "")
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.NoDirExists(t, filepath.Join(dir, ".git"))
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) {
|
||||
repositoryURL := setup(t)
|
||||
referenceName := "refs/heads/main"
|
||||
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "")
|
||||
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
|
||||
|
||||
@@ -2,6 +2,7 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -20,6 +21,7 @@ type baseOption struct {
|
||||
repositoryUrl string
|
||||
username string
|
||||
password string
|
||||
tlsSkipVerify bool
|
||||
}
|
||||
|
||||
// fetchOption allows to specify the reference name of the target repository
|
||||
@@ -119,13 +121,14 @@ func (service *Service) timerHasStopped() bool {
|
||||
|
||||
// CloneRepository clones a git repository using the specified URL in the specified
|
||||
// destination folder.
|
||||
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
|
||||
options := cloneOption{
|
||||
fetchOption: fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
},
|
||||
@@ -144,12 +147,13 @@ func (service *Service) cloneRepository(destination string, options cloneOption)
|
||||
}
|
||||
|
||||
// LatestCommitID returns SHA1 of the latest commit of the specified reference
|
||||
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
|
||||
options := fetchOption{
|
||||
baseOption: baseOption{
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
}
|
||||
@@ -162,8 +166,8 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p
|
||||
}
|
||||
|
||||
// ListRefs will list target repository's references without cloning the repository
|
||||
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
refCacheKey := generateCacheKey(repositoryURL, password)
|
||||
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
|
||||
refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
|
||||
if service.cacheEnabled && hardRefresh {
|
||||
// Should remove the cache explicitly, so that the following normal list can show the correct result
|
||||
service.repoRefCache.Remove(refCacheKey)
|
||||
@@ -193,6 +197,7 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -219,8 +224,8 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
|
||||
|
||||
// ListFiles will list all the files of the target repository with specific extensions.
|
||||
// If extension is not provided, it will list all the files under the target repository
|
||||
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
repoKey := generateCacheKey(repositoryURL, referenceName)
|
||||
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
|
||||
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify))
|
||||
|
||||
if service.cacheEnabled && hardRefresh {
|
||||
// Should remove the cache explicitly, so that the following normal list can show the correct result
|
||||
@@ -246,6 +251,7 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
|
||||
repositoryUrl: repositoryURL,
|
||||
username: username,
|
||||
password: password,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
},
|
||||
referenceName: referenceName,
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package gittypes
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrIncorrectRepositoryURL = errors.New("Git repository could not be found, please ensure that the URL is correct.")
|
||||
ErrAuthenticationFailure = errors.New("Authentication failed, please ensure that the git credentials are correct.")
|
||||
ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct")
|
||||
ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct")
|
||||
)
|
||||
|
||||
// RepoConfig represents a configuration for a repo
|
||||
@@ -19,6 +19,8 @@ type RepoConfig struct {
|
||||
Authentication *GitAuthentication
|
||||
// Repository hash
|
||||
ConfigHash string `example:"bc4c183d756879ea4d173315338110b31004b8e0"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
type GitAuthentication struct {
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// UpdateGitObject updates a git object based on its config
|
||||
func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.DataStore, objId string, gitConfig *gittypes.RepoConfig, autoUpdateConfig *portainer.AutoUpdateSettings, projectPath string) (bool, string, error) {
|
||||
func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *gittypes.RepoConfig, forceUpdate bool, projectPath string) (bool, string, error) {
|
||||
if gitConfig == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -29,13 +28,13 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
|
||||
return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId)
|
||||
}
|
||||
|
||||
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password)
|
||||
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password, gitConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
|
||||
}
|
||||
|
||||
hashChanged := !strings.EqualFold(newHash, string(gitConfig.ConfigHash))
|
||||
forceUpdate := autoUpdateConfig != nil && autoUpdateConfig.ForceUpdate
|
||||
hashChanged := !strings.EqualFold(newHash, gitConfig.ConfigHash)
|
||||
|
||||
if !hashChanged && !forceUpdate {
|
||||
log.Debug().
|
||||
Str("hash", newHash).
|
||||
@@ -48,9 +47,10 @@ func UpdateGitObject(gitService portainer.GitService, dataStore dataservices.Dat
|
||||
}
|
||||
|
||||
cloneParams := &cloneRepositoryParameters{
|
||||
url: gitConfig.URL,
|
||||
ref: gitConfig.ReferenceName,
|
||||
toDir: projectPath,
|
||||
url: gitConfig.URL,
|
||||
ref: gitConfig.ReferenceName,
|
||||
toDir: projectPath,
|
||||
tlsSkipVerify: gitConfig.TLSSkipVerify,
|
||||
}
|
||||
if gitConfig.Authentication != nil {
|
||||
cloneParams.auth = &gitAuth{
|
||||
@@ -78,6 +78,8 @@ type cloneRepositoryParameters struct {
|
||||
ref string
|
||||
toDir string
|
||||
auth *gitAuth
|
||||
// tlsSkipVerify skips SSL verification when cloning the Git repository
|
||||
tlsSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
type gitAuth struct {
|
||||
@@ -87,8 +89,8 @@ type gitAuth struct {
|
||||
|
||||
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
|
||||
if cloneParams.auth != nil {
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password)
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password, cloneParams.tlsSkipVerify)
|
||||
}
|
||||
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "")
|
||||
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "", cloneParams.tlsSkipVerify)
|
||||
}
|
||||
|
||||
@@ -342,12 +342,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27 h1:PceCpp86SDYb3lZHT4KpuBCkmcJMW5x1qrdFNEfAdUo=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20221215210951-2c30d1b17a27/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a h1:4VGM1OH15fqm5rgki0eLF6vND/NxHfoPt3CA6/YdA0k=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209071700-ee11af9c546a/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f h1:z/lmLhZMMSIwg70Ap1rPluXNe1vQXH9gfK9K/ols4JA=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230209082344-8a5b52de366f/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7 h1:/i985KPNw0KvVtLhTEPUa86aJMtun5ZPOyFCJzdY+dY=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20230301083819-3dbc6abf1ce7/go.mod h1:03UmPLyjiPUexGJuW20mQXvmsoSpeErvMlItJGtq/Ww=
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a h1:B0z3skIMT+OwVNJPQhKp52X+9OWW6A9n5UWig3lHBJk=
|
||||
|
||||
@@ -213,6 +213,8 @@ type customTemplateFromGitRepositoryPayload struct {
|
||||
ComposeFilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
|
||||
// Definitions of variables in the stack file
|
||||
Variables []portainer.CustomTemplateVariableDefinition
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -279,7 +281,7 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
|
||||
repositoryPassword = ""
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword, payload.TLSSkipVerify)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
return nil, fmt.Errorf("invalid git credential")
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// @tags edge_jobs
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -20,7 +20,7 @@ type edgeJobFileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @success 200 {object} edgeJobFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -21,7 +21,7 @@ type edgeJobInspectResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -20,8 +20,8 @@ type fileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param taskID path int true "Task Id"
|
||||
// @success 200 {object} fileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -25,7 +25,7 @@ type taskContainer struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @success 200 {array} taskContainer
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -41,7 +41,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param id path int true "EdgeJob Id"
|
||||
// @param body body edgeJobUpdatePayload true "EdgeGroup data"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
// @failure 500
|
||||
|
||||
@@ -201,6 +201,8 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
Registries []portainer.RegistryID
|
||||
// Uses the manifest's namespaces instead of the default one
|
||||
UseManifestNamespaces bool
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -247,6 +249,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request, dryru
|
||||
URL: payload.RepositoryURL,
|
||||
ReferenceName: payload.RepositoryReferenceName,
|
||||
ConfigFilePath: payload.FilePathInRepository,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
@@ -345,7 +348,7 @@ func (handler *Handler) storeManifestFromGitRepository(stackFolder string, relat
|
||||
repositoryPassword = repositoryConfig.Authentication.Password
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword)
|
||||
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// @tags edge_stacks
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -20,7 +20,7 @@ type stackFileResponse struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @success 200 {object} stackFileResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -40,7 +40,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
// @tags edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -18,32 +18,12 @@ import (
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Helper()
|
||||
@@ -98,7 +78,7 @@ func setupHandler(t *testing.T) (*Handler, string, func()) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.GitService = &gitService{errors.New("Clone error"), "git-service-id"}
|
||||
handler.GitService = testhelpers.NewGitService(errors.New("Clone error"), "git-service-id")
|
||||
|
||||
return handler, rawAPIKey, storeTeardown
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param body body updateEdgeStackPayload true "EdgeStack data"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
|
||||
@@ -25,8 +25,8 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
// @tags edge, endpoints
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "environment(endpoint) Id"
|
||||
// @param jobID path string true "Job Id"
|
||||
// @param id path int true "environment(endpoint) Id"
|
||||
// @param jobID path int true "Job Id"
|
||||
// @success 200
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
@@ -25,8 +25,8 @@ type configResponse struct {
|
||||
// @tags edge, endpoints, edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "environment(endpoint) Id"
|
||||
// @param stackId path string true "EdgeStack Id"
|
||||
// @param id path int true "environment(endpoint) Id"
|
||||
// @param stackId path int true "EdgeStack Id"
|
||||
// @success 200 {object} configResponse
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
|
||||
25
api/http/handler/endpoints/endpoint_agent_browse_docs.go
Normal file
25
api/http/handler/endpoints/endpoint_agent_browse_docs.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package endpoints
|
||||
|
||||
/// This feature is implemented in the agent API and not directly here.
|
||||
/// However, it's proxied. So we document it here.
|
||||
|
||||
// @summary Upload a file under a specific path on the file system of an environment (endpoint)
|
||||
// @description Use this environment(endpoint) to upload TLS files.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept multipart/form-data
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param volumeID query string false "Optional volume identifier to upload the file"
|
||||
// @param Path formData string true "The destination path to upload the file to"
|
||||
// @param file formData file true "The file to upload"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/docker/v2/browse/put [post]
|
||||
func _fileBrowseFileUploadV2() {
|
||||
// dummy function to make swag pick up the above docs for the following REST call
|
||||
// POST request on /browse/put?volumeID=:id
|
||||
}
|
||||
@@ -177,20 +177,20 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
// @accept multipart/form-data
|
||||
// @produce json
|
||||
// @param Name formData string true "Name that will be used to identify this environment(endpoint) (example: my-environment)"
|
||||
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment" Enum(1,2,3,4,5)
|
||||
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)". Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)
|
||||
// @param EndpointCreationType formData integer true "Environment(Endpoint) type. Value must be one of: 1 (Local Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge agent environment) or 5 (Local Kubernetes Environment)" Enum(1,2,3,4,5)
|
||||
// @param URL formData string false "URL or IP address of a Docker host (example: docker.mydomain.tld:2375). Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine). Cannot be empty if EndpointCreationType is set to 4 (Edge agent environment)"
|
||||
// @param PublicURL formData string false "URL or IP address where exposed containers will be reachable. Defaults to URL if not specified (example: docker.mydomain.tld:2375)"
|
||||
// @param GroupID formData int false "Environment(Endpoint) group identifier. If not specified will default to 1 (unassigned)."
|
||||
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint)"
|
||||
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS"
|
||||
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS"
|
||||
// @param TLS formData bool false "Require TLS to connect against this environment(endpoint). Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLSSkipVerify formData bool false "Skip server verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLSSkipClientVerify formData bool false "Skip client verification when using TLS. Must be true if EndpointCreationType is set to 2 (Agent environment)"
|
||||
// @param TLSCACertFile formData file false "TLS CA certificate file"
|
||||
// @param TLSCertFile formData file false "TLS client certificate file"
|
||||
// @param TLSKeyFile formData file false "TLS client key file"
|
||||
// @param AzureApplicationID formData string false "Azure application ID. Required if environment(endpoint) type is set to 3"
|
||||
// @param AzureTenantID formData string false "Azure tenant ID. Required if environment(endpoint) type is set to 3"
|
||||
// @param AzureAuthenticationKey formData string false "Azure authentication key. Required if environment(endpoint) type is set to 3"
|
||||
// @param TagIDs formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
|
||||
// @param TagIds formData []int false "List of tag identifiers to which this environment(endpoint) is associated"
|
||||
// @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)"
|
||||
// @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel"
|
||||
// @param Gpus formData array false "List of GPUs"
|
||||
|
||||
@@ -42,7 +42,13 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
}
|
||||
|
||||
hideFields(endpoint)
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(endpoint, settings)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
if !excludeSnapshot(r) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
@@ -103,6 +104,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
paginatedEndpoints[idx].QueryDate = time.Now().Unix()
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings)
|
||||
if !query.excludeSnapshots {
|
||||
err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx])
|
||||
if err != nil {
|
||||
|
||||
@@ -82,7 +82,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.18.0
|
||||
// @version 2.18.2
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -97,7 +97,7 @@ type Handler struct {
|
||||
|
||||
// @securitydefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @name x-api-key
|
||||
|
||||
// @securitydefinitions.apikey jwt
|
||||
// @in header
|
||||
@@ -107,6 +107,8 @@ type Handler struct {
|
||||
// @tag.description Authenticate against Portainer HTTP API
|
||||
// @tag.name custom_templates
|
||||
// @tag.description Manage Custom Templates
|
||||
// @tag.name edge
|
||||
// @tag.description Manage Edge related environment(endpoint) settings
|
||||
// @tag.name edge_groups
|
||||
// @tag.description Manage Edge Groups
|
||||
// @tag.name edge_jobs
|
||||
@@ -115,8 +117,6 @@ type Handler struct {
|
||||
// @tag.description Manage Edge Stacks
|
||||
// @tag.name edge_templates
|
||||
// @tag.description Manage Edge Templates
|
||||
// @tag.name edge
|
||||
// @tag.description Manage Edge related environment(endpoint) settings
|
||||
// @tag.name endpoints
|
||||
// @tag.description Manage Docker environments(endpoints)
|
||||
// @tag.name endpoint_groups
|
||||
@@ -133,8 +133,14 @@ type Handler struct {
|
||||
// @tag.description Manage roles
|
||||
// @tag.name settings
|
||||
// @tag.description Manage Portainer settings
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name ssl
|
||||
// @tag.description Manage ssl settings
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage stacks
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name system
|
||||
// @tag.description Manage Portainer system
|
||||
// @tag.name tags
|
||||
// @tag.description Manage tags
|
||||
// @tag.name teams
|
||||
@@ -143,20 +149,14 @@ type Handler struct {
|
||||
// @tag.description Manage team memberships
|
||||
// @tag.name templates
|
||||
// @tag.description Manage App Templates
|
||||
// @tag.name stacks
|
||||
// @tag.description Manage stacks
|
||||
// @tag.name ssl
|
||||
// @tag.description Manage ssl settings
|
||||
// @tag.name users
|
||||
// @tag.description Manage users
|
||||
// @tag.name upload
|
||||
// @tag.description Upload files
|
||||
// @tag.name webhooks
|
||||
// @tag.description Manage webhooks
|
||||
// @tag.name websocket
|
||||
// @tag.description Create exec sessions using websockets
|
||||
// @tag.name status
|
||||
// @tag.description Information about the Portainer instance
|
||||
// @tag.name system
|
||||
// @tag.description Manage Portainer system
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param release path string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
// @param namespace query string false "An optional namespace"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid environment(endpoint) id or bad request"
|
||||
// @failure 401 "Unauthorized"
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param namespace query string true "specify an optional namespace"
|
||||
// @param filter query string true "specify an optional filter"
|
||||
// @param selector query string true "specify an optional selector"
|
||||
// @param namespace query string false "specify an optional namespace"
|
||||
// @param filter query string false "specify an optional filter"
|
||||
// @param selector query string false "specify an optional selector"
|
||||
// @success 200 {array} release.ReleaseElement "Success"
|
||||
// @failure 400 "Invalid environment(endpoint) identifier"
|
||||
// @failure 401 "Unauthorized"
|
||||
|
||||
@@ -25,7 +25,7 @@ type addHelmRepoUrlPayload struct {
|
||||
}
|
||||
|
||||
func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error {
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL)
|
||||
return libhelm.ValidateHelmRepositoryURL(p.URL, nil)
|
||||
}
|
||||
|
||||
// @id HelmUserRepositoryCreate
|
||||
|
||||
@@ -143,7 +143,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
|
||||
|
||||
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
|
||||
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL, nil)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid Helm repository URL. Must correspond to a valid URL format", err)
|
||||
}
|
||||
|
||||
@@ -162,9 +162,11 @@ type composeStackFromGitRepositoryPayload struct {
|
||||
Env []portainer.Pair
|
||||
// Whether the stack is from a app template
|
||||
FromAppTemplate bool `example:"false"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
Name: name,
|
||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||
@@ -173,6 +175,7 @@ func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoU
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
ComposeFile: composeFile,
|
||||
AdditionalFiles: additionalFiles,
|
||||
@@ -258,7 +261,9 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate,
|
||||
payload.Env,
|
||||
payload.FromAppTemplate)
|
||||
payload.FromAppTemplate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
|
||||
composeStackBuilder := stackbuilders.CreateComposeStackGitBuilder(securityContext,
|
||||
handler.DataStore,
|
||||
|
||||
@@ -46,9 +46,11 @@ type kubernetesGitDeploymentPayload struct {
|
||||
ManifestFile string
|
||||
AdditionalFiles []string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication, composeFormat bool, namespace, manifest string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
StackName: name,
|
||||
RepositoryConfigPayload: stackbuilders.RepositoryConfigPayload{
|
||||
@@ -57,6 +59,7 @@ func createStackPayloadFromK8sGitPayload(name, repoUrl, repoReference, repoUsern
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
Namespace: namespace,
|
||||
ComposeFormat: composeFormat,
|
||||
@@ -203,7 +206,9 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
|
||||
payload.Namespace,
|
||||
payload.ManifestFile,
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate)
|
||||
payload.AutoUpdate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
|
||||
k8sStackBuilder := stackbuilders.CreateKubernetesStackGitBuilder(handler.DataStore,
|
||||
handler.FileService,
|
||||
|
||||
@@ -117,6 +117,8 @@ type swarmStackFromGitRepositoryPayload struct {
|
||||
AdditionalFiles []string `example:"[nz.compose.yml, uat.compose.yml]"`
|
||||
// Optional auto update configuration
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
@@ -138,7 +140,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool) stackbuilders.StackPayload {
|
||||
func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference, repoUsername, repoPassword string, repoAuthentication bool, composeFile string, additionalFiles []string, autoUpdate *portainer.AutoUpdateSettings, env []portainer.Pair, fromAppTemplate bool, repoSkipSSLVerify bool) stackbuilders.StackPayload {
|
||||
return stackbuilders.StackPayload{
|
||||
Name: name,
|
||||
SwarmID: swarmID,
|
||||
@@ -148,6 +150,7 @@ func createStackPayloadFromSwarmGitPayload(name, swarmID, repoUrl, repoReference
|
||||
Authentication: repoAuthentication,
|
||||
Username: repoUsername,
|
||||
Password: repoPassword,
|
||||
TLSSkipVerify: repoSkipSSLVerify,
|
||||
},
|
||||
ComposeFile: composeFile,
|
||||
AdditionalFiles: additionalFiles,
|
||||
@@ -201,7 +204,9 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
payload.AdditionalFiles,
|
||||
payload.AutoUpdate,
|
||||
payload.Env,
|
||||
payload.FromAppTemplate)
|
||||
payload.FromAppTemplate,
|
||||
payload.TLSSkipVerify,
|
||||
)
|
||||
|
||||
swarmStackBuilder := stackbuilders.CreateSwarmStackGitBuilder(securityContext,
|
||||
handler.DataStore,
|
||||
|
||||
@@ -25,6 +25,7 @@ type stackGitUpdatePayload struct {
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -138,6 +139,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
//update retrieved stack data based on the payload
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
stack.Env = payload.Env
|
||||
stack.UpdatedBy = user.Username
|
||||
@@ -151,6 +153,9 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
password := payload.RepositoryPassword
|
||||
|
||||
// When the existing stack is using the custom username/password and the password is not updated,
|
||||
// the stack should keep using the saved username/password
|
||||
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
password = stack.GitConfig.Authentication.Password
|
||||
}
|
||||
@@ -158,7 +163,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
_, err = handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch git repository", err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -137,23 +135,29 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
|
||||
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to move git repository directory", err)
|
||||
}
|
||||
|
||||
repositoryUsername := ""
|
||||
repositoryPassword := ""
|
||||
if payload.RepositoryAuthentication {
|
||||
repositoryPassword = payload.RepositoryPassword
|
||||
|
||||
// When the existing stack is using the custom username/password and the password is not updated,
|
||||
// the stack should keep using the saved username/password
|
||||
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
|
||||
repositoryPassword = stack.GitConfig.Authentication.Password
|
||||
}
|
||||
repositoryUsername = payload.RepositoryUsername
|
||||
}
|
||||
|
||||
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, git.CloneOptions{ProjectPath: stack.ProjectPath, URL: stack.GitConfig.URL, ReferenceName: stack.GitConfig.ReferenceName, Username: repositoryUsername, Password: repositoryPassword})
|
||||
cloneOptions := git.CloneOptions{
|
||||
ProjectPath: stack.ProjectPath,
|
||||
URL: stack.GitConfig.URL,
|
||||
ReferenceName: stack.GitConfig.ReferenceName,
|
||||
Username: repositoryUsername,
|
||||
Password: repositoryPassword,
|
||||
TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
|
||||
}
|
||||
|
||||
clean, err := git.CloneWithBackup(handler.GitService, handler.FileService, cloneOptions)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clone git repository directory", err)
|
||||
}
|
||||
@@ -165,7 +169,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
return httpErr
|
||||
}
|
||||
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword)
|
||||
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, stack.GitConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type kubernetesGitStackUpdatePayload struct {
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
AutoUpdate *portainer.AutoUpdateSettings
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -62,6 +63,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
}
|
||||
|
||||
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
|
||||
stack.GitConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
stack.AutoUpdate = payload.AutoUpdate
|
||||
|
||||
if payload.RepositoryAuthentication {
|
||||
@@ -73,7 +75,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
|
||||
Username: payload.RepositoryUsername,
|
||||
Password: password,
|
||||
}
|
||||
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password)
|
||||
_, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to fetch git repository", err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @tags teams
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path string true "Team Id"
|
||||
// @param id path int true "Team Id"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path string true "Team Id"
|
||||
// @param id path int true "Team Id"
|
||||
// @success 200 {array} portainer.TeamMembership "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
|
||||
@@ -98,7 +98,7 @@ func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
defer handler.cleanUp(projectPath)
|
||||
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "")
|
||||
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, "", "", "", false)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to clone git repository", err)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
type themePayload struct {
|
||||
// Color represents the color theme of the UI
|
||||
Color *string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
||||
SubtleUpgradeButton *bool `json:"subtleUpgradeButton" example:"false"`
|
||||
}
|
||||
|
||||
type userUpdatePayload struct {
|
||||
@@ -33,11 +31,11 @@ type userUpdatePayload struct {
|
||||
|
||||
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.Contains(payload.Username, " ") {
|
||||
return errors.New("Invalid username. Must not contain any whitespace")
|
||||
return errors.New("invalid username. Must not contain any whitespace")
|
||||
}
|
||||
|
||||
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
||||
return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
return errors.New("invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -120,10 +118,6 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||
if payload.Theme.Color != nil {
|
||||
user.ThemeSettings.Color = *payload.Theme.Color
|
||||
}
|
||||
|
||||
if payload.Theme.SubtleUpgradeButton != nil {
|
||||
user.ThemeSettings.SubtleUpgradeButton = *payload.Theme.SubtleUpgradeButton
|
||||
}
|
||||
}
|
||||
|
||||
if payload.Role != 0 {
|
||||
|
||||
@@ -395,7 +395,7 @@ func (transport *Transport) updateDefaultGitBranch(request *http.Request) error
|
||||
remote := request.URL.Query().Get("remote")
|
||||
if strings.HasSuffix(remote, ".git") {
|
||||
repositoryURL := remote[:len(remote)-4]
|
||||
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "")
|
||||
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type noopGitService struct{}
|
||||
|
||||
func (s *noopGitService) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopGitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return "my-latest-commit-id", nil
|
||||
}
|
||||
func (g *noopGitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (g *noopGitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestTransport_updateDefaultGitBranch(t *testing.T) {
|
||||
type fields struct {
|
||||
gitService portainer.GitService
|
||||
@@ -33,8 +20,10 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
commitId := "my-latest-commit-id"
|
||||
|
||||
defaultFields := fields{
|
||||
gitService: &noopGitService{},
|
||||
gitService: testhelpers.NewGitService(nil, commitId),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -51,7 +40,7 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
|
||||
request: httptest.NewRequest(http.MethodPost, "http://unixsocket/build?dockerfile=Dockerfile&remote=https://my-host.com/my-user/my-repo.git&t=my-image", nil),
|
||||
},
|
||||
wantErr: false,
|
||||
expectedQuery: "dockerfile=Dockerfile&remote=https%3A%2F%2Fmy-host.com%2Fmy-user%2Fmy-repo.git%23my-latest-commit-id&t=my-image",
|
||||
expectedQuery: fmt.Sprintf("dockerfile=Dockerfile&remote=https%%3A%%2F%%2Fmy-host.com%%2Fmy-user%%2Fmy-repo.git%%23%s&t=my-image", commitId),
|
||||
},
|
||||
{
|
||||
name: "not append commit ID",
|
||||
|
||||
@@ -76,6 +76,16 @@ func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bo
|
||||
}
|
||||
|
||||
func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerIngressClassDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerIngressClassDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
cli, err := factory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to create kubernetes client for ingress class detection")
|
||||
@@ -107,6 +117,16 @@ func InitialIngressClassDetection(endpoint *portainer.Endpoint, endpointService
|
||||
}
|
||||
|
||||
func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerMetricsDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
cli, err := factory.GetKubeClient(endpoint)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to create kubernetes client for initial metrics detection")
|
||||
@@ -118,11 +138,6 @@ func InitialMetricsDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||
return
|
||||
}
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = true
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
err = endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("unable to enable UseServerMetrics inside the database")
|
||||
return
|
||||
@@ -158,6 +173,16 @@ func storageDetect(endpoint *portainer.Endpoint, endpointService dataservices.En
|
||||
}
|
||||
|
||||
func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService dataservices.EndpointService, factory *cli.ClientFactory) {
|
||||
if endpoint.Kubernetes.Flags.IsServerStorageDetected {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||
endpointService.UpdateEndpoint(
|
||||
portainer.EndpointID(endpoint.ID),
|
||||
endpoint,
|
||||
)
|
||||
}()
|
||||
log.Info().Msg("attempting to detect storage classes in the cluster")
|
||||
err := storageDetect(endpoint, endpointService, factory)
|
||||
if err == nil {
|
||||
@@ -172,3 +197,39 @@ func InitialStorageDetection(endpoint *portainer.Endpoint, endpointService datas
|
||||
log.Err(err).Msg("final error while detecting storage classes")
|
||||
}()
|
||||
}
|
||||
|
||||
func UpdateEdgeEndpointHeartbeat(endpoint *portainer.Endpoint, settings *portainer.Settings) {
|
||||
if IsEdgeEndpoint(endpoint) {
|
||||
checkInInterval := getEndpointCheckinInterval(endpoint, settings)
|
||||
endpoint.Heartbeat = endpoint.QueryDate-endpoint.LastCheckInDate <= int64(checkInInterval*2+20)
|
||||
}
|
||||
}
|
||||
|
||||
func getEndpointCheckinInterval(endpoint *portainer.Endpoint, settings *portainer.Settings) int {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
defaultInterval := 60
|
||||
intervals := [][]int{
|
||||
{endpoint.Edge.PingInterval, settings.Edge.PingInterval},
|
||||
{endpoint.Edge.CommandInterval, settings.Edge.CommandInterval},
|
||||
{endpoint.Edge.SnapshotInterval, settings.Edge.SnapshotInterval},
|
||||
}
|
||||
|
||||
for i := 0; i < len(intervals); i++ {
|
||||
effectiveInterval := intervals[i][0]
|
||||
if effectiveInterval <= 0 {
|
||||
effectiveInterval = intervals[i][1]
|
||||
}
|
||||
if effectiveInterval > 0 && effectiveInterval < defaultInterval {
|
||||
defaultInterval = effectiveInterval
|
||||
}
|
||||
}
|
||||
|
||||
return defaultInterval
|
||||
}
|
||||
|
||||
if endpoint.EdgeCheckinInterval > 0 {
|
||||
return endpoint.EdgeCheckinInterval
|
||||
}
|
||||
|
||||
return settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
package testhelpers
|
||||
|
||||
type gitService struct{}
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
// NewGitService creates new mock for portainer.GitService.
|
||||
func NewGitService() *gitService {
|
||||
return &gitService{}
|
||||
func NewGitService(cloneErr error, id string) portainer.GitService {
|
||||
return &gitService{
|
||||
cloneErr: cloneErr,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *gitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string) error {
|
||||
return nil
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultKubeClientQPS = 30
|
||||
DefaultKubeClientBurst = 100
|
||||
)
|
||||
|
||||
type (
|
||||
// ClientFactory is used to create Kubernetes clients
|
||||
ClientFactory struct {
|
||||
@@ -113,6 +118,9 @@ func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, k
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cliConfig.QPS = DefaultKubeClientQPS
|
||||
cliConfig.Burst = DefaultKubeClientBurst
|
||||
|
||||
cli, err := kubernetes.NewForConfig(cliConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -198,7 +206,10 @@ func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernete
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Insecure = true
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &agentHeaderRoundTripper{
|
||||
@@ -217,6 +228,9 @@ func buildLocalClient() (*kubernetes.Clientset, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.QPS = DefaultKubeClientQPS
|
||||
config.Burst = DefaultKubeClientBurst
|
||||
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ type (
|
||||
// It contains some information of Docker's ContainerJSON struct
|
||||
DockerContainerSnapshot struct {
|
||||
types.Container
|
||||
Env []string `json:"Env"`
|
||||
Env []string `json:"Env,omitempty"` // EE-5240
|
||||
}
|
||||
|
||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||
@@ -388,6 +388,9 @@ type (
|
||||
LastCheckInDate int64
|
||||
// QueryDate of each query with the endpoints list
|
||||
QueryDate int64
|
||||
// Heartbeat indicates the heartbeat status of an edge environment
|
||||
Heartbeat bool `json:"Heartbeat" example:"true"`
|
||||
|
||||
// IsEdgeDevice marks if the environment was created as an EdgeDevice
|
||||
IsEdgeDevice bool
|
||||
// Whether the device has been trusted or not by the user
|
||||
@@ -588,9 +591,12 @@ type (
|
||||
Flags KubernetesFlags `json:"Flags"`
|
||||
}
|
||||
|
||||
// KubernetesFlags are used to detect if we need to run initial cluster
|
||||
// detection again.
|
||||
KubernetesFlags struct {
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
IsServerMetricsDetected bool `json:"IsServerMetricsDetected"`
|
||||
IsServerIngressClassDetected bool `json:"IsServerIngressClassDetected"`
|
||||
IsServerStorageDetected bool `json:"IsServerStorageDetected"`
|
||||
}
|
||||
|
||||
// KubernetesSnapshot represents a snapshot of a specific Kubernetes environment(endpoint) at a specific time
|
||||
@@ -1286,8 +1292,6 @@ type (
|
||||
UserThemeSettings struct {
|
||||
// Color represents the color theme of the UI
|
||||
Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"`
|
||||
// SubtleUpgradeButton indicates if the upgrade banner should be displayed in a subtle way
|
||||
SubtleUpgradeButton bool `json:"subtleUpgradeButton"`
|
||||
}
|
||||
|
||||
// Webhook represents a url webhook that can be used to update a service
|
||||
@@ -1392,10 +1396,10 @@ type (
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
GitService interface {
|
||||
CloneRepository(destination string, repositoryURL, referenceName, username, password string) error
|
||||
LatestCommitID(repositoryURL, referenceName, username, password string) (string, error)
|
||||
ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error)
|
||||
ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string) ([]string, error)
|
||||
CloneRepository(destination string, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error
|
||||
LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error)
|
||||
ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error)
|
||||
ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includeExts []string, tlsSkipVerify bool) ([]string, error)
|
||||
}
|
||||
|
||||
// OpenAMTService represents a service for managing OpenAMT
|
||||
@@ -1510,7 +1514,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.18.0"
|
||||
APIVersion = "2.18.2"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
||||
@@ -53,14 +53,14 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
Str("author", author).
|
||||
Str("stack", stack.Name).
|
||||
Int("endpoint_id", int(stack.EndpointID)).
|
||||
Msg("cannot autoupdate a stack, stack author user is missing")
|
||||
Msg("cannot auto update a stack, stack author user is missing")
|
||||
|
||||
return &StackAuthorMissingErr{int(stack.ID), author}
|
||||
}
|
||||
|
||||
var gitCommitChangedOrForceUpdate bool
|
||||
if !stack.FromAppTemplate {
|
||||
updated, newHash, err := update.UpdateGitObject(gitService, datastore, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, stack.AutoUpdate, stack.ProjectPath)
|
||||
updated, newHash, err := update.UpdateGitObject(gitService, fmt.Sprintf("stack:%d", stackID), stack.GitConfig, false, stack.ProjectPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
|
||||
|
||||
err := deployer.DeployKubernetesStack(stack, endpoint, user)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to deploy a kubternetes app stack %v", stackID)
|
||||
return errors.WithMessagef(err, "failed to deploy a kubernetes app stack %v", stackID)
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
|
||||
|
||||
@@ -6,33 +6,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type gitService struct {
|
||||
cloneErr error
|
||||
id string
|
||||
}
|
||||
|
||||
func (g *gitService) CloneRepository(destination, repositoryURL, referenceName, username, password string) error {
|
||||
return g.cloneErr
|
||||
}
|
||||
|
||||
func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
|
||||
return g.id, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListRefs(repositoryURL, username, password string, hardRefresh bool) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gitService) ListFiles(repositoryURL, referenceName, username, password string, hardRefresh bool, includedExts []string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type noopDeployer struct{}
|
||||
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
@@ -67,7 +47,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNotAGitBasedStack(t *testing.T) {
|
||||
err = store.Stack().Create(&portainer.Stack{ID: 1, CreatedBy: "admin"})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""})
|
||||
err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(nil, ""))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -97,7 +77,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
|
||||
}})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{nil, "oldHash"})
|
||||
err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(nil, "oldHash"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -125,7 +105,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
|
||||
}})
|
||||
assert.NoError(t, err, "failed to create a test stack")
|
||||
|
||||
err = RedeployWhenChanged(1, nil, store, &gitService{cloneErr, "newHash"})
|
||||
err = RedeployWhenChanged(1, nil, store, testhelpers.NewGitService(cloneErr, "newHash"))
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, cloneErr, "should failed to clone but didn't, check test setup")
|
||||
}
|
||||
@@ -162,7 +142,7 @@ func Test_redeployWhenChanged(t *testing.T) {
|
||||
stack.Type = portainer.DockerComposeStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -170,7 +150,7 @@ func Test_redeployWhenChanged(t *testing.T) {
|
||||
stack.Type = portainer.DockerSwarmStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -178,7 +158,7 @@ func Test_redeployWhenChanged(t *testing.T) {
|
||||
stack.Type = portainer.KubernetesStack
|
||||
store.Stack().UpdateStack(stack.ID, &stack)
|
||||
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
|
||||
err = RedeployWhenChanged(1, &noopDeployer{}, store, testhelpers.NewGitService(nil, "newHash"))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ func (b *GitMethodStackBuilder) SetGitRepository(payload *StackPayload) GitMetho
|
||||
|
||||
repoConfig.URL = payload.URL
|
||||
repoConfig.ReferenceName = payload.ReferenceName
|
||||
repoConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
|
||||
repoConfig.ConfigFilePath = payload.ComposeFile
|
||||
if payload.ComposeFile == "" {
|
||||
repoConfig.ConfigFilePath = filesystem.ComposeFileDefaultName
|
||||
|
||||
@@ -52,4 +52,6 @@ type RepositoryConfigPayload struct {
|
||||
// Password used in basic authentication. Required when RepositoryAuthentication is true
|
||||
// and RepositoryGitCredentialID is 0
|
||||
Password string `example:"myGitPassword"`
|
||||
// TLSSkipVerify skips SSL verification when cloning the Git repository
|
||||
TLSSkipVerify bool `example:"false"`
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
|
||||
stackFolder := fmt.Sprintf("%d", stackID)
|
||||
projectPath := fileService.GetStackProjectPath(stackFolder)
|
||||
|
||||
err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password)
|
||||
err := gitService.CloneRepository(projectPath, config.URL, config.ReferenceName, username, password, config.TLSSkipVerify)
|
||||
if err != nil {
|
||||
if err == gittypes.ErrAuthenticationFailure {
|
||||
newErr := ErrInvalidGitCredential
|
||||
@@ -38,7 +38,7 @@ func DownloadGitRepository(stackID portainer.StackID, config gittypes.RepoConfig
|
||||
return "", newErr
|
||||
}
|
||||
|
||||
commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password)
|
||||
commitID, err := gitService.LatestCommitID(config.URL, config.ReferenceName, username, password, config.TLSSkipVerify)
|
||||
if err != nil {
|
||||
newErr := fmt.Errorf("unable to fetch git repository id: %w", err)
|
||||
return "", newErr
|
||||
|
||||
@@ -15,6 +15,10 @@ func UserIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.Endpo
|
||||
}
|
||||
|
||||
// GetStackFilePaths returns a list of file paths based on stack project path
|
||||
// If absolute is false, the path sanitization step will be skipped, which makes the returning
|
||||
// paths vulnerable to path traversal attacks. Thus, the followed function using the returning
|
||||
// paths are responsible to sanitize the raw paths
|
||||
// If absolute is true, the raw paths will be sanitized
|
||||
func GetStackFilePaths(stack *portainer.Stack, absolute bool) []string {
|
||||
if !absolute {
|
||||
return append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||
@@ -11,6 +12,9 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { DockerfileDetails } from '@/react/docker/images/ItemView/DockerfileDetails';
|
||||
import { HealthStatus } from '@/react/docker/containers/ItemView/HealthStatus';
|
||||
import { GpusList } from '@/react/docker/host/SetupView/GpusList';
|
||||
import { GpusInsights } from '@/react/docker/host/SetupView/GpusInsights';
|
||||
import { InsightsBox } from '@/react/components/InsightsBox';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.docker.react.components', [])
|
||||
@@ -45,4 +49,20 @@ export const componentsModule = angular
|
||||
'usedAllGpus',
|
||||
'enableGpuManagement',
|
||||
])
|
||||
).name;
|
||||
)
|
||||
.component(
|
||||
'gpusList',
|
||||
r2a(withControlledInput(GpusList), ['value', 'onChange'])
|
||||
)
|
||||
.component(
|
||||
'insightsBox',
|
||||
r2a(InsightsBox, [
|
||||
'header',
|
||||
'content',
|
||||
'setHtmlContent',
|
||||
'insightCloseId',
|
||||
'type',
|
||||
'className',
|
||||
])
|
||||
)
|
||||
.component('gpusInsights', r2a(GpusInsights, [])).name;
|
||||
|
||||
@@ -179,6 +179,8 @@ export default class DockerFeaturesConfigurationController {
|
||||
disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers,
|
||||
};
|
||||
|
||||
// this.endpoint.Gpus could be null as it is Gpus: []Pair in the API
|
||||
this.endpoint.Gpus = this.endpoint.Gpus || [];
|
||||
this.state.enableGPUManagement = this.isDockerStandaloneEnv && (this.endpoint.EnableGPUManagement || this.endpoint.Gpus.length > 0);
|
||||
this.initialGPUs = this.endpoint.Gpus;
|
||||
this.initialEnableGPUManagement = this.endpoint.EnableGPUManagement;
|
||||
|
||||
@@ -151,21 +151,7 @@
|
||||
<div class="col-sm-12 form-section-title"> Other </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 pb-3">
|
||||
<insights-box
|
||||
header="'GPU settings update'"
|
||||
set-html-content="true"
|
||||
insight-close-id="'gpu-settings-update-closed'"
|
||||
content="'
|
||||
<p>
|
||||
From 2.18 on, the set-up of available GPUs for a Docker Standalone environment has been shifted from Add environment and Environment details to Host -> Setup, so as to align with other settings.
|
||||
</p>
|
||||
<p>
|
||||
A toggle has been introduced for enabling/disabling management of GPU settings in the Portainer UI - to alleviate the performance impact of showing those settings.
|
||||
</p>
|
||||
<p>
|
||||
The UI has been updated to clarify that GPU settings support is only for Docker Standalone (and not Docker Swarm, which was never supported in the UI).
|
||||
</p>'"
|
||||
></insights-box>
|
||||
<gpus-insights></gpus-insights>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<box-selector options="options" slim="true" value="state.BuildType" on-change="(onChangeBuildType)"></box-selector>
|
||||
|
||||
<!-- web-editor -->
|
||||
<!-- TODO use web-editor-form component -->
|
||||
<div ng-show="state.BuildType === 'editor'">
|
||||
<div class="col-sm-12 form-section-title"> Web editor </div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -167,6 +167,6 @@ function confirmImageForceRemoval() {
|
||||
title: 'Are you sure?',
|
||||
modalType: ModalType.Destructive,
|
||||
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
|
||||
confirmButton: buildConfirmButton('Remote the image', 'danger'),
|
||||
confirmButton: buildConfirmButton('Remove the image', 'danger'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,13 +135,14 @@
|
||||
<!-- !execution-method -->
|
||||
|
||||
<!-- web-editor -->
|
||||
<!-- TODO use web-editor-form component -->
|
||||
<div ng-show="$ctrl.formValues.method === 'editor'">
|
||||
<div class="col-sm-12 form-section-title"> Web editor </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="execute-edge-job-editor"
|
||||
placeholder="# Define or paste the content of your script file here"
|
||||
placeholder="Define or paste the content of your script file here"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
value="$ctrl.model.FileContent"
|
||||
></code-editor>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
value="$ctrl.model.StackFileContent"
|
||||
yml="true"
|
||||
identifier="compose-editor"
|
||||
placeholder="# Define or paste the content of your docker compose file here"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
on-change="($ctrl.onChangeComposeConfig)"
|
||||
read-only="$ctrl.hasKubeEndpoint()"
|
||||
>
|
||||
@@ -63,7 +63,7 @@
|
||||
value="$ctrl.model.StackFileContent"
|
||||
yml="true"
|
||||
identifier="kube-manifest-editor"
|
||||
placeholder="# Define or paste the content of your manifest here"
|
||||
placeholder="Define or paste the content of your manifest here"
|
||||
on-change="($ctrl.onChangeKubeManifest)"
|
||||
>
|
||||
<editor-description>
|
||||
|
||||
@@ -7,12 +7,13 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
|
||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { EdgeStackDeploymentTypeSelector } from '@/react/edge/edge-stacks/components/EdgeStackDeploymentTypeSelector';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.edge.react.components', [])
|
||||
.component(
|
||||
'edgeGroupsSelector',
|
||||
r2a(withReactQuery(EdgeGroupsSelector), [
|
||||
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
||||
'onChange',
|
||||
'value',
|
||||
'error',
|
||||
|
||||
@@ -56,6 +56,7 @@ angular.module('portainer.edge').factory('EdgeStackService', function EdgeStackS
|
||||
RepositoryAuthentication: repositoryOptions.RepositoryAuthentication,
|
||||
RepositoryUsername: repositoryOptions.RepositoryUsername,
|
||||
RepositoryPassword: repositoryOptions.RepositoryPassword,
|
||||
TLSSkipVerify: repositoryOptions.TLSSkipVerify,
|
||||
}
|
||||
).$promise;
|
||||
} catch (err) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class CreateEdgeStackViewController {
|
||||
Groups: [],
|
||||
DeploymentType: 0,
|
||||
UseManifestNamespaces: false,
|
||||
TLSSkipVerify: false,
|
||||
};
|
||||
|
||||
this.EditorType = EditorType;
|
||||
@@ -86,7 +87,6 @@ export default class CreateEdgeStackViewController {
|
||||
async $onInit() {
|
||||
try {
|
||||
this.edgeGroups = await this.EdgeGroupService.groups();
|
||||
this.noGroups = this.edgeGroups.length === 0;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
||||
}
|
||||
@@ -216,6 +216,7 @@ export default class CreateEdgeStackViewController {
|
||||
RepositoryAuthentication: this.formValues.RepositoryAuthentication,
|
||||
RepositoryUsername: this.formValues.RepositoryUsername,
|
||||
RepositoryPassword: this.formValues.RepositoryPassword,
|
||||
TLSSkipVerify: this.formValues.TLSSkipVerify,
|
||||
};
|
||||
return this.EdgeStackService.createStackFromGitRepository(
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
|
||||
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
<edge-groups-selector value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
|
||||
<edge-stack-deployment-type-selector
|
||||
value="$ctrl.formValues.DeploymentType"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your docker compose file here"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
>
|
||||
<editor-description>
|
||||
You can get more information about Compose file format in the
|
||||
@@ -55,7 +55,7 @@
|
||||
value="$ctrl.formValues.StackFileContent"
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your docker compose file here"
|
||||
placeholder="Define or paste the content of your docker compose file here"
|
||||
ng-required="true"
|
||||
>
|
||||
</web-editor-form>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
value="$ctrl.formValues.StackFileContent"
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your manifest here"
|
||||
placeholder="Define or paste the content of your manifest here"
|
||||
ng-required="true"
|
||||
>
|
||||
<editor-description>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EnvironmentStatus } from '@/react/portainer/environments/types';
|
||||
import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/service';
|
||||
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
@@ -46,6 +47,14 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment && endpoint.Status === EnvironmentStatus.Down) {
|
||||
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
|
||||
}
|
||||
|
||||
// use selfsubject access review to check if we can connect to the kubernetes environment
|
||||
// because it's gets a fast response, and is accessible to all users
|
||||
try {
|
||||
await getSelfSubjectAccessReview(endpoint.Id, 'default');
|
||||
} catch (e) {
|
||||
throw new Error('Environment is unreachable.');
|
||||
}
|
||||
} catch (e) {
|
||||
let params = {};
|
||||
|
||||
|
||||
@@ -1,132 +1,144 @@
|
||||
<div class="datatable">
|
||||
<!-- toolbar header actions and settings -->
|
||||
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col gap-1">
|
||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
||||
<div ng-if="$ctrl.isPrimary" class="toolBar !flex-col !gap-0">
|
||||
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="'box'"></pr-icon>
|
||||
</div>
|
||||
Applications
|
||||
</div>
|
||||
<div class="form-group namespaces !mb-0 !mr-0 min-w-[280px]">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<pr-icon icon="'filter'"></pr-icon>
|
||||
Namespace
|
||||
</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.namespace"
|
||||
ng-change="$ctrl.onChangeNamespace()"
|
||||
data-cy="component-namespaceSelect"
|
||||
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar vertical-center !mr-0 min-w-[280px]">
|
||||
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search for an application..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sApp-searchApplicationsInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
|
||||
ui-sref="kubernetes.applications.new"
|
||||
data-cy="k8sApp-addApplicationButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
||||
</button>
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
|
||||
ui-sref="kubernetes.deploy"
|
||||
data-cy="k8sApp-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings" data-cy="k8sApp-tableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle aria-label="Settings">
|
||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||
</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Table settings </div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<label for="applications_setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||
<select
|
||||
id="settings_refresh_rate"
|
||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
class="small-select"
|
||||
data-cy="k8sApp-refreshRateDropdown"
|
||||
>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
<option value="120">2min</option>
|
||||
<option value="300">5min</option>
|
||||
</select>
|
||||
<span>
|
||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||
</span>
|
||||
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||
<div class="settings" data-cy="k8sApp-tableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle aria-label="Settings">
|
||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||
</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Table settings </div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<label for="applications_setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||
<select
|
||||
id="settings_refresh_rate"
|
||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
class="small-select"
|
||||
data-cy="k8sApp-refreshRateDropdown"
|
||||
>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
<option value="120">2min</option>
|
||||
<option value="300">5min</option>
|
||||
</select>
|
||||
<span>
|
||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||
<div>
|
||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sApp-removeAppButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
hide-deployment-option="form"
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary vertical-center !ml-0 h-fit"
|
||||
ui-sref="kubernetes.applications.new"
|
||||
data-cy="k8sApp-addApplicationButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
||||
</button>
|
||||
<button
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
|
||||
ui-sref="kubernetes.deploy"
|
||||
data-cy="k8sApp-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="searchBar">
|
||||
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
data-cy="k8sApp-searchApplicationsInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] min-w-[140px]">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
|
||||
Namespace
|
||||
</span>
|
||||
<select
|
||||
class="form-control !h-[30px] !py-1"
|
||||
ng-model="$ctrl.state.namespace"
|
||||
ng-change="$ctrl.onChangeNamespace()"
|
||||
data-cy="component-namespaceSelect"
|
||||
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||
<span class="small text-muted vertical-center mt-1">
|
||||
<pr-icon icon="'info'" mode="'primary'" class="vertical-center"></pr-icon>
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
System resources are hidden, this can be changed in the table settings.
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="w-fit">
|
||||
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- data table content -->
|
||||
<div ng-class="{ 'table-responsive': $ctrl.isPrimary, 'inner-datatable': !$ctrl.isPrimary }">
|
||||
@@ -232,6 +244,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
ng-show="!$ctrl.isAppsLoading"
|
||||
ng-click="$ctrl.expandItem(item, !$ctrl.isItemExpanded(item))"
|
||||
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.applyFilters | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||
ng-class="{ active: item.Checked, interactive: $ctrl.isExpandable(item), 'secondary-body': !$ctrl.isPrimary }"
|
||||
@@ -323,16 +336,16 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<tr ng-if="$ctrl.isAppsLoading">
|
||||
<td colspan="8" class="text-muted text-center">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0 && !$ctrl.isAppsLoading">
|
||||
<td colspan="8" class="text-muted text-center">No application available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div ng-if="$ctrl.isPrimary" class="footer pl-5" ng-if="$ctrl.dataset">
|
||||
<div class="footer pl-5" ng-if="$ctrl.isPrimary && $ctrl.dataset">
|
||||
<div class="infoBar !ml-0" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
|
||||
@@ -18,6 +18,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
|
||||
namespaces: '<',
|
||||
namespace: '<',
|
||||
onChangeNamespaceDropdown: '<',
|
||||
isAppsLoading: '<',
|
||||
isSystemResources: '<',
|
||||
setSystemResources: '<',
|
||||
},
|
||||
|
||||
@@ -144,15 +144,17 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||
};
|
||||
|
||||
this.updateNamespace = function () {
|
||||
if (this.namespaces) {
|
||||
const namespaces = [{ Name: 'All namespaces', Value: '', IsSystem: false }];
|
||||
this.namespaces.find((ns) => {
|
||||
if (!this.settings.showSystem && ns.IsSystem) {
|
||||
return false;
|
||||
}
|
||||
namespaces.push({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem });
|
||||
});
|
||||
this.state.namespaces = namespaces;
|
||||
if (this.namespaces && this.settingsLoaded) {
|
||||
const allNamespacesOption = { Name: 'All namespaces', Value: '', IsSystem: false };
|
||||
const visibleNamespaceOptions = this.namespaces
|
||||
.filter((ns) => {
|
||||
if (!this.settings.showSystem && ns.IsSystem) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((ns) => ({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem }));
|
||||
this.state.namespaces = [allNamespacesOption, ...visibleNamespaceOptions];
|
||||
|
||||
if (this.state.namespace && !this.state.namespaces.find((ns) => ns.Name === this.state.namespace)) {
|
||||
if (this.state.namespaces.length > 1) {
|
||||
@@ -216,7 +218,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
||||
|
||||
this.setSystemResources && this.setSystemResources(this.settings.showSystem);
|
||||
}
|
||||
|
||||
this.settingsLoaded = true;
|
||||
// Set the default selected namespace
|
||||
if (!this.state.namespace) {
|
||||
this.state.namespace = this.namespace;
|
||||
|
||||
@@ -1,115 +1,122 @@
|
||||
<div class="datatable">
|
||||
<!-- table title and action menu -->
|
||||
<div class="toolBar !flex-col gap-1">
|
||||
<div class="toolBar vertical-center w-full flex-wrap !gap-x-5 !gap-y-1 !p-0">
|
||||
<!-- title -->
|
||||
<div class="toolBar !flex-col !gap-0">
|
||||
<div class="toolBar w-full !items-start !gap-x-5 !p-0">
|
||||
<div class="toolBarTitle vertical-center">
|
||||
<div class="widget-icon space-right">
|
||||
<pr-icon icon="'list'"></pr-icon>
|
||||
</div>
|
||||
Stacks
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div class="form-group namespaces !mb-0 !mr-0 min-w-[280px]">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<pr-icon icon="'filter'"></pr-icon>
|
||||
Namespace
|
||||
</span>
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="$ctrl.state.namespace"
|
||||
ng-change="$ctrl.onChangeNamespace()"
|
||||
data-cy="component-namespaceSelect"
|
||||
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||
<!-- use row reverse to make the left most items wrap first to the right side in the next line -->
|
||||
<div class="inline-flex flex-row-reverse flex-wrap !gap-x-5 gap-y-3">
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput min-w-min self-start"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search for a stack..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div class="actionBar !mr-0 !gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
data-cy="k8sApp-removeStackButton"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle>
|
||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||
</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Table settings </div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<label for="applications_setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox-stack"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||
<select
|
||||
id="settings_refresh_rate"
|
||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
class="small-select"
|
||||
data-cy="k8sApp-refreshRateDropdown-stack"
|
||||
>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
<option value="120">2min</option>
|
||||
<option value="300">5min</option>
|
||||
</select>
|
||||
<span>
|
||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||
</span>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<div class="settings" data-cy="k8sApp-StackTableSettings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||
<span uib-dropdown-toggle>
|
||||
<pr-icon icon="'more-vertical'" class-name="'!mr-0 !h-4'"></pr-icon>
|
||||
</span>
|
||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Table settings </div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
|
||||
<label for="applications_setting_show_system">Show system resources</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input
|
||||
id="setting_auto_refresh"
|
||||
type="checkbox"
|
||||
ng-model="$ctrl.settings.repeater.autoRefresh"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
data-cy="k8sApp-autoRefreshCheckbox-stack"
|
||||
/>
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
</div>
|
||||
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||
<label for="settings_refresh_rate"> Refresh rate </label>
|
||||
<select
|
||||
id="settings_refresh_rate"
|
||||
ng-model="$ctrl.settings.repeater.refreshRate"
|
||||
ng-change="$ctrl.onSettingsRepeaterChange()"
|
||||
class="small-select"
|
||||
data-cy="k8sApp-refreshRateDropdown-stack"
|
||||
>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">1min</option>
|
||||
<option value="120">2min</option>
|
||||
<option value="300">5min</option>
|
||||
</select>
|
||||
<span>
|
||||
<pr-icon id="refreshRateChange" icon="'check'" mode="'success'" style="display: none"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
|
||||
<div>
|
||||
<a type="button" class="btn btn-sm btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton-stack">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar vertical-center">
|
||||
<pr-icon icon="'search'" class-name="'!h-3'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="searchInput min-w-min self-start"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
auto-focus
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group namespaces !mb-0 !mr-0 !h-[30px] w-fit min-w-[140px]">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<pr-icon icon="'filter'" size="'sm'"></pr-icon>
|
||||
Namespace
|
||||
</span>
|
||||
<select
|
||||
class="form-control !h-[30px] !py-1"
|
||||
ng-model="$ctrl.state.namespace"
|
||||
ng-change="$ctrl.onChangeNamespace()"
|
||||
data-cy="component-namespaceSelect"
|
||||
ng-options="o.Value as (o.Name + (o.IsSystem ? ' - system' : '')) for o in $ctrl.state.namespaces"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- info text -->
|
||||
<div class="flex w-full flex-row">
|
||||
<span class="small text-muted vertical-center mt-1" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||
<div class="flex w-full flex-row" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
|
||||
<span class="small text-muted vertical-center mt-1">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
System resources are hidden, this can be changed in the table settings.
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="w-fit">
|
||||
<insights-box class-name="'mt-2'" type="'slim'" header="'From 2.18 on, you can filter this view by namespace.'" insight-close-id="'k8s-namespace-filtering'"></insights-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table-hover nowrap-cells table">
|
||||
@@ -161,6 +168,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
ng-show="!$ctrl.isAppsLoading"
|
||||
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||
ng-class="{ active: item.Checked, 'datatable-highlighted': item.Highlighted }"
|
||||
ng-click="$ctrl.expandItem(item, !item.Expanded)"
|
||||
@@ -213,10 +221,10 @@
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<tr ng-if="$ctrl.isAppsLoading">
|
||||
<td colspan="5" class="text-muted text-center">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0 && !$ctrl.isAppsLoading">
|
||||
<td colspan="5" class="text-muted text-center">No stack available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -13,6 +13,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDa
|
||||
namespaces: '<',
|
||||
namespace: '<',
|
||||
onChangeNamespaceDropdown: '<',
|
||||
isAppsLoading: '<',
|
||||
isSystemResources: '<',
|
||||
setSystemResources: '<',
|
||||
},
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
<div class="actionBar">
|
||||
<form class="form-horizontal" name="addUserHelmRepoForm">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small"> Add a Helm repository. All Helm charts in the repository will be added to the list. </span>
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
Add a Helm repository. All Helm charts in the repository will be added to the list.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-2">
|
||||
|
||||
@@ -141,7 +141,9 @@ export default class HelmTemplatesController {
|
||||
try {
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
||||
const nonSystemNamespaces = resourcePools.filter((resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||
const nonSystemNamespaces = resourcePools.filter(
|
||||
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
this.state.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||
this.state.resourcePool = this.state.resourcePools[0];
|
||||
} catch (err) {
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
value="$ctrl.state.values"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your values yaml file here"
|
||||
placeholder="Define or paste the content of your values yaml file here"
|
||||
>
|
||||
<editor-description class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
value="$ctrl.formValues.DataYaml"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
yml="true"
|
||||
placeholder="# Define or paste key-value pairs, one pair per line"
|
||||
placeholder="Define or paste key-value pairs, one pair per line"
|
||||
>
|
||||
</web-editor-form>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
identifier="application-details-yaml"
|
||||
value="$ctrl.data"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your manifest here"
|
||||
placeholder="Define or paste the content of your manifest here"
|
||||
read-only="true"
|
||||
hide-title="true"
|
||||
height="{{ $ctrl.expanded ? '800px' : '500px' }}"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your manifest file here"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
on-change="($ctrl.onChangeFileContent)"
|
||||
ng-required="true"
|
||||
yml="true"
|
||||
placeholder="# Define or paste the content of your manifest file here"
|
||||
placeholder="Define or paste the content of your manifest file here"
|
||||
>
|
||||
<editor-description>
|
||||
<p>Templates allow deploying any kind of Kubernetes resource (Deployment, Secret, ConfigMap...)</p>
|
||||
|
||||
@@ -18,7 +18,6 @@ class KubernetesNamespaceService {
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
this.getJSONAsync = this.getJSONAsync.bind(this);
|
||||
this.updateFinalizeAsync = this.updateFinalizeAsync.bind(this);
|
||||
this.refreshCacheAsync = this.refreshCacheAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,15 +84,9 @@ class KubernetesNamespaceService {
|
||||
if (name) {
|
||||
return this.$async(this.getAsync, name);
|
||||
}
|
||||
const cachedAllowedNamespaces = this.LocalStorage.getAllowedNamespaces();
|
||||
if (cachedAllowedNamespaces) {
|
||||
updateNamespaces(cachedAllowedNamespaces);
|
||||
return cachedAllowedNamespaces;
|
||||
} else {
|
||||
const allowedNamespaces = await this.getAllAsync();
|
||||
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
|
||||
return allowedNamespaces;
|
||||
}
|
||||
const allowedNamespaces = await this.getAllAsync();
|
||||
updateNamespaces(allowedNamespaces);
|
||||
return allowedNamespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +97,6 @@ class KubernetesNamespaceService {
|
||||
const payload = KubernetesNamespaceConverter.createPayload(namespace);
|
||||
const params = {};
|
||||
const data = await this.KubernetesNamespaces().create(params, payload).$promise;
|
||||
await this.refreshCacheAsync();
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to create namespace', err);
|
||||
@@ -115,14 +107,6 @@ class KubernetesNamespaceService {
|
||||
return this.$async(this.createAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshCacheAsync() {
|
||||
this.LocalStorage.deleteAllowedNamespaces();
|
||||
const allowedNamespaces = await this.getAllAsync();
|
||||
this.LocalStorage.storeAllowedNamespaces(allowedNamespaces);
|
||||
updateNamespaces(allowedNamespaces);
|
||||
return allowedNamespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user