Compare commits

...

69 Commits

Author SHA1 Message Date
deviantony
ababd63d97 feat: update semver matching 2024-10-04 07:57:31 +00:00
James Carppe
b40d22dc74 Update bug report template for 2.22.0 (#12283) 2024-10-03 14:53:37 +13:00
Steven Kang
a257696c25 fix access conditions when the restrict default namespace is enabled (#12280) 2024-10-02 15:55:05 +13:00
andres-portainer
f742937359 fix(endpoints): optimize the search performance BE-11267 (#12262) 2024-10-01 15:13:54 -03:00
Steven Kang
c0db48b29d fix ingress creation for none class (#12273) 2024-10-01 14:43:46 +13:00
Steven Kang
ea228c3d6d refactor(k8s): namespace core logic (#12142)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2024-10-01 14:15:51 +13:00
Ali
da010f3d08 fix(podman): ensure initial env type matches container runtime [r8s-98] (#12259) 2024-09-30 09:16:24 +13:00
Ali
32e94d4e4e feat(podman): support add podman envs in the wizard [r8s-20] (#12056) 2024-09-25 11:55:07 +12:00
Ali
db616bc8a5 fix(wizard): update nodeport placeholder [r8s-62] (#12255) 2024-09-25 11:36:50 +12:00
James Carppe
b8b46ec129 Update bug report template for 2.21.2 (#12251) 2024-09-24 11:42:15 +12:00
LP B
7d0b79a546 fix(app/images): export images to tar (#12223) 2024-09-23 21:55:45 +02:00
LP B
fd26565b14 fix(app/templates): non admins cannot load templates list (#12235) 2024-09-23 17:54:32 +02:00
Nik Wakelin
e0b6f2283a chore(branding): Changes Linode to Akamai Connected Cloud (#12221) 2024-09-23 09:21:02 +12:00
Oscar Zhou
d3d3d50569 fix(version): add specific version for updater image [BE-11153] (#12227) 2024-09-21 14:54:08 +12:00
andres-portainer
cee997e0b3 fix(edgestacks): reorder operations to properly update the endpoint relations BE-11233 (#12239) 2024-09-20 19:10:28 -03:00
LP B
80f53ed6ec fix(api): skip guessing env when there is no env in DB (#12238) 2024-09-20 17:56:41 -03:00
Chaim Lev-Ari
6f84317e7a feat(system): upgrade on swarm [EE-5848] (#11728)
Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-09-20 18:00:38 +02:00
LP B
3cb484f06a fix(app/users): password validation hint + missing message on empty teams list (#12231) 2024-09-20 16:33:13 +02:00
LP B
61353cbe8a fix(app/edge): race between redirects when selecting a template (#12230) 2024-09-20 16:00:40 +02:00
Yajith Dayarathna
d647980c3a updating attest params (#12228) 2024-09-20 11:48:32 +12:00
Oscar Zhou
5740abe31b fix(authorization): add registry button disappear for admin [BE-11228] (#12213) 2024-09-20 08:18:51 +12:00
andres-portainer
5fd4f52e35 fix(jwt): fix handling of non-expiring JWT tokens BE-11242 (#12220) 2024-09-17 18:23:33 -03:00
Yajith Dayarathna
dbe7cd16d4 2024-09-CVE (#12189) 2024-09-11 11:08:46 +12:00
Yajith Dayarathna
2b630ca2dd enabling build attestations (#12211) 2024-09-11 10:57:52 +12:00
Oscar Zhou
2ede22646b fix(version): add specific version for updater image [BE-11153] (#12202) 2024-09-11 08:29:23 +12:00
James Carppe
994b6bb471 Update bug report template for 2.21.1 (#12207) 2024-09-10 14:33:32 +12:00
andres-portainer
92f338e0cd fix(users): fix data-race in userCreate() BE-11209 (#12193) 2024-09-05 22:28:04 -03:00
andres-portainer
7a176cf284 fix(teams): fix data-race in teamCreate() BE-11210 (#12195) 2024-09-05 21:36:13 -03:00
Oscar Zhou
80e607ab30 fix(stack): env placeholder as host path [BE-11187] (#12192) 2024-09-06 08:43:12 +12:00
Anthony Lapenna
6cff21477e service: update stop grace period description (#12173) 2024-09-05 08:47:06 +02:00
Yajith Dayarathna
4bb5a7f480 updating ci workflow (#12183) 2024-09-05 09:19:36 +12:00
andres-portainer
9a88511d00 fix(docker): avoid specifying the MAC address of container for Docker API < v1.44 BE-10880 (#12179) 2024-09-03 10:31:24 -03:00
Yajith Dayarathna
48cd614948 CVE 2024 43798 (#12171) 2024-09-03 09:27:24 +12:00
andres-portainer
2fe252d62b fix(jwt): generate JWT IDs BE-11179 (#12175) 2024-09-02 12:06:39 -03:00
LP B
8fae7f8438 feat(app/wizard): info panel telling to add env only once per swarm cluster (#11954) 2024-09-02 14:22:07 +02:00
andres-portainer
e4e55157e8 fix(bouncer): add support for JWT revocation BE-11179 (#12164) 2024-08-30 20:24:05 -03:00
Yajith Dayarathna
a5e246cc16 testing go directive change (#12124) 2024-08-30 08:27:42 +02:00
andres-portainer
d28dc59584 fix(git): optimize listFiles() BE-11184 (#12160) 2024-08-29 19:01:51 -03:00
andres-portainer
5353570721 task(code): remove unnecessary uses of govalidator BE-11181 (#12156) 2024-08-28 19:37:20 -03:00
andres-portainer
eb3e367ba8 fix(edgestacks): change the level of a logged line EE-6874 (#11396) 2024-08-28 18:16:34 -03:00
Chaim Lev-Ari
3c1441d462 refactor(users): migrate list view to react [EE-2202] (#11914) 2024-08-28 17:04:32 -03:00
Chaim Lev-Ari
33ce841040 refactor(docker/events): migrate list view to react [EE-2228] (#11581) 2024-08-28 16:41:15 -03:00
Chaim Lev-Ari
9797201c2a feat(docker): label gpu as nvidia only [EE-6999] (#11729) 2024-08-28 16:38:27 -03:00
Chaim Lev-Ari
6e14ac583b fix(access-control): fix dt column header typo [EE-7113] (#11853) 2024-08-28 16:37:12 -03:00
Anthony Lapenna
0b37b677c1 refactor: fix linting issues across the codebase (#12152) 2024-08-28 15:03:15 +02:00
Oscar Zhou
f59dd34154 fix(swarm/service): list task when filtering service [BE-11029] (#12146) 2024-08-28 18:28:38 +12:00
James Carppe
e8ec648886 Update bug report template for 2.21.0 (#12145) 2024-08-27 16:42:49 +12:00
Ali
10767a06df fix(invalidate): keep invalidate default behaviour [BE-11064] (#12080) 2024-08-27 09:48:50 +12:00
James Carppe
59b3375b59 Update bug report template for 2.21.0-rc2 (#12128) 2024-08-23 10:55:43 +12:00
andres-portainer
4408fd0cd3 chore(polling): simplify the polling logic BE-4585 (#12121) 2024-08-22 10:54:34 -03:00
Yajith Dayarathna
975a9517b9 undo change to go directive 2024-08-22 16:21:13 +12:00
Yajith Dayarathna
89c92b7834 updating go directive 2024-08-22 16:17:28 +12:00
Anthony Lapenna
747cea8084 security: bump dependencies to address CVEs (#12119) 2024-08-21 20:08:25 +12:00
Ali
f016b31388 fix(docker-desktop): support auth cookies [BE-11134] (#12108) 2024-08-21 18:21:51 +12:00
Oscar Zhou
8cd53a4b7a fix(registry): non admin can see add registry button [BE-10834] (#12112) 2024-08-21 11:00:00 +12:00
LP B
a39abe61c2 fix(api/edge_stacks): ensure edge stacks related endpoints list generation returns unique elements (#12101) 2024-08-20 10:20:03 +02:00
James Carppe
054898f821 Update bug report template for 2.21.0-rc1 (#12104) 2024-08-15 19:27:24 +12:00
Oscar Zhou
13d9b12a2e fix(group): create group twice when associating devices [EE-7418] (#12092) 2024-08-12 17:09:49 +12:00
LP B
aaec856282 fix(app/registries): enforce user accesses on registries (#12087) 2024-08-10 11:53:16 +02:00
andres-portainer
009eec9475 fix(compose): avoid the need to pass the file to remove the stack BE-11057 (#12065)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
2024-08-09 10:22:31 -03:00
Yajith Dayarathna
8d14535fd5 updating github workflow 2024-08-09 14:58:20 +12:00
Oscar Zhou
cc7f14951c fix(stack/remote): pass forceRecreate setting [EE-7374] (#12051) 2024-08-06 09:02:21 +12:00
Yajith Dayarathna
b67ff87f35 Installing docker-compose during test-server step (#12075) 2024-08-05 11:28:47 +12:00
andres-portainer
f55ef6e691 fix(pendingactions): remove excessive logging BE-11094 (#12071) 2024-08-02 16:35:14 -03:00
andres-portainer
560a1a00ca fix(scheduler): remove jobs that won't be used anymore BE-11045 (#12058) 2024-08-01 10:59:29 -03:00
andres-portainer
3b5ce1b053 fix(scheduler): remove unnecessary goroutines BE-11044 (#12059) 2024-08-01 10:58:53 -03:00
andres-portainer
03e8d05f18 fix(scheduler): fix a data race in a unit test BE-11084 (#12057) 2024-08-01 10:58:08 -03:00
Oscar Zhou
bedb7fb255 fix(swarm): auto multi-select volume with same name [EE-7240] (#11955) 2024-07-31 12:12:26 +12:00
Oscar Zhou
4d586f7a85 fix(docker): missing browse volume option [EE-7179] (#11901) 2024-07-30 08:53:17 +12:00
582 changed files with 14698 additions and 5341 deletions

View File

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

View File

@@ -22,7 +22,6 @@ on:
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
jobs:
@@ -34,7 +33,6 @@ jobs:
- { platform: linux, arch: arm64, version: "" }
- { platform: linux, arch: arm, version: "" }
- { platform: linux, arch: ppc64le, version: "" }
- { platform: linux, arch: s390x, version: "" }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
@@ -47,7 +45,7 @@ jobs:
- name: '[preparation] set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
go-version-file: go.mod
- name: '[preparation] set up node.js'
uses: actions/setup-node@v4.0.1
with:
@@ -103,14 +101,14 @@ jobs:
run: |
if [ "${{ matrix.config.platform }}" == "windows" ]; then
mv dist/portainer dist/portainer.exe
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
else
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
env:
@@ -152,25 +150,17 @@ jobs:
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x"
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64"
fi

View File

@@ -5,6 +5,7 @@ env:
NODE_VERSION: 18.x
on:
workflow_dispatch:
pull_request:
branches:
- master
@@ -27,15 +28,22 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up node.js'
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
test-server:
strategy:
matrix:
@@ -48,9 +56,21 @@ jobs:
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
- name: Run tests
- name: 'install dependencies'
run: make test-deps PLATFORM=linux ARCH=amd64
- name: 'update $PATH'
run: echo "$(pwd)/dist" >> $GITHUB_PATH
- name: 'run tests'
run: make test-server

View File

@@ -9,8 +9,6 @@ linters:
- gosimple
- govet
- errorlint
- copyloopvar
- intrange
linters-settings:
depguard:

View File

@@ -30,7 +30,7 @@ build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
yarn storybook:build
@@ -64,6 +64,9 @@ clean: ## Remove all build and download artifacts
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
test-deps: init-dist
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
test-client: ## Run client tests
yarn test $(ARGS)
@@ -82,6 +85,8 @@ dev-client: ## Run the client in development mode
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
dev-server-podman: build-server ## Run the server in development mode
@./dev/run_container_podman.sh
##@ Format
.PHONY: format format-client format-server

View File

@@ -45,6 +45,7 @@ import (
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/featureflags"
@@ -532,7 +533,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
platformService, err := platform.NewService(dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing platform service")
}
upgradeService, err := upgrade.NewService(
*flags.Assets,
kubernetesClientFactory,
dockerClientFactory,
composeStackManager,
dataStore,
fileService,
stackDeployer,
)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
@@ -589,6 +603,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
UpgradeService: upgradeService,
AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService,
PlatformService: platformService,
}
}

View File

@@ -19,8 +19,7 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
@@ -32,6 +31,16 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]{
Bucket: BucketName,
Connection: service.Connection,
Tx: tx,
},
}
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team

View File

@@ -0,0 +1,48 @@
package team
import (
"errors"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
type ServiceTx struct {
dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]
}
// TeamByName returns a team by name.
func (service ServiceTx) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team
err := service.Tx.GetAll(
BucketName,
&portainer.Team{},
dataservices.FirstFn(&t, func(e portainer.Team) bool {
return strings.EqualFold(e.Name, name)
}),
)
if errors.Is(err, dataservices.ErrStop) {
return &t, nil
}
if err == nil {
return nil, dserrors.ErrObjectNotFound
}
return nil, err
}
// CreateTeam creates a new Team.
func (service ServiceTx) Create(team *portainer.Team) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},
)
}

View File

@@ -85,7 +85,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
@@ -106,7 +106,7 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
@@ -126,7 +126,7 @@ func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.T
BucketName,
&portainer.TeamMembership{},
func(obj any) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)

View File

@@ -9,7 +9,7 @@ import (
"path/filepath"
"testing"
"github.com/Masterminds/semver"
"github.com/Masterminds/semver/v3"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"

View File

@@ -7,7 +7,7 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/Masterminds/semver"
"github.com/Masterminds/semver/v3"
"github.com/rs/zerolog/log"
)

View File

@@ -3,7 +3,7 @@ package migrator
import (
"errors"
"github.com/Masterminds/semver"
"github.com/Masterminds/semver/v3"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/dockerhub"

View File

@@ -90,7 +90,7 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
switch {
case endpointutils.IsKubernetesEndpoint(environment):
// get the kubeclient for the environment, and skip all kube migrations if there's an error
kubeclient, err := migrator.kubeFactory.GetKubeClient(environment)
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
if err != nil {
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err

View File

@@ -389,7 +389,6 @@ type storeExport struct {
}
func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().ReadAll(); err != nil {
@@ -593,6 +592,7 @@ func (store *Store) Export(filename string) (err error) {
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}

View File

@@ -82,7 +82,9 @@ func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService {
return tx.store.TeamMembershipService.Tx(tx.tx)
}
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
func (tx *StoreTx) Team() dataservices.TeamService {
return tx.store.TeamService.Tx(tx.tx)
}
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }

View File

@@ -38,6 +38,7 @@
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"ContainerEngine": "",
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
@@ -601,7 +602,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell",
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -768,6 +769,7 @@
"GpuUseList": null,
"HealthyContainerCount": 0,
"ImageCount": 9,
"IsPodman": false,
"NodeCount": 0,
"RunningContainerCount": 5,
"ServiceCount": 0,

View File

@@ -9,6 +9,7 @@ import (
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/docker/images"
"github.com/Masterminds/semver/v3"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
@@ -30,6 +31,44 @@ func NewContainerService(factory *dockerclient.ClientFactory, dataStore dataserv
}
}
// applyVersionConstraint uses the version to apply a transformation function to
// the value when the constraint is satisfied
func applyVersionConstraint[T any](currentVersion, versionConstraint string, value T, transform func(T) T) (T, error) {
newValue := value
constraint, err := semver.NewConstraint(versionConstraint)
if err != nil {
return newValue, errors.New("invalid version constraint specified")
}
currentVer, err := semver.NewVersion(currentVersion)
if err != nil {
log.Warn().Err(err).Msg("Unable to parse the Docker client version")
return newValue, nil
}
if satisfiesConstraint, _ := constraint.Validate(currentVer); satisfiesConstraint {
newValue = transform(value)
}
return newValue, nil
}
func clearMacAddrs(n network.NetworkingConfig) network.NetworkingConfig {
netConfig := network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings),
}
for k := range n.EndpointsConfig {
endpointConfig := n.EndpointsConfig[k].Copy()
endpointConfig.MacAddress = ""
netConfig.EndpointsConfig[k] = endpointConfig
}
return netConfig
}
// Recreate a container
func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.Endpoint, containerId string, forcePullImage bool, imageTag, nodeName string) (*types.ContainerJSON, error) {
cli, err := c.factory.CreateClient(endpoint, nodeName, nil)
@@ -83,7 +122,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
return nil, errors.Wrap(err, "rename container error")
}
networkWithCreation := network.NetworkingConfig{
initialNetwork := network.NetworkingConfig{
EndpointsConfig: make(map[string]*network.EndpointSettings),
}
@@ -95,10 +134,10 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
}
// 5. get the first network attached to the current container
if len(networkWithCreation.EndpointsConfig) == 0 {
if len(initialNetwork.EndpointsConfig) == 0 {
// Retrieve the first network that is linked to the present container, which
// will be utilized when creating the container.
networkWithCreation.EndpointsConfig[name] = network
initialNetwork.EndpointsConfig[name] = network
}
}
c.sr.enable()
@@ -124,7 +163,15 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
// to retain the same network settings we have to connect on creation to one of the old
// container's networks, and connect to the other networks after creation.
// see: https://portainer.atlassian.net/browse/EE-5448
create, err := cli.ContainerCreate(ctx, container.Config, container.HostConfig, &networkWithCreation, nil, container.Name)
// Docker API < 1.44 does not support specifying MAC addresses
// https://github.com/moby/moby/blob/6aea26b431ea152a8b085e453da06ea403f89886/client/container_create.go#L44-L46
initialNetwork, err = applyVersionConstraint(cli.ClientVersion(), "< 1.44", initialNetwork, clearMacAddrs)
if err != nil {
return nil, err
}
create, err := cli.ContainerCreate(ctx, container.Config, container.HostConfig, &initialNetwork, nil, container.Name)
c.sr.push(func() {
log.Debug().Str("container_id", create.ID).Msg("removing the new container")
@@ -144,7 +191,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
log.Debug().Str("container_id", newContainerId).Msg("connecting networks to container")
networks := container.NetworkSettings.Networks
for key, network := range networks {
if _, ok := networkWithCreation.EndpointsConfig[key]; ok {
if _, ok := initialNetwork.EndpointsConfig[key]; ok {
// skip the network that is used during container creation
continue
}

View File

@@ -0,0 +1,52 @@
package docker
import (
"testing"
"github.com/docker/docker/api/types/network"
"github.com/stretchr/testify/require"
)
func TestApplyVersionConstraint(t *testing.T) {
initialNet := network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
"key1": {
MacAddress: "mac1",
EndpointID: "endpointID1",
},
"key2": {
MacAddress: "mac2",
EndpointID: "endpointID2",
},
},
}
f := func(currentVer string, constraint string, success, emptyMac bool) {
t.Helper()
transformedNet, err := applyVersionConstraint(currentVer, constraint, initialNet, clearMacAddrs)
if success {
require.NoError(t, err)
} else {
require.Error(t, err)
}
require.Len(t, transformedNet.EndpointsConfig, len(initialNet.EndpointsConfig))
for k := range initialNet.EndpointsConfig {
if emptyMac {
require.NotEqual(t, initialNet.EndpointsConfig[k], transformedNet.EndpointsConfig[k])
require.Empty(t, transformedNet.EndpointsConfig[k].MacAddress)
continue
}
require.Equal(t, initialNet.EndpointsConfig[k], transformedNet.EndpointsConfig[k])
}
}
f("1.45", "< 1.44", true, false) // No transformation
f("1.43", "< 1.44", true, true) // Transformation
f("a.b.", "< 1.44", true, false) // Invalid current version
f("1.45", "z 1.44", false, false) // Invalid version constraint
}

View File

@@ -267,6 +267,17 @@ func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) err
}
snapshot.SnapshotRaw.Version = version
snapshot.IsPodman = isPodman(version)
return nil
}
// isPodman checks if the version is for Podman by checking if any of the components contain "podman".
// If it's podman, a component name should be "Podman Engine"
func isPodman(version types.Version) bool {
for _, component := range version.Components {
if strings.Contains(strings.ToLower(component.Name), "podman") {
return true
}
}
return false
}

View File

@@ -38,7 +38,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeUpOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -61,7 +61,39 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
Host: url,
ProjectName: stack.Name,
},
ForceRecreate: forceRecreate,
ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit,
})
return errors.Wrap(err, "failed to deploy a stack")
}
// Run runs a one-off command on a service. Wraps `docker-compose run` command
func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, serviceName string, options portainer.ComposeRunOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
}
if proxy != nil {
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Run(ctx, filePaths, serviceName, libstack.RunOptions{
Options: libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
},
Remove: options.Remove,
Args: options.Args,
Detached: options.Detached,
})
return errors.Wrap(err, "failed to deploy a stack")
}

View File

@@ -60,7 +60,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, false)
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{})
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}

View File

@@ -44,7 +44,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
}
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
kubeCLI, err := deployer.kubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return "", err
}

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"strings"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
type MultiFilterArgs []struct {

View File

@@ -1,9 +1,10 @@
package filesystem
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
"testing"
)
func TestMultiFilterDirForPerDevConfigs(t *testing.T) {

View File

@@ -6,6 +6,8 @@ import (
"path/filepath"
"strings"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
@@ -14,7 +16,6 @@ import (
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
gittypes "github.com/portainer/portainer/api/git/types"
)
type gitClient struct {
@@ -143,6 +144,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
Tags: git.NoTags,
}
repo, err := git.Clone(memory.NewStorage(), nil, cloneOption)
@@ -166,7 +168,10 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
}
var allPaths []string
w := object.NewTreeWalker(tree, true, nil)
defer w.Close()
for {
name, entry, err := w.Next()
if err != nil {

View File

@@ -91,6 +91,29 @@ func Test_latestCommitID(t *testing.T) {
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
}
func Test_ListRefs(t *testing.T) {
service := Service{git: NewGitClient(true)}
repositoryURL := setup(t)
fs, err := service.ListRefs(repositoryURL, "", "", false, false)
assert.NoError(t, err)
assert.Equal(t, []string{"refs/heads/main"}, fs)
}
func Test_ListFiles(t *testing.T) {
service := Service{git: NewGitClient(true)}
repositoryURL := setup(t)
referenceName := "refs/heads/main"
fs, err := service.ListFiles(repositoryURL, referenceName, "", "", false, false, []string{".yml"}, false)
assert.NoError(t, err)
assert.Equal(t, []string{"docker-compose.yml"}, fs)
}
func getCommitHistoryLength(t *testing.T, err error, dir string) int {
repo, err := git.PlainOpen(dir)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
lru "github.com/hashicorp/golang-lru"
"github.com/rs/zerolog/log"
"golang.org/x/sync/singleflight"
)
const (
@@ -139,12 +140,18 @@ func (service *Service) CloneRepository(destination, repositoryURL, referenceNam
return service.cloneRepository(destination, options)
}
func (service *Service) cloneRepository(destination string, options cloneOption) error {
func (service *Service) repoManager(options baseOption) repoManager {
repoManager := service.git
if isAzureUrl(options.repositoryUrl) {
return service.azure.download(context.TODO(), destination, options)
repoManager = service.azure
}
return service.git.download(context.TODO(), destination, options)
return repoManager
}
func (service *Service) cloneRepository(destination string, options cloneOption) error {
return service.repoManager(options.baseOption).download(context.TODO(), destination, options)
}
// LatestCommitID returns SHA1 of the latest commit of the specified reference
@@ -159,11 +166,7 @@ func (service *Service) LatestCommitID(repositoryURL, referenceName, username, p
referenceName: referenceName,
}
if isAzureUrl(options.repositoryUrl) {
return service.azure.latestCommitID(context.TODO(), options)
}
return service.git.latestCommitID(context.TODO(), options)
return service.repoManager(options.baseOption).latestCommitID(context.TODO(), options)
}
// ListRefs will list target repository's references without cloning the repository
@@ -174,21 +177,16 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
service.repoRefCache.Remove(refCacheKey)
// Remove file caches pointed to the same repository
for _, fileCacheKey := range service.repoFileCache.Keys() {
key, ok := fileCacheKey.(string)
if ok {
if strings.HasPrefix(key, repositoryURL) {
service.repoFileCache.Remove(key)
}
if key, ok := fileCacheKey.(string); ok && strings.HasPrefix(key, repositoryURL) {
service.repoFileCache.Remove(key)
}
}
}
if service.repoRefCache != nil {
// Lookup the refs cache first
cache, ok := service.repoRefCache.Get(refCacheKey)
if ok {
refs, success := cache.([]string)
if success {
if cache, ok := service.repoRefCache.Get(refCacheKey); ok {
if refs, ok := cache.([]string); ok {
return refs, nil
}
}
@@ -201,33 +199,35 @@ func (service *Service) ListRefs(repositoryURL, username, password string, hardR
tlsSkipVerify: tlsSkipVerify,
}
var (
refs []string
err error
)
if isAzureUrl(options.repositoryUrl) {
refs, err = service.azure.listRefs(context.TODO(), options)
if err != nil {
return nil, err
}
} else {
refs, err = service.git.listRefs(context.TODO(), options)
if err != nil {
return nil, err
}
refs, err := service.repoManager(options).listRefs(context.TODO(), options)
if err != nil {
return nil, err
}
if service.cacheEnabled && service.repoRefCache != nil {
service.repoRefCache.Add(refCacheKey, refs)
}
return refs, nil
}
var singleflightGroup = &singleflight.Group{}
// 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, dirOnly, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
fs, err, _ := singleflightGroup.Do(repoKey, func() (any, error) {
return service.listFiles(repositoryURL, referenceName, username, password, dirOnly, hardRefresh, tlsSkipVerify)
})
return filterFiles(fs.([]string), includedExts), err
}
func (service *Service) listFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
service.repoFileCache.Remove(repoKey)
@@ -235,14 +235,9 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
if service.repoFileCache != nil {
// lookup the files cache first
cache, ok := service.repoFileCache.Get(repoKey)
if ok {
files, success := cache.([]string)
if success {
// For the case while searching files in a repository without include extensions for the first time,
// but with include extensions for the second time
includedFiles := filterFiles(files, includedExts)
return includedFiles, nil
if cache, ok := service.repoFileCache.Get(repoKey); ok {
if files, ok := cache.([]string); ok {
return files, nil
}
}
}
@@ -258,28 +253,16 @@ func (service *Service) ListFiles(repositoryURL, referenceName, username, passwo
dirOnly: dirOnly,
}
var (
files []string
err error
)
if isAzureUrl(options.repositoryUrl) {
files, err = service.azure.listFiles(context.TODO(), options)
if err != nil {
return nil, err
}
} else {
files, err = service.git.listFiles(context.TODO(), options)
if err != nil {
return nil, err
}
files, err := service.repoManager(options.baseOption).listFiles(context.TODO(), options)
if err != nil {
return nil, err
}
includedFiles := filterFiles(files, includedExts)
if service.cacheEnabled && service.repoFileCache != nil {
service.repoFileCache.Add(repoKey, includedFiles)
return includedFiles, nil
service.repoFileCache.Add(repoKey, files)
}
return includedFiles, nil
return files, nil
}
func (service *Service) purgeCache() {
@@ -306,6 +289,7 @@ func matchExtensions(target string, exts []string) bool {
return true
}
}
return false
}
@@ -316,10 +300,11 @@ func filterFiles(paths []string, includedExts []string) []string {
var includedFiles []string
for _, filename := range paths {
// filter out the filenames with non-included extension
// Filter out the filenames with non-included extension
if matchExtensions(filename, includedExts) {
includedFiles = append(includedFiles, filename)
}
}
return includedFiles
}

View File

@@ -8,7 +8,7 @@ import (
)
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
if govalidator.IsNull(repoConfig.URL) || !govalidator.IsURL(repoConfig.URL) {
if len(repoConfig.URL) == 0 || !govalidator.IsURL(repoConfig.URL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
}
@@ -17,7 +17,7 @@ func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
}
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {
if auth != nil && govalidator.IsNull(auth.Password) && auth.GitCredentialID == 0 {
if auth != nil && len(auth.Password) == 0 && auth.GitCredentialID == 0 {
return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password or GitCredentialID must be specified when authentication is enabled")
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand"
"fmt"
"net/http"
"os"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@@ -13,6 +14,13 @@ import (
)
func WithProtect(handler http.Handler) (http.Handler, error) {
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
isDockerDesktopExtension := false
if val, ok := os.LookupEnv("DOCKER_EXTENSION"); ok && val == "1" {
isDockerDesktopExtension = true
}
handler = withSendCSRFToken(handler)
token := make([]byte, 32)
@@ -26,7 +34,7 @@ func WithProtect(handler http.Handler) (http.Handler, error) {
gorillacsrf.Secure(false),
)(handler)
return withSkipCSRF(handler), nil
return withSkipCSRF(handler, isDockerDesktopExtension), nil
}
func withSendCSRFToken(handler http.Handler) http.Handler {
@@ -45,9 +53,9 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
})
}
func withSkipCSRF(handler http.Handler) http.Handler {
func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
skip, err := security.ShouldSkipCSRFCheck(r)
skip, err := security.ShouldSkipCSRFCheck(r, isDockerDesktopExtension)
if err != nil {
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)

View File

@@ -12,7 +12,6 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -30,11 +29,11 @@ type authenticateResponse struct {
}
func (payload *authenticatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) {
if len(payload.Username) == 0 {
return errors.New("Invalid username")
}
if govalidator.IsNull(payload.Password) {
if len(payload.Password) == 0 {
return errors.New("Invalid password")
}
@@ -155,7 +154,6 @@ func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *p
security.AddAuthCookie(w, token, expirationTime)
return response.JSON(w, &authenticateResponse{JWT: token})
}
func (handler *Handler) syncUserTeamsWithLDAPGroups(user *portainer.User, settings *portainer.LDAPSettings) error {
@@ -180,20 +178,18 @@ func (handler *Handler) syncUserTeamsWithLDAPGroups(user *portainer.User, settin
}
for _, team := range teams {
if teamExists(team.Name, userGroups) {
if teamMembershipExists(team.ID, userMemberships) {
continue
}
if !teamExists(team.Name, userGroups) || teamMembershipExists(team.ID, userMemberships) {
continue
}
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: team.ID,
Role: portainer.TeamMember,
}
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: team.ID,
Role: portainer.TeamMember,
}
if err := handler.DataStore.TeamMembership().Create(membership); err != nil {
return err
}
if err := handler.DataStore.TeamMembership().Create(membership); err != nil {
return err
}
}

View File

@@ -9,7 +9,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
"github.com/rs/zerolog/log"
)
@@ -19,7 +18,7 @@ type oauthPayload struct {
}
func (payload *oauthPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Code) {
if len(payload.Code) == 0 {
return errors.New("Invalid OAuth authorization code")
}

View File

@@ -28,5 +28,7 @@ func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperro
security.RemoveAuthCookie(w)
handler.bouncer.RevokeJWT(tokenData.Token)
return response.Empty(w)
}

View File

@@ -108,13 +108,13 @@ type customTemplateFromFileContentPayload struct {
}
func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
if len(payload.Title) == 0 {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.Description) {
if len(payload.Description) == 0 {
return errors.New("Invalid custom template description")
}
if govalidator.IsNull(payload.FileContent) {
if len(payload.FileContent) == 0 {
return errors.New("Invalid file content")
}
if payload.Type != portainer.KubernetesStack && payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows {
@@ -132,7 +132,7 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
}
func isValidNote(note string) bool {
if govalidator.IsNull(note) {
if len(note) == 0 {
return true
}
match, _ := regexp.MatchString("<img", note)
@@ -226,19 +226,19 @@ type customTemplateFromGitRepositoryPayload struct {
}
func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
if len(payload.Title) == 0 {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.Description) {
if len(payload.Description) == 0 {
return errors.New("Invalid custom template description")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
if len(payload.ComposeFilePathInRepository) == 0 {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}

View File

@@ -64,11 +64,11 @@ type customTemplateUpdatePayload struct {
}
func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Title) {
if len(payload.Title) == 0 {
return errors.New("Invalid custom template title")
}
if govalidator.IsNull(payload.FileContent) && govalidator.IsNull(payload.RepositoryURL) {
if len(payload.FileContent) == 0 && len(payload.RepositoryURL) == 0 {
return errors.New("Either file content or git repository url need to be provided")
}
@@ -80,7 +80,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid custom template type")
}
if govalidator.IsNull(payload.Description) {
if len(payload.Description) == 0 {
return errors.New("Invalid custom template description")
}
@@ -88,11 +88,11 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid note. <img> tag is not supported")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
if len(payload.ComposeFilePathInRepository) == 0 {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}

View File

@@ -9,8 +9,6 @@ import (
"github.com/portainer/portainer/api/internal/endpointutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
)
type edgeGroupCreatePayload struct {
@@ -22,7 +20,7 @@ type edgeGroupCreatePayload struct {
}
func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("invalid Edge group name")
}

View File

@@ -12,8 +12,6 @@ import (
"github.com/portainer/portainer/api/slicesx"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
)
type edgeGroupUpdatePayload struct {
@@ -25,7 +23,7 @@ type edgeGroupUpdatePayload struct {
}
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("invalid Edge group name")
}

View File

@@ -49,7 +49,7 @@ type edgeJobCreateFromFileContentPayload struct {
}
func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("invalid Edge job name")
}
@@ -57,7 +57,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
}
if govalidator.IsNull(payload.CronExpression) {
if len(payload.CronExpression) == 0 {
return errors.New("invalid cron expression")
}
@@ -65,7 +65,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
return errors.New("no environments or groups have been provided")
}
if govalidator.IsNull(payload.FileContent) {
if len(payload.FileContent) == 0 {
return errors.New("invalid script file content")
}

View File

@@ -46,15 +46,15 @@ type edgeStackFromGitRepositoryPayload struct {
}
func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return httperrors.NewInvalidPayloadError("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {
return httperrors.NewInvalidPayloadError("Invalid repository credentials. Password must be specified when authentication is enabled")
}
@@ -62,7 +62,7 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
return httperrors.NewInvalidPayloadError("Invalid deployment type")
}
if govalidator.IsNull(payload.FilePathInRepository) {
if len(payload.FilePathInRepository) == 0 {
switch payload.DeploymentType {
case portainer.EdgeStackDeploymentCompose:
payload.FilePathInRepository = filesystem.ComposeFileDefaultName

View File

@@ -10,7 +10,6 @@ import (
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
)
@@ -33,11 +32,11 @@ type edgeStackFromStringPayload struct {
}
func (payload *edgeStackFromStringPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return httperrors.NewInvalidPayloadError("Invalid stack name")
}
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return httperrors.NewInvalidPayloadError("Invalid stack file content")
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
"github.com/rs/zerolog/log"
)
@@ -31,7 +30,7 @@ func (payload *updateStatusPayload) Validate(r *http.Request) error {
return errors.New("invalid EnvironmentID")
}
if *payload.Status == portainer.EdgeStackStatusError && govalidator.IsNull(payload.Error) {
if *payload.Status == portainer.EdgeStackStatusError && len(payload.Error) == 0 {
return errors.New("error message is mandatory when status is error")
}
@@ -88,7 +87,7 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *ht
if err != nil {
if dataservices.IsErrObjectNotFound(err) {
// skip error because agent tries to report on deleted stack
log.Warn().
log.Debug().
Err(err).
Int("stackID", int(stackID)).
Int("status", int(*payload.Status)).

View File

@@ -2,6 +2,7 @@ package endpointedge
import (
"bytes"
"cmp"
"encoding/base64"
"errors"
"fmt"
@@ -78,8 +79,7 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
return httperror.BadRequest("Invalid environment identifier route variable", err)
}
cachedResp := handler.respondFromCache(w, r, portainer.EndpointID(endpointID))
if cachedResp {
if cachedResp := handler.respondFromCache(w, r, portainer.EndpointID(endpointID)); cachedResp {
return nil
}
@@ -96,24 +96,21 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
firstConn := endpoint.LastCheckInDate == 0
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
}
handler.DataStore.Endpoint().UpdateHeartbeat(endpoint.ID)
err = handler.requestBouncer.TrustedEdgeEnvironmentAccess(handler.DataStore, endpoint)
if err != nil {
if err := handler.requestBouncer.TrustedEdgeEnvironmentAccess(handler.DataStore, endpoint); err != nil {
return httperror.Forbidden("Permission denied to access environment", err)
}
var statusResponse *endpointEdgeStatusInspectResponse
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
statusResponse, err = handler.inspectStatus(tx, r, portainer.EndpointID(endpointID), firstConn)
return err
})
if err != nil {
}); err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
@@ -125,15 +122,29 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
return cacheResponse(w, endpoint.ID, *statusResponse)
}
func (handler *Handler) parseHeaders(r *http.Request, endpoint *portainer.Endpoint) error {
endpoint.EdgeID = cmp.Or(endpoint.EdgeID, r.Header.Get(portainer.PortainerAgentEdgeIDHeader))
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", agentPlatformErr)
}
endpoint.Type = agentPlatform
version := r.Header.Get(portainer.PortainerAgentHeader)
endpoint.Agent.Version = version
return nil
}
func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Request, endpointID portainer.EndpointID, firstConn bool) (*endpointEdgeStatusInspectResponse, error) {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
return nil, err
}
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
if err := handler.parseHeaders(r, endpoint); err != nil {
return nil, err
}
// Take an initial snapshot
@@ -143,19 +154,9 @@ func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Reque
}
}
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return nil, httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
version := r.Header.Get(portainer.PortainerAgentHeader)
endpoint.Agent.Version = version
endpoint.LastCheckInDate = time.Now().Unix()
err = tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
return nil, httperror.InternalServerError("Unable to persist environment changes inside the database", err)
}
@@ -262,9 +263,8 @@ func (handler *Handler) buildEdgeStacks(tx dataservices.DataStoreTx, endpointID
func cacheResponse(w http.ResponseWriter, endpointID portainer.EndpointID, statusResponse endpointEdgeStatusInspectResponse) *httperror.HandlerError {
rr := httptest.NewRecorder()
httpErr := response.JSON(rr, statusResponse)
if httpErr != nil {
return httpErr
if err := response.JSON(rr, statusResponse); err != nil {
return err
}
h := fnv.New32a()

View File

@@ -152,7 +152,7 @@ func TestMissingEdgeIdentifier(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
t.Fatalf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code)
}
}
@@ -177,7 +177,7 @@ func TestWithEndpoints(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != test.expectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
t.Fatalf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID)
}
}
}
@@ -217,7 +217,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
@@ -260,7 +260,7 @@ func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code)
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
@@ -326,7 +326,7 @@ func TestEdgeStackStatus(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
var data endpointEdgeStatusInspectResponse
@@ -390,7 +390,7 @@ func TestEdgeJobsResponse(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
var data endpointEdgeStatusInspectResponse

View File

@@ -9,8 +9,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type endpointGroupCreatePayload struct {
@@ -25,7 +23,7 @@ type endpointGroupCreatePayload struct {
}
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("invalid environment group name")
}

View File

@@ -98,8 +98,8 @@ func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpoin
payloadTagSet := tag.Set(payload.TagIDs)
endpointGroupTagSet := tag.Set((endpointGroup.TagIDs))
union := tag.Union(payloadTagSet, endpointGroupTagSet)
intersection := tag.Intersection(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > len(intersection)
intersection := tag.IntersectionCount(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > intersection
if tagsChanged {
removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet)

View File

@@ -40,6 +40,7 @@ type endpointCreatePayload struct {
AzureAuthenticationKey string
TagIDs []portainer.TagID
EdgeCheckinInterval int
ContainerEngine string
}
type endpointCreationEnum int
@@ -66,6 +67,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
}
payload.EndpointCreationType = endpointCreationEnum(endpointCreationType)
payload.ContainerEngine, err = request.RetrieveMultiPartFormValue(r, "ContainerEngine", true)
if err != nil || (payload.ContainerEngine != "" && payload.ContainerEngine != portainer.ContainerEngineDocker && payload.ContainerEngine != portainer.ContainerEnginePodman) {
return errors.New("invalid container engine value. Value must be one of: 'docker' or 'podman'")
}
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
if groupID == 0 {
groupID = 1
@@ -186,6 +192,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
// @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 ContainerEngine formData string false "Container engine used by the environment(endpoint). Value must be one of: 'docker' or 'podman'"
// @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)."
@@ -371,12 +378,13 @@ func (handler *Handler) createEdgeAgentEndpoint(tx dataservices.DataStoreTx, pay
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
ContainerEngine: payload.ContainerEngine,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -424,13 +432,14 @@ func (handler *Handler) createUnsecuredEndpoint(tx dataservices.DataStoreTx, pay
endpointID := tx.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
ContainerEngine: payload.ContainerEngine,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -486,13 +495,14 @@ func (handler *Handler) createKubernetesEndpoint(tx dataservices.DataStoreTx, pa
func (handler *Handler) createTLSSecuredEndpoint(tx dataservices.DataStoreTx, payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID := tx.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: payload.URL,
Type: endpointType,
ContainerEngine: payload.ContainerEngine,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,

View File

@@ -144,19 +144,19 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
if err := tx.Snapshot().Delete(endpointID); err != nil {
log.Warn().Err(err).Msgf("Unable to remove the snapshot from the database")
log.Warn().Err(err).Msg("Unable to remove the snapshot from the database")
}
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
if err := handler.AuthorizationService.UpdateUsersAuthorizationsTx(tx); err != nil {
log.Warn().Err(err).Msgf("Unable to update user authorizations")
log.Warn().Err(err).Msg("Unable to update user authorizations")
}
}
if err := tx.EndpointRelation().DeleteEndpointRelation(endpoint.ID); err != nil {
log.Warn().Err(err).Msgf("Unable to remove environment relation from the database")
log.Warn().Err(err).Msg("Unable to remove environment relation from the database")
}
for _, tagID := range endpoint.TagIDs {
@@ -168,9 +168,9 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
if handler.DataStore.IsErrObjectNotFound(err) {
log.Warn().Err(err).Msgf("Unable to find tag inside the database")
log.Warn().Err(err).Msg("Unable to find tag inside the database")
} else if err != nil {
log.Warn().Err(err).Msgf("Unable to delete tag relation from the database")
log.Warn().Err(err).Msg("Unable to delete tag relation from the database")
}
}
@@ -185,38 +185,38 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
})
if err := tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup); err != nil {
log.Warn().Err(err).Msgf("Unable to update edge group")
log.Warn().Err(err).Msg("Unable to update edge group")
}
}
edgeStacks, err := tx.EdgeStack().EdgeStacks()
if err != nil {
log.Warn().Err(err).Msgf("Unable to retrieve edge stacks from the database")
log.Warn().Err(err).Msg("Unable to retrieve edge stacks from the database")
}
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
err = tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update edge stack")
if err := tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack); err != nil {
log.Warn().Err(err).Msg("Unable to update edge stack")
}
}
}
registries, err := tx.Registry().ReadAll()
if err != nil {
log.Warn().Err(err).Msgf("Unable to retrieve registries from the database")
log.Warn().Err(err).Msg("Unable to retrieve registries from the database")
}
for idx := range registries {
registry := &registries[idx]
if _, ok := registry.RegistryAccesses[endpoint.ID]; ok {
delete(registry.RegistryAccesses, endpoint.ID)
err = tx.Registry().Update(registry.ID, registry)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update registry accesses")
if err := tx.Registry().Update(registry.ID, registry); err != nil {
log.Warn().Err(err).Msg("Unable to update registry accesses")
}
}
}
@@ -224,7 +224,7 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
if endpointutils.IsEdgeEndpoint(endpoint) {
edgeJobs, err := handler.DataStore.EdgeJob().ReadAll()
if err != nil {
log.Warn().Err(err).Msgf("Unable to retrieve edge jobs from the database")
log.Warn().Err(err).Msg("Unable to retrieve edge jobs from the database")
}
for idx := range edgeJobs {
@@ -232,9 +232,8 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
if _, ok := edgeJob.Endpoints[endpoint.ID]; ok {
delete(edgeJob.Endpoints, endpoint.ID)
err = tx.EdgeJob().Update(edgeJob.ID, edgeJob)
if err != nil {
log.Warn().Err(err).Msgf("Unable to update edge job")
if err := tx.EdgeJob().Update(edgeJob.ID, edgeJob); err != nil {
log.Warn().Err(err).Msg("Unable to update edge job")
}
}
}
@@ -242,7 +241,7 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
// delete the pending actions
if err := tx.PendingActions().DeleteByEndpointID(endpoint.ID); err != nil {
log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msgf("Unable to delete pending actions")
log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msg("Unable to delete pending actions")
}
if err := tx.Endpoint().DeleteEndpoint(endpointID); err != nil {

View File

@@ -3,15 +3,16 @@ package endpoints
import (
"net/http"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/pkg/errors"
)
// @id endpointRegistriesList
@@ -123,11 +124,11 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name
return true, nil
}
if namespace == "default" {
if !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace && namespace == kubernetes.DefaultNamespace {
return true, nil
}
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return false, errors.Wrap(err, "unable to retrieve kubernetes client")
}
@@ -186,7 +187,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, re
}
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint)
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
}

View File

@@ -134,7 +134,7 @@ func (handler *Handler) updateKubeAccess(endpoint *portainer.Endpoint, registry
namespacesToRemove := setDifference(oldNamespacesSet, newNamespacesSet)
namespacesToAdd := setDifference(newNamespacesSet, oldNamespacesSet)
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View File

@@ -193,7 +193,7 @@ func (handler *Handler) filterEndpointsByQuery(
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
tagsMap := make(map[portainer.TagID]string, len(tags))
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
@@ -304,8 +304,7 @@ func filterEndpointsBySearchCriteria(
) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
if endpointMatchSearchCriteria(&endpoint, tagsMap, searchCriteria) {
endpoints[n] = endpoint
n++
@@ -319,7 +318,7 @@ func filterEndpointsBySearchCriteria(
continue
}
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpoints, endpointGroups) {
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpointGroups) {
endpoints[n] = endpoint
n++
@@ -365,7 +364,7 @@ func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portai
return endpoints[:n]
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
@@ -380,8 +379,8 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
for _, tagID := range endpoint.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
@@ -391,16 +390,17 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
if group.ID != endpoint.GroupID {
continue
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tagID := range group.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
}
@@ -413,11 +413,10 @@ func edgeGroupMatchSearchCriteria(
endpoint *portainer.Endpoint,
edgeGroups []portainer.EdgeGroup,
searchCriteria string,
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
) bool {
for _, edgeGroup := range edgeGroups {
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, endpoints, endpointGroups)
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, []portainer.Endpoint{*endpoint}, endpointGroups)
for _, endpointID := range relatedEndpointIDs {
if endpointID == endpoint.ID {
@@ -448,16 +447,6 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []port
return endpoints[:n]
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0, len(tagIDs))
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {

View File

@@ -1,6 +1,7 @@
package endpoints
import (
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -148,6 +149,103 @@ func Test_Filter_excludeIDs(t *testing.T) {
runTests(tests, t, handler, environments)
}
func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := range n {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := range 1000 {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1, 2, 3},
PartialMatch: true,
})
}
tagsMap := map[portainer.TagID]string{}
for i := range 10 {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for range b.N {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := range n {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1, 2, 3},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := range 1000 {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1},
})
}
tagsMap := map[portainer.TagID]string{}
for i := range 10 {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for range b.N {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {

View File

@@ -29,15 +29,15 @@ type repositoryFilePreviewPayload struct {
}
func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Repository) || !govalidator.IsURL(payload.Repository) {
if len(payload.Repository) == 0 || !govalidator.IsURL(payload.Repository) {
return errors.New("invalid repository URL. Must correspond to a valid URL format")
}
if govalidator.IsNull(payload.Reference) {
if len(payload.Reference) == 0 {
payload.Reference = "refs/heads/main"
}
if govalidator.IsNull(payload.TargetFile) {
if len(payload.TargetFile) == 0 {
return errors.New("invalid target filename")
}

View File

@@ -0,0 +1,150 @@
package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id GetApplicationsResources
// @summary Get the total resource requests and limits of all applications
// @description Get the total CPU (cores) and memory requests (MB) and limits of all applications across all namespaces.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param node query string true "Node name"
// @success 200 {object} models.K8sApplicationResource "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the total resource requests and limits for all applications from the cluster."
// @router /kubernetes/{id}/metrics/applications_resources [get]
func (handler *Handler) getApplicationsResources(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
node, err := request.RetrieveQueryParameter(r, "node", true)
if err != nil {
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to parse the namespace query parameter")
return httperror.BadRequest("Unable to parse the node query parameter", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getApplicationsResources").Msg("Unable to prepare kube client")
return httperror.InternalServerError("Unable to prepare kube client", httpErr)
}
applicationsResources, err := cli.GetApplicationsResource("", node)
if err != nil {
if k8serrors.IsUnauthorized(err) {
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to get the total resource requests and limits for all applications in the namespace")
return httperror.Unauthorized("Unable to get the total resource requests and limits for all applications in the namespace", err)
}
if k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to get the total resource requests and limits for all applications in the namespace")
return httperror.Forbidden("Unable to get the total resource requests and limits for all applications in the namespace", err)
}
log.Error().Err(err).Str("context", "getApplicationsResources").Msg("Unable to calculate the total resource requests and limits for all applications in the namespace")
return httperror.InternalServerError("Unable to calculate the total resource requests and limits for all applications in the namespace", err)
}
return response.JSON(w, applicationsResources)
}
// @id GetAllKubernetesApplications
// @summary Get a list of applications across all namespaces in the cluster. If the nodeName is provided, it will return the applications running on that node.
// @description Get a list of applications across all namespaces in the cluster. If the nodeName is provided, it will return the applications running on that node.
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param namespace query string true "Namespace name"
// @param nodeName query string true "Node name"
// @param withDependencies query boolean false "Include dependencies in the response"
// @success 200 {array} models.K8sApplication "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of applications from the cluster."
// @router /kubernetes/{id}/applications [get]
func (handler *Handler) GetAllKubernetesApplications(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
applications, err := handler.getAllKubernetesApplications(r)
if err != nil {
return err
}
return response.JSON(w, applications)
}
// @id GetAllKubernetesApplicationsCount
// @summary Get Applications count
// @description Get the count of Applications across all namespaces in the cluster. If the nodeName is provided, it will return the count of applications running on that node.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the count of all applications from the cluster."
// @router /kubernetes/{id}/applications/count [get]
func (handler *Handler) getAllKubernetesApplicationsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
applications, err := handler.getAllKubernetesApplications(r)
if err != nil {
return err
}
return response.JSON(w, len(applications))
}
func (handler *Handler) getAllKubernetesApplications(r *http.Request) ([]models.K8sApplication, *httperror.HandlerError) {
namespace, err := request.RetrieveQueryParameter(r, "namespace", true)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the namespace query parameter")
return nil, httperror.BadRequest("Unable to parse the namespace query parameter", err)
}
withDependencies, err := request.RetrieveBooleanQueryParameter(r, "withDependencies", true)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the withDependencies query parameter")
return nil, httperror.BadRequest("Unable to parse the withDependencies query parameter", err)
}
nodeName, err := request.RetrieveQueryParameter(r, "nodeName", true)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Msg("Unable to parse the nodeName query parameter")
return nil, httperror.BadRequest("Unable to parse the nodeName query parameter", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get a Kubernetes client for the user")
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
}
applications, err := cli.GetApplications(namespace, nodeName, withDependencies)
if err != nil {
if k8serrors.IsUnauthorized(err) {
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get the list of applications")
return nil, httperror.Unauthorized("Unable to get the list of applications", err)
}
log.Error().Err(err).Str("context", "getAllKubernetesApplications").Str("namespace", namespace).Str("nodeName", nodeName).Msg("Unable to get the list of applications")
return nil, httperror.InternalServerError("Unable to get the list of applications", err)
}
return applications, nil
}

View File

@@ -0,0 +1,37 @@
package kubernetes
import (
"net/http"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/kubernetes/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/rs/zerolog/log"
)
// prepareKubeClient is a helper function to prepare a Kubernetes client for the user
// it first fetches getProxyKubeClient to grab the user's admin status and non admin namespaces
// then these two values are parsed to create a privileged client
func (handler *Handler) prepareKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr.Err).Str("context", "prepareKubeClient").Msg("Unable to get a Kubernetes client for the user.")
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user.", httpErr)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to find the Kubernetes endpoint associated to the request.")
return nil, httperror.NotFound("Unable to find the Kubernetes endpoint associated to the request.", err)
}
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to get a privileged Kubernetes client for the user.")
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
}
pcli.IsKubeAdmin = cli.IsKubeAdmin
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
return pcli, nil
}

View File

@@ -0,0 +1,45 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetAllKubernetesClusterRoleBindings
// @summary Get a list of kubernetes cluster role bindings
// @description Get a list of kubernetes cluster role bindings within the given environment at the cluster level.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {array} kubernetes.K8sClusterRoleBinding "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of cluster role bindings."
// @router /kubernetes/{id}/clusterrolebindings [get]
func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr.Err).Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", httpErr)
}
if !cli.IsKubeAdmin {
log.Error().Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
}
clusterrolebindings, err := cli.GetClusterRoleBindings()
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesClusterRoleBindings").Msg("Unable to fetch cluster role bindings.")
return httperror.InternalServerError("Unable to fetch cluster role bindings.", err)
}
return response.JSON(w, clusterrolebindings)
}

View File

@@ -0,0 +1,45 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetAllKubernetesClusterRoles
// @summary Get a list of kubernetes cluster roles
// @description Get a list of kubernetes cluster roles within the given environment at the cluster level.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {array} kubernetes.K8sClusterRole "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of cluster roles."
// @router /kubernetes/{id}/clusterroles [get]
func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr.Err).Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", httpErr)
}
if !cli.IsKubeAdmin {
log.Error().Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", nil)
}
clusterroles, err := cli.GetClusterRoles()
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesClusterRoles").Msg("Unable to fetch clusterroles.")
return httperror.InternalServerError("Unable to fetch clusterroles.", err)
}
return response.JSON(w, clusterroles)
}

View File

@@ -12,45 +12,48 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
)
// @id GetKubernetesConfig
// @summary Generate a kubeconfig file enabling client communication with k8s api server
// @description Generate a kubeconfig file enabling client communication with k8s api server
// @description **Access policy**: authenticated
// @summary Generate a kubeconfig file
// @description Generate a kubeconfig file that allows a client to communicate with the Kubernetes API server
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @security ApiKeyAuth || jwt
// @produce application/json, application/yaml
// @param ids query []int false "will include only these environments(endpoints)"
// @param excludeIds query []int false "will exclude these environments(endpoints)"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
// @failure 500 "Server error"
// @success 200 {object} interface{} "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to generate the kubeconfig file."
// @router /kubernetes/config [get]
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Permission denied to access environment")
return httperror.Forbidden("Permission denied to access environment", err)
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Unable to generate JWT token")
return httperror.InternalServerError("Unable to generate JWT token", err)
}
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
if handlerErr != nil {
log.Error().Err(handlerErr).Str("context", "getKubernetesConfig").Msg("Unable to filter user kube endpoints")
return handlerErr
}
if len(endpoints) == 0 {
log.Error().Str("context", "getKubernetesConfig").Msg("Empty endpoints list")
return httperror.BadRequest("empty endpoints list", errors.New("empty endpoints list"))
}
@@ -67,16 +70,19 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
_ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true)
if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 {
log.Error().Str("context", "filterUserKubeEndpoints").Msg("Can't provide both 'ids' and 'excludeIds' parameters")
return nil, httperror.BadRequest("Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters"))
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve info from request context")
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
}
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
if err != nil {
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment groups from the database")
return nil, httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
@@ -85,6 +91,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
for _, endpointID := range endpointIDs {
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
if err != nil {
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment from the database")
return nil, httperror.InternalServerError("Unable to retrieve environment from the database", err)
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
@@ -101,6 +108,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En
var kubeEndpoints []portainer.Endpoint
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environments from the database")
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
}
@@ -197,6 +205,7 @@ func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portai
if r.Header.Get("Accept") == "text/yaml" {
yaml, err := kcli.GenerateYAML(config)
if err != nil {
log.Error().Err(err).Str("context", "writeFileContent").Msg("Failed to generate Kubeconfig")
return httperror.InternalServerError("Failed to generate Kubeconfig", err)
}

View File

@@ -0,0 +1,159 @@
package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id GetKubernetesConfigMap
// @summary Get a ConfigMap
// @description Get a ConfigMap by name for a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "The namespace name where the configmap is located"
// @param configmap path string true "The configmap name to get details for"
// @success 200 {object} models.K8sConfigMap "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or a configmap with the specified name in the given namespace."
// @failure 500 "Server error occurred while attempting to retrieve a configmap by name within the specified namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/configmaps/{configmap} [get]
func (handler *Handler) getKubernetesConfigMap(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("Unable to retrieve namespace identifier route variable", err)
}
configMapName, err := request.RetrieveRouteVariableValue(r, "configmap")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Msg("Unable to retrieve configMap identifier route variable")
return httperror.BadRequest("Unable to retrieve configMap identifier route variable", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
}
configMap, err := cli.GetConfigMap(namespace, configMapName)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to retrieve configMap")
return httperror.NotFound("Unable to retrieve configMap", err)
}
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to retrieve configMap")
return httperror.InternalServerError("Unable to retrieve configMap", err)
}
configMapWithApplications, err := cli.CombineConfigMapWithApplications(configMap)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesConfigMap").Str("namespace", namespace).Str("configMap", configMapName).Msg("Unable to combine configMap with applications")
return httperror.InternalServerError("Unable to combine configMap with applications", err)
}
return response.JSON(w, configMapWithApplications)
}
// @id GetAllKubernetesConfigMaps
// @summary Get a list of ConfigMaps
// @description Get a list of ConfigMaps across all namespaces in the cluster. For non-admin users, it will only return ConfigMaps based on the namespaces that they have access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param isUsed query bool true "Set to true to include information about applications that use the ConfigMaps in the response"
// @success 200 {array} models.K8sConfigMap "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve all configmaps from the cluster."
// @router /kubernetes/{id}/configmaps [get]
func (handler *Handler) GetAllKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
configMaps, err := handler.getAllKubernetesConfigMaps(r)
if err != nil {
return err
}
return response.JSON(w, configMaps)
}
// @id GetAllKubernetesConfigMapsCount
// @summary Get ConfigMaps count
// @description Get the count of ConfigMaps across all namespaces in the cluster. For non-admin users, it will only return the count of ConfigMaps based on the namespaces that they have access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the count of all configmaps from the cluster."
// @router /kubernetes/{id}/configmaps/count [get]
func (handler *Handler) getAllKubernetesConfigMapsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
configMaps, err := handler.getAllKubernetesConfigMaps(r)
if err != nil {
return err
}
return response.JSON(w, len(configMaps))
}
func (handler *Handler) getAllKubernetesConfigMaps(r *http.Request) ([]models.K8sConfigMap, *httperror.HandlerError) {
isUsed, err := request.RetrieveBooleanQueryParameter(r, "isUsed", true)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to retrieve isUsed query parameter")
return nil, httperror.BadRequest("Unable to retrieve isUsed query parameter", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to prepare kube client")
return nil, httperror.InternalServerError("Unable to prepare kube client", httpErr)
}
configMaps, err := cli.GetConfigMaps("")
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unauthorized access to the Kubernetes API")
return nil, httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to get configMaps")
return nil, httperror.InternalServerError("Unable to get configMaps", err)
}
if isUsed {
configMapsWithApplications, err := cli.CombineConfigMapsWithApplications(configMaps)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to combine configMaps with associated applications")
return nil, httperror.InternalServerError("Unable to combine configMaps with associated applications", err)
}
return configMapsWithApplications, nil
}
return configMaps, nil
}

View File

@@ -1,44 +0,0 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id getKubernetesConfigMapsAndSecrets
// @summary Get ConfigMaps and Secrets
// @description Get all ConfigMaps and Secrets for a given namespace
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @success 200 {array} []kubernetes.K8sConfigMapOrSecret "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @deprecated
// @router /kubernetes/{id}/namespaces/{namespace}/configuration [get]
func (handler *Handler) getKubernetesConfigMapsAndSecrets(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
configmaps, err := cli.GetConfigMapsAndSecrets(namespace)
if err != nil {
return httperror.InternalServerError("Unable to retrieve configmaps and secrets", err)
}
return response.JSON(w, configmaps)
}

View File

@@ -9,11 +9,10 @@ import (
// @id GetKubernetesDashboard
// @summary Get the dashboard summary data
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources
// @description **Access policy**: authenticated
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"

View File

@@ -1,7 +1,7 @@
package kubernetes
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
@@ -11,11 +11,11 @@ import (
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/rs/zerolog/log"
"github.com/gorilla/mux"
)
@@ -49,94 +49,98 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
// endpoints
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(kubeOnlyMiddleware)
endpointRouter.Use(h.kubeClientMiddleware)
endpointRouter.Handle("/applications", httperror.LoggerHandler(h.GetAllKubernetesApplications)).Methods(http.MethodGet)
endpointRouter.Handle("/applications/count", httperror.LoggerHandler(h.getAllKubernetesApplicationsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps", httperror.LoggerHandler(h.GetAllKubernetesConfigMaps)).Methods(http.MethodGet)
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.getKubernetesDashboard)).Methods(http.MethodGet)
endpointRouter.Handle("/nodes_limits", httperror.LoggerHandler(h.getKubernetesNodesLimits)).Methods(http.MethodGet)
endpointRouter.Handle("/max_resource_limits", httperror.LoggerHandler(h.getKubernetesMaxResourceLimits)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/applications_resources", httperror.LoggerHandler(h.getApplicationsResources)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/nodes", httperror.LoggerHandler(h.getKubernetesMetricsForAllNodes)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/nodes/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForNode)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/pods/namespace/{namespace}", httperror.LoggerHandler(h.getKubernetesMetricsForAllPods)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/pods/namespace/{namespace}/{name}", httperror.LoggerHandler(h.getKubernetesMetricsForPod)).Methods(http.MethodGet)
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllers)).Methods(http.MethodGet)
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getAllKubernetesIngressControllers)).Methods(http.MethodGet)
endpointRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllers)).Methods(http.MethodPut)
endpointRouter.Handle("/ingresses/delete", httperror.LoggerHandler(h.deleteKubernetesIngresses)).Methods(http.MethodPost)
endpointRouter.Handle("/ingresses", httperror.LoggerHandler(h.GetAllKubernetesClusterIngresses)).Methods(http.MethodGet)
endpointRouter.Handle("/ingresses/count", httperror.LoggerHandler(h.getAllKubernetesClusterIngressesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
endpointRouter.Handle("/services", httperror.LoggerHandler(h.GetAllKubernetesServices)).Methods(http.MethodGet)
endpointRouter.Handle("/services/count", httperror.LoggerHandler(h.getAllKubernetesServicesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/secrets", httperror.LoggerHandler(h.GetAllKubernetesSecrets)).Methods(http.MethodGet)
endpointRouter.Handle("/secrets/count", httperror.LoggerHandler(h.getAllKubernetesSecretsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/services/delete", httperror.LoggerHandler(h.deleteKubernetesServices)).Methods(http.MethodPost)
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.isRBACEnabled)).Methods(http.MethodGet)
endpointRouter.Handle("/rbac_enabled", httperror.LoggerHandler(h.getKubernetesRBACStatus)).Methods(http.MethodGet)
endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet)
endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.createKubernetesNamespace)).Methods(http.MethodPost)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.updateKubernetesNamespace)).Methods(http.MethodPut)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
endpointRouter.Handle("/namespaces", httperror.LoggerHandler(h.getKubernetesNamespaces)).Methods(http.MethodGet)
endpointRouter.Handle("/namespace/{namespace}", httperror.LoggerHandler(h.deleteKubernetesNamespace)).Methods(http.MethodDelete)
endpointRouter.Handle("/namespaces/count", httperror.LoggerHandler(h.getKubernetesNamespacesCount)).Methods(http.MethodGet)
endpointRouter.Handle("/namespaces/{namespace}", httperror.LoggerHandler(h.getKubernetesNamespace)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes", httperror.LoggerHandler(h.GetAllKubernetesVolumes)).Methods(http.MethodGet)
endpointRouter.Handle("/volumes/count", httperror.LoggerHandler(h.getAllKubernetesVolumesCount)).Methods(http.MethodGet)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
// to keep it simple, we've decided to leave it like this.
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
namespaceRouter.Handle("/configuration", httperror.LoggerHandler(h.getKubernetesConfigMapsAndSecrets)).Methods(http.MethodGet)
namespaceRouter.Handle("/ingresses/{ingress}", httperror.LoggerHandler(h.getKubernetesIngress)).Methods(http.MethodGet)
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.createKubernetesIngress)).Methods(http.MethodPost)
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.updateKubernetesIngress)).Methods(http.MethodPut)
namespaceRouter.Handle("/ingresses", httperror.LoggerHandler(h.getKubernetesIngresses)).Methods(http.MethodGet)
namespaceRouter.Handle("/secrets/{secret}", httperror.LoggerHandler(h.getKubernetesSecret)).Methods(http.MethodGet)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.createKubernetesService)).Methods(http.MethodPost)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.updateKubernetesService)).Methods(http.MethodPut)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServices)).Methods(http.MethodGet)
namespaceRouter.Handle("/services", httperror.LoggerHandler(h.getKubernetesServicesByNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/volumes/{volume}", httperror.LoggerHandler(h.getKubernetesVolume)).Methods(http.MethodGet)
return h
}
func kubeOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.InternalServerError(
"Unable to find an environment on request context",
err,
)
return
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
errMessage := "environment is not a Kubernetes environment"
httperror.BadRequest(
errMessage,
errors.New(errMessage),
)
return
}
rw.Header().Set(portainer.PortainerCacheHeader, "true")
next.ServeHTTP(rw, request)
})
}
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
// admin permissions. If you're unsure which one to use, use this.
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return nil, httperror.BadRequest("Invalid environment identifier route variable", err)
return nil, httperror.BadRequest(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return nil, httperror.Forbidden("Permission denied to access environment", err)
return nil, httperror.Forbidden(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, permission denied to access the environment /api/kubernetes/%d. Error: ", endpointID), err)
}
cli, ok := h.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), tokenData.Token)
if !ok {
return nil, httperror.InternalServerError("Failed to lookup KubeClient", nil)
return nil, httperror.InternalServerError("an error occurred during the getProxyKubeClient operation,failed to get proxy KubeClient", nil)
}
return cli, nil
}
// kubeClientMiddleware is a middleware that will create a kubeclient for the user if it doesn't exist
// and store it in the factory for future use.
// if there is a kubeclient against this auth token already, the existing one will be reused.
// otherwise, generate a new one
func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(portainer.PortainerCacheHeader, "true")
if handler.KubernetesClientFactory == nil {
next.ServeHTTP(w, r)
return
@@ -144,13 +148,13 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
httperror.WriteError(w, http.StatusBadRequest, "Invalid environment identifier route variable", err)
httperror.WriteError(w, http.StatusBadRequest, fmt.Sprintf("an error occurred during the KubeClientMiddleware operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteError(w, http.StatusForbidden, "Permission denied to access environment", err)
httperror.WriteError(w, http.StatusForbidden, "an error occurred during the KubeClientMiddleware operation, permission denied to access the environment. Error: ", err)
}
// Check if we have a kubeclient against this auth token already, otherwise generate a new one
@@ -163,35 +167,60 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
httperror.WriteError(
w,
http.StatusNotFound,
"Unable to find an environment with the specified identifier inside the database",
err,
)
httperror.WriteError(w, http.StatusNotFound,
"an error occurred during the KubeClientMiddleware operation, unable to find an environment with the specified environment identifier inside the database. Error: ", err)
return
}
httperror.WriteError(w, http.StatusInternalServerError, "Unable to read the environment from the database", err)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, error reading from the Portainer database. Error: ", err)
return
}
user, err := security.RetrieveUserFromRequest(r, handler.DataStore)
if err != nil {
httperror.InternalServerError("an error occurred during the KubeClientMiddleware operation, unable to retrieve the user from request. Error: ", err)
return
}
log.
Debug().
Str("context", "KubeClientMiddleware").
Str("endpoint", endpoint.Name).
Str("user", user.Username).
Msg("Creating a Kubernetes client")
isKubeAdmin := true
nonAdminNamespaces := []string{}
if user.Role != portainer.AdministratorRole {
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to get privileged kube client to grab all namespaces. Error: ", err)
return
}
nonAdminNamespaces, err = pcli.GetNonAdminNamespaces(int(user.ID), endpoint.Kubernetes.Configuration.RestrictDefaultNamespace)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to retrieve non-admin namespaces. Error: ", err)
return
}
isKubeAdmin = false
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to create JWT token", err)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to generate token for kubeconfig. Error: ", err)
return
}
config := handler.buildConfig(r, tokenData, bearerToken, []portainer.Endpoint{*endpoint}, true)
if len(config.Clusters) == 0 {
httperror.WriteError(w, http.StatusInternalServerError, "Unable build cluster kubeconfig", nil)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to build kubeconfig. Error: ", nil)
return
}
// Manually setting serverURL to localhost to route the request to proxy server
serverURL, err := url.Parse(config.Clusters[0].Cluster.Server)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable parse cluster's kubeconfig server URL", nil)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to parse server URL for building kubeconfig. Error: ", err)
return
}
serverURL.Scheme = "https"
@@ -200,17 +229,12 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
yaml, err := cli.GenerateYAML(config)
if err != nil {
httperror.WriteError(
w,
http.StatusInternalServerError,
"Unable to generate yaml from endpoint kubeconfig",
err,
)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to generate kubeconfig YAML. Error: ", err)
return
}
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml))
kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml), isKubeAdmin, nonAdminNamespaces)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Failed to create client from kubeconfig", err)
httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the KubeClientMiddleware operation, unable to create kubernetes client from kubeconfig. Error: ", err)
return
}

View File

@@ -10,67 +10,65 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id getKubernetesIngressControllers
// @id GetAllKubernetesIngressControllers
// @summary Get a list of ingress controllers
// @description Get a list of ingress controllers for the given environment
// @description **Access policy**: authenticated
// @description Get a list of ingress controllers for the given environment. If the allowedOnly query parameter is set, only ingress controllers that are allowed by the environment's ingress configuration will be returned.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param allowedOnly query boolean false "Only return allowed ingress controllers"
// @success 200 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve ingress controllers"
// @router /kubernetes/{id}/ingresscontrollers [get]
func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
func (handler *Handler) getAllKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Invalid environment identifier route variable")
return httperror.BadRequest("Invalid environment identifier route variable", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
}
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
}
allowedOnly, err := request.RetrieveBooleanQueryParameter(r, "allowedOnly", true)
if err != nil {
return httperror.BadRequest(
"Invalid allowedOnly boolean query parameter",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to retrieve allowedOnly query parameter")
return httperror.BadRequest("Unable to retrieve allowedOnly query parameter", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to create Kubernetes client",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to get privileged kube client")
return httperror.InternalServerError("Unable to get privileged kube client", err)
}
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
}
// Add none controller if "AllowNone" is set for endpoint.
@@ -82,16 +80,17 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
})
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
for i := range controllers {
controllers[i].Availability = true
if controllers[i].ClassName != "none" {
controllers[i].New = true
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = controllers[i].ClassName
updatedClass.Type = controllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: controllers[i].ClassName,
Type: controllers[i].Type,
}
// Check if the controller is already known.
for _, existingClass := range existingClasses {
@@ -112,16 +111,14 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
endpoint,
)
if err != nil {
return httperror.InternalServerError(
"Unable to store found IngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
// If the allowedOnly query parameter was set. We need to prune out
// disallowed controllers from the response.
if allowedOnly {
var allowedControllers models.K8sIngressControllers
allowedControllers := models.K8sIngressControllers{}
for _, controller := range controllers {
if controller.Availability {
allowedControllers = append(allowedControllers, controller)
@@ -132,62 +129,61 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r
return response.JSON(w, controllers)
}
// @id getKubernetesIngressControllersByNamespace
// @id GetKubernetesIngressControllersByNamespace
// @summary Get a list ingress controllers by namespace
// @description Get a list of ingress controllers for the given environment in the provided namespace
// @description **Access policy**: authenticated
// @description Get a list of ingress controllers for the given environment in the provided namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @success 200 {object} models.K8sIngressControllers "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or a namespace with the specified name."
// @failure 500 "Server error occurred while attempting to retrieve ingress controllers by a namespace"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [get]
func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve environment identifier from request")
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
}
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
}
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesIngressControllers").Msg("Unable to create Kubernetes client")
return httperror.InternalServerError("Unable to create Kubernetes client", err)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest(
"Invalid namespace identifier route variable",
err,
)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
currentControllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Failed to fetch ingressclasses",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to retrieve ingress controllers from the Kubernetes")
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
}
// Add none controller if "AllowNone" is set for endpoint.
if endpoint.Kubernetes.Configuration.AllowNoneIngressClass {
@@ -197,21 +193,24 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
Type: "custom",
})
}
kubernetesConfig := endpoint.Kubernetes.Configuration
existingClasses := kubernetesConfig.IngressClasses
ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace
var updatedClasses []portainer.KubernetesIngressClassConfig
var controllers models.K8sIngressControllers
updatedClasses := []portainer.KubernetesIngressClassConfig{}
controllers := models.K8sIngressControllers{}
for i := range currentControllers {
var globallyblocked bool
globallyblocked := false
currentControllers[i].Availability = true
if currentControllers[i].ClassName != "none" {
currentControllers[i].New = true
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = currentControllers[i].ClassName
updatedClass.Type = currentControllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: currentControllers[i].ClassName,
Type: currentControllers[i].Type,
}
// Check if the controller is blocked globally or in the current
// namespace.
@@ -243,81 +242,77 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon
// Update the database to match the list of found controllers.
// This includes pruning out controllers which no longer exist.
endpoint.Kubernetes.Configuration.IngressClasses = updatedClasses
err = handler.DataStore.Endpoint().UpdateEndpoint(
portainer.EndpointID(endpointID),
endpoint,
)
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to store found IngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "getKubernetesIngressControllersByNamespace").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
return response.JSON(w, controllers)
}
// @id updateKubernetesIngressControllers
// @id UpdateKubernetesIngressControllers
// @summary Update (block/unblock) ingress controllers
// @description Update (block/unblock) ingress controllers
// @description **Access policy**: authenticated
// @description Update (block/unblock) ingress controllers for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param id path int true "Environment identifier"
// @param body body models.K8sIngressControllers true "Ingress controllers"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the ingress controllers to update."
// @failure 500 "Server error occurred while attempting to update ingress controllers."
// @router /kubernetes/{id}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest(
"Invalid environment identifier route variable",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve environment identifier from request")
return httperror.BadRequest("Unable to retrieve environment identifier from request", err)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound(
"Unable to find an environment with the specified identifier inside the database",
err,
)
} else if err != nil {
return httperror.InternalServerError(
"Unable to find an environment with the specified identifier inside the database",
err,
)
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
}
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to find an environment with the specified identifier inside the database")
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
}
var payload models.K8sIngressControllers
payload := models.K8sIngressControllers{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError(
"Unable to create Kubernetes client",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to get privileged kube client")
return httperror.InternalServerError("Unable to get privileged kube client", err)
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
controllers, err := cli.GetIngressControllers()
if err != nil {
return httperror.InternalServerError(
"Unable to get ingress controllers",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
return httperror.NotFound("Unable to retrieve ingress controllers from the Kubernetes", err)
}
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to retrieve ingress controllers from the Kubernetes")
return httperror.InternalServerError("Unable to retrieve ingress controllers from the Kubernetes", err)
}
// Add none controller if "AllowNone" is set for endpoint.
@@ -329,14 +324,15 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
})
}
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
for i := range controllers {
controllers[i].Availability = true
controllers[i].New = true
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = controllers[i].ClassName
updatedClass.Type = controllers[i].Type
updatedClass := portainer.KubernetesIngressClassConfig{
Name: controllers[i].ClassName,
Type: controllers[i].Type,
}
// Check if the controller is already known.
for _, existingClass := range existingClasses {
@@ -366,59 +362,64 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter
endpoint,
)
if err != nil {
return httperror.InternalServerError(
"Unable to update the BlockedIngressClasses inside the database",
err,
)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllers").Msg("Unable to store found IngressClasses inside the database")
return httperror.InternalServerError("Unable to store found IngressClasses inside the database", err)
}
return response.Empty(w)
}
// @id updateKubernetesIngressControllersByNamespace
// @id UpdateKubernetesIngressControllersByNamespace
// @summary Update (block/unblock) ingress controllers by namespace
// @description Update (block/unblock) ingress controllers by namespace for the provided environment
// @description **Access policy**: authenticated
// @description Update (block/unblock) ingress controllers by namespace for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body []models.K8sIngressControllers true "Ingress controllers"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param body body models.K8sIngressControllers true "Ingress controllers"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to update ingress controllers by namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresscontrollers [put]
func (handler *Handler) updateKubernetesIngressControllersByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Msg("Unable to fetch endpoint")
return httperror.NotFound("Unable to fetch endpoint", err)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
var payload models.K8sIngressControllers
payload := models.K8sIngressControllers{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
existingClasses := endpoint.Kubernetes.Configuration.IngressClasses
var updatedClasses []portainer.KubernetesIngressClassConfig
updatedClasses := []portainer.KubernetesIngressClassConfig{}
PayloadLoop:
for _, p := range payload {
for _, existingClass := range existingClasses {
if p.ClassName != existingClass.Name {
continue
}
var updatedClass portainer.KubernetesIngressClassConfig
updatedClass.Name = existingClass.Name
updatedClass.Type = existingClass.Type
updatedClass.GloballyBlocked = existingClass.GloballyBlocked
updatedClass := portainer.KubernetesIngressClassConfig{
Name: existingClass.Name,
Type: existingClass.Type,
GloballyBlocked: existingClass.GloballyBlocked,
}
// Handle "allow"
if p.Availability {
@@ -445,10 +446,7 @@ PayloadLoop:
continue PayloadLoop
}
}
updatedClass.BlockedNamespaces = append(
updatedClass.BlockedNamespaces,
namespace,
)
updatedClass.BlockedNamespaces = append(updatedClass.BlockedNamespaces, namespace)
updatedClasses = append(updatedClasses, updatedClass)
}
}
@@ -458,7 +456,7 @@ PayloadLoop:
// part of updatedClasses, but we MUST include it or we would remove the
// global block.
for _, existingClass := range existingClasses {
var found bool
found := false
for _, updatedClass := range updatedClasses {
if existingClass.Name == updatedClass.Name {
@@ -474,32 +472,125 @@ PayloadLoop:
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return httperror.InternalServerError("Unable to update the BlockedIngressClasses inside the database", err)
log.Error().Err(err).Str("context", "updateKubernetesIngressControllersByNamespace").Str("namespace", namespace).Msg("Unable to store BlockedIngressClasses inside the database")
return httperror.InternalServerError("Unable to store BlockedIngressClasses inside the database", err)
}
return response.Empty(w)
}
// @id getKubernetesIngresses
// @summary Get kubernetes ingresses by namespace
// @description Get kubernetes ingresses by namespace for the provided environment
// @description **Access policy**: authenticated
// @id GetAllKubernetesClusterIngresses
// @summary Get kubernetes ingresses at the cluster level
// @description Get kubernetes ingresses at the cluster level for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param withServices query boolean false "Lookup services associated with each ingress"
// @success 200 {array} models.K8sIngressInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve ingresses."
// @router /kubernetes/{id}/ingresses [get]
func (handler *Handler) GetAllKubernetesClusterIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
ingresses, err := handler.getKubernetesClusterIngresses(r)
if err != nil {
return err
}
return response.JSON(w, ingresses)
}
// @id GetAllKubernetesClusterIngressesCount
// @summary Get Ingresses count
// @description Get the number of kubernetes ingresses within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve ingresses count."
// @router /kubernetes/{id}/ingresses/count [get]
func (handler *Handler) getAllKubernetesClusterIngressesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
ingresses, err := handler.getKubernetesClusterIngresses(r)
if err != nil {
return err
}
return response.JSON(w, len(ingresses))
}
func (handler *Handler) getKubernetesClusterIngresses(r *http.Request) ([]models.K8sIngressInfo, *httperror.HandlerError) {
withServices, err := request.RetrieveBooleanQueryParameter(r, "withServices", true)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve withApplications query parameter")
return nil, httperror.BadRequest("Unable to retrieve withApplications query parameter", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getKubernetesClusterIngresses").Msg("Unable to get a Kubernetes client for the user")
return nil, httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
}
ingresses, err := cli.GetIngresses("")
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unauthorized access to the Kubernetes API")
return nil, httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a cluster level user")
return nil, httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a cluster level user", err)
}
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a cluster level user")
return nil, httperror.InternalServerError("Unable to retrieve ingresses from the Kubernetes for a cluster level user", err)
}
if withServices {
ingressesWithServices, err := cli.CombineIngressesWithServices(ingresses)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesClusterIngresses").Msg("Unable to combine ingresses with services")
return nil, httperror.InternalServerError("Unable to combine ingresses with services", err)
}
return ingressesWithServices, nil
}
return ingresses, nil
}
// @id GetAllKubernetesIngresses
// @summary Get a list of Ingresses
// @description Get a list of Ingresses. If namespace is provided, it will return the list of Ingresses in that namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body []models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 200 {array} models.K8sIngressInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve ingresses"
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [get]
func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "getKubernetesIngresses").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
@@ -509,38 +600,103 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re
ingresses, err := cli.GetIngresses(namespace)
if err != nil {
return httperror.InternalServerError("Unable to retrieve ingresses", err)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesIngresses").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getKubernetesIngresses").Str("namespace", namespace).Msg("Unable to retrieve ingresses from the Kubernetes for a namespace level user")
return httperror.InternalServerError("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
}
return response.JSON(w, ingresses)
}
// @id createKubernetesIngress
// @summary Create a kubernetes ingress by namespace
// @description Create a kubernetes ingress by namespace for the provided environment
// @description **Access policy**: authenticated
// @id GetKubernetesIngress
// @summary Get an Ingress by name
// @description Get an Ingress by name for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param ingress path string true "Ingress name"
// @success 200 {object} models.K8sIngressInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find an ingress with the specified name."
// @failure 500 "Server error occurred while attempting to retrieve an ingress."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses/{ingress} [get]
func (handler *Handler) getKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesIngress").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
ingressName, err := request.RetrieveRouteVariableValue(r, "ingress")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesIngress").Msg("Unable to retrieve ingress from request")
return httperror.BadRequest("Unable to retrieve ingress from request", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
ingress, err := cli.GetIngress(namespace, ingressName)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unable to retrieve ingress from the Kubernetes for a namespace level user")
return httperror.NotFound("Unable to retrieve ingress from the Kubernetes for a namespace level user", err)
}
log.Error().Err(err).Str("context", "getKubernetesIngress").Str("namespace", namespace).Str("ingress", ingressName).Msg("Unable to retrieve ingress from the Kubernetes for a namespace level user")
return httperror.InternalServerError("Unable to retrieve ingress from the Kubernetes for a namespace level user", err)
}
return response.JSON(w, ingress)
}
// @id CreateKubernetesIngress
// @summary Create an Ingress
// @description Create an Ingress for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 409 "Conflict - an ingress with the same name already exists in the specified namespace."
// @failure 500 "Server error occurred while attempting to create an ingress."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [post]
func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "createKubernetesIngress").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
var payload models.K8sIngressInfo
payload := models.K8sIngressInfo{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "createKubernetesIngress").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
owner := "admin"
@@ -556,26 +712,39 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R
err = cli.CreateIngress(namespace, payload, owner)
if err != nil {
return httperror.InternalServerError("Unable to retrieve the ingress", err)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsAlreadyExists(err) {
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Ingress already exists")
return httperror.Conflict("Ingress already exists", err)
}
log.Error().Err(err).Str("context", "createKubernetesIngress").Str("namespace", namespace).Msg("Unable to create an ingress")
return httperror.InternalServerError("Unable to create an ingress", err)
}
return response.Empty(w)
}
// @id deleteKubernetesIngresses
// @summary Delete kubernetes ingresses
// @description Delete kubernetes ingresses for the provided environment
// @description **Access policy**: authenticated
// @id DeleteKubernetesIngresses
// @summary Delete one or more Ingresses
// @description Delete one or more Ingresses in the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param body body models.K8sIngressDeleteRequests true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific ingress."
// @failure 500 "Server error occurred while attempting to delete specified ingresses."
// @router /kubernetes/{id}/ingresses/delete [post]
func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, handlerErr := handler.getProxyKubeClient(r)
@@ -583,46 +752,62 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http
return handlerErr
}
var payload models.K8sIngressDeleteRequests
payload := models.K8sIngressDeleteRequests{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
err = cli.DeleteIngresses(payload)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to retrieve ingresses from the Kubernetes for a namespace level user")
return httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
}
log.Error().Err(err).Str("context", "deleteKubernetesIngresses").Msg("Unable to delete ingresses")
return httperror.InternalServerError("Unable to delete ingresses", err)
}
return response.Empty(w)
}
// @id updateKubernetesIngress
// @summary Update kubernetes ingress rule
// @description Update kubernetes ingress rule for the provided environment
// @description **Access policy**: authenticated
// @id UpdateKubernetesIngress
// @summary Update an Ingress
// @description Update an Ingress for the provided environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sIngressInfo true "Ingress details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the specified ingress."
// @failure 500 "Server error occurred while attempting to update the specified ingress."
// @router /kubernetes/{id}/namespaces/{namespace}/ingresses [put]
func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "updateKubernetesIngress").Msg("Unable to retrieve namespace from request")
return httperror.BadRequest("Unable to retrieve namespace from request", err)
}
var payload models.K8sIngressInfo
payload := models.K8sIngressInfo{}
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "updateKubernetesIngress").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("Unable to decode and validate the request payload", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
@@ -632,7 +817,18 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R
err = cli.UpdateIngress(namespace, payload)
if err != nil {
return httperror.InternalServerError("Unable to update the ingress", err)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unable to retrieve ingresses from the K ubernetes for a namespace level user")
return httperror.NotFound("Unable to retrieve ingresses from the Kubernetes for a namespace level user", err)
}
log.Error().Err(err).Str("context", "updateKubernetesIngress").Str("namespace", namespace).Msg("Unable to update ingress in a namespace")
return httperror.InternalServerError("Unable to update ingress in a namespace", err)
}
return response.Empty(w)

View File

@@ -7,23 +7,22 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// @id getKubernetesMetricsForAllNodes
// @id GetKubernetesMetricsForAllNodes
// @summary Get a list of nodes with their live metrics
// @description Get a list of nodes with their live metrics
// @description **Access policy**: authenticated
// @description Get a list of metrics associated with all nodes of a cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @success 200 {object} v1beta1.NodeMetricsList "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 500 "Server error occurred while attempting to retrieve the list of nodes with their live metrics."
// @router /kubernetes/{id}/metrics/nodes [get]
func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
@@ -33,45 +32,49 @@ func (handler *Handler) getKubernetesMetricsForAllNodes(w http.ResponseWriter, r
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllNodes").Msg("Failed to create metrics KubeClient")
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
}
metrics, err := cli.MetricsV1beta1().NodeMetricses().List(r.Context(), v1.ListOptions{})
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllNodes").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForNode
// @id GetKubernetesMetricsForNode
// @summary Get live metrics for a node
// @description Get live metrics for a node
// @description **Access policy**: authenticated
// @description Get live metrics for the specified node.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param name path string true "Node identifier"
// @success 200 {object} v1beta1.NodeMetrics "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 500 "Server error occurred while attempting to retrieve the live metrics for the specified node."
// @router /kubernetes/{id}/metrics/nodes/{name} [get]
func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to create metrics KubeClient")
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
}
nodeName, err := request.RetrieveRouteVariableValue(r, "name")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Invalid node identifier route variable")
return httperror.BadRequest("Invalid node identifier route variable", err)
}
@@ -81,90 +84,98 @@ func (handler *Handler) getKubernetesMetricsForNode(w http.ResponseWriter, r *ht
v1.GetOptions{},
)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForNode").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForAllPods
// @id GetKubernetesMetricsForAllPods
// @summary Get a list of pods with their live metrics
// @description Get a list of pods with their live metrics
// @description **Access policy**: authenticated
// @description Get a list of pods with their live metrics for the specified namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @success 200 {object} v1beta1.PodMetricsList "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 500 "Server error occurred while attempting to retrieve the list of pods with their live metrics."
// @router /kubernetes/{id}/metrics/pods/{namespace} [get]
func (handler *Handler) getKubernetesMetricsForAllPods(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to create metrics KubeClient")
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Invalid namespace identifier route variable")
return httperror.BadRequest("Invalid namespace identifier route variable", err)
}
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).List(r.Context(), v1.ListOptions{})
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForAllPods").Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}
return response.JSON(w, metrics)
}
// @id getKubernetesMetricsForPod
// @id GetKubernetesMetricsForPod
// @summary Get live metrics for a pod
// @description Get live metrics for a pod
// @description **Access policy**: authenticated
// @description Get live metrics for the specified pod.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @param name path string true "Pod identifier"
// @success 200 {object} v1beta1.PodMetrics "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 500 "Server error occurred while attempting to retrieve the live metrics for the specified pod."
// @router /kubernetes/{id}/metrics/pods/{namespace}/{name} [get]
func (handler *Handler) getKubernetesMetricsForPod(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Failed to fetch endpoint")
return httperror.InternalServerError(err.Error(), err)
}
cli, err := handler.KubernetesClientFactory.CreateRemoteMetricsClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Failed to create metrics KubeClient")
return httperror.InternalServerError("failed to create metrics KubeClient", nil)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Invalid namespace identifier route variable")
return httperror.BadRequest("Invalid namespace identifier route variable", err)
}
podName, err := request.RetrieveRouteVariableValue(r, "name")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Msg("Invalid pod identifier route variable")
return httperror.BadRequest("Invalid pod identifier route variable", err)
}
metrics, err := cli.MetricsV1beta1().PodMetricses(namespace).Get(r.Context(), podName, v1.GetOptions{})
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesMetricsForPod").Str("namespace", namespace).Str("pod", podName).Msg("Failed to fetch metrics")
return httperror.InternalServerError("Failed to fetch metrics", err)
}

View File

@@ -1,185 +1,282 @@
package kubernetes
import (
"errors"
"fmt"
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id getKubernetesNamespaces
// @summary Get a list of kubernetes namespaces
// @description Get a list of all kubernetes namespaces in the cluster
// @description **Access policy**: authenticated
// @id GetKubernetesNamespaces
// @summary Get a list of namespaces
// @description Get a list of all namespaces within the given environment based on the user role and permissions. If the user is an admin, they can access all namespaces. If the user is not an admin, they can only access namespaces that they have access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @success 200 {object} map[string]portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @param id path int true "Environment identifier"
// @param withResourceQuota query boolean true "When set to true, include the resource quota information as part of the Namespace information. Default is false"
// @success 200 {array} portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of namespaces."
// @router /kubernetes/{id}/namespaces [get]
func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Invalid query parameter withResourceQuota")
return httperror.BadRequest("an error occurred during the GetKubernetesNamespaces operation, invalid query parameter withResourceQuota. Error: ", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespaces").Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
namespaces, err := cli.GetNamespaces()
if err != nil {
return httperror.InternalServerError("Unable to retrieve namespaces", err)
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Unable to retrieve namespaces from the Kubernetes cluster")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to retrieve namespaces from the Kubernetes cluster. Error: ", err)
}
return response.JSON(w, namespaces)
if withResourceQuota {
return cli.CombineNamespacesWithResourceQuotas(namespaces, w)
}
return response.JSON(w, cli.ConvertNamespaceMapToSlice(namespaces))
}
// @id getKubernetesNamespace
// @summary Get kubernetes namespace details
// @description Get kubernetes namespace details for the provided namespace within the given environment
// @description **Access policy**: authenticated
// @id GetKubernetesNamespacesCount
// @summary Get the total number of kubernetes namespaces within the given Portainer environment.
// @description Get the total number of kubernetes namespaces within the given environment, including the system namespaces. The total count depends on the user's role and permissions.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace"
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to compute the namespace count."
// @router /kubernetes/{id}/namespaces/count [get]
func (handler *Handler) getKubernetesNamespacesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespacesCount").Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespacesCount operation, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
namespaces, err := cli.GetNamespaces()
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNamespacesCount").Msg("Unable to retrieve namespaces from the Kubernetes cluster to count the total")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespacesCount operation, unable to retrieve namespaces from the Kubernetes cluster to count the total. Error: ", err)
}
return response.JSON(w, len(namespaces))
}
// @id GetKubernetesNamespace
// @summary Get namespace details
// @description Get namespace details for the provided namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "The namespace name to get details for"
// @param withResourceQuota query boolean true "When set to true, include the resource quota information as part of the Namespace information. Default is false"
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific namespace."
// @failure 500 "Server error occurred while attempting to retrieve specified namespace information."
// @router /kubernetes/{id}/namespaces/{namespace} [get]
func (handler *Handler) getKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
ns, err := request.RetrieveRouteVariableValue(r, "namespace")
namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest(
"Invalid namespace identifier route variable",
err,
)
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Invalid namespace parameter namespace")
return httperror.BadRequest("an error occurred during the GetKubernetesNamespace operation, invalid namespace parameter namespace. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
withResourceQuota, err := request.RetrieveBooleanQueryParameter(r, "withResourceQuota", true)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Invalid query parameter withResourceQuota")
return httperror.BadRequest("an error occurred during the GetKubernetesNamespace operation for the namespace %s, invalid query parameter withResourceQuota. Error: ", err)
}
namespace, err := cli.GetNamespace(ns)
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespace").Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
namespaceInfo, err := cli.GetNamespace(namespaceName)
if err != nil {
return httperror.InternalServerError("Unable to retrieve namespace", err)
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unable to find the namespace")
return httperror.NotFound(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation for the namespace %s, unable to find the namespace. Error: ", namespaceName), err)
}
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unauthorized to access the namespace")
return httperror.Forbidden(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation, unauthorized to access the namespace: %s. Error: ", namespaceName), err)
}
log.Error().Err(err).Str("context", "GetKubernetesNamespace").Msg("Unable to get the namespace")
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the GetKubernetesNamespace operation, unable to get the namespace: %s. Error: ", namespaceName), err)
}
if withResourceQuota {
return cli.CombineNamespaceWithResourceQuota(namespaceInfo, w)
}
return response.JSON(w, namespaceInfo)
}
// @id CreateKubernetesNamespace
// @summary Create a namespace
// @description Create a namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment identifier"
// @param body body models.K8sNamespaceDetails true "Namespace configuration details"
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 409 "Conflict - the namespace already exists."
// @failure 500 "Server error occurred while attempting to create the namespace."
// @router /kubernetes/{id}/namespaces [post]
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
payload := models.K8sNamespaceDetails{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Msg("Invalid request payload")
return httperror.BadRequest("an error occurred during the CreateKubernetesNamespace operation, invalid request payload. Error: ", err)
}
namespaceName := payload.Name
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("an error occurred during the CreateKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
namespace, err := cli.CreateNamespace(payload)
if err != nil {
if k8serrors.IsAlreadyExists(err) {
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("The namespace already exists")
return httperror.Conflict(fmt.Sprintf("an error occurred during the CreateKubernetesNamespace operation, the namespace %s already exists. Error: ", namespaceName), err)
}
log.Error().Err(err).Str("context", "CreateKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to create the namespace")
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the CreateKubernetesNamespace operation, unable to create the namespace: %s", namespaceName), err)
}
return response.JSON(w, namespace)
}
// @id createKubernetesNamespace
// @summary Create a kubernetes namespace
// @description Create a kubernetes namespace within the given environment
// @description **Access policy**: authenticated
// @id DeleteKubernetesNamespace
// @summary Delete a kubernetes namespace
// @description Delete a kubernetes namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param body body models.K8sNamespaceDetails true "Namespace configuration details"
// @security ApiKeyAuth || jwt
// @param id path int true "Environment identifier"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces [post]
func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
err = cli.CreateNamespace(payload)
if err != nil {
return httperror.InternalServerError("Unable to create namespace", err)
}
return nil
}
// @id deleteKubernetesNamespace
// @summary Delete kubernetes namespace
// @description Delete a kubernetes namespace within the given environment
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace} [delete]
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error occurred while attempting to delete the namespace."
// @router /kubernetes/{id}/namespaces [delete]
func (handler *Handler) deleteKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sNamespaceDetails
err := request.DecodeAndValidateJSONPayload(r, &payload)
namespaceNames, err := request.GetPayload[deleteKubernetesNamespacePayload](r)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Msg("Invalid namespace identifier route variable")
return httperror.BadRequest("an error occurred during the DeleteKubernetesNamespace operation, invalid namespace identifier route variable. Error: ", err)
}
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "DeleteKubernetesNamespace").Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", httpErr)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
for _, namespaceName := range *namespaceNames {
_, err := cli.DeleteNamespace(namespaceName)
if err != nil {
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to find the namespace")
return httperror.NotFound(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to find the namespace. Error: ", namespaceName), err)
}
log.Error().Err(err).Str("context", "DeleteKubernetesNamespace").Str("namespace", namespaceName).Msg("Unable to delete the namespace")
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the DeleteKubernetesNamespace operation for the namespace %s, unable to delete the Kubernetes namespace. Error: ", namespaceName), err)
}
}
err = cli.DeleteNamespace(namespace)
if err != nil {
return httperror.InternalServerError("Unable to delete namespace", err)
return response.JSON(w, namespaceNames)
}
type deleteKubernetesNamespacePayload []string
func (payload deleteKubernetesNamespacePayload) Validate(r *http.Request) error {
if len(payload) == 0 {
return errors.New("namespace names are required")
}
return nil
}
// @id updateKubernetesNamespace
// @summary Updates a kubernetes namespace
// @description Update a kubernetes namespace within the given environment
// @description **Access policy**: authenticated
// @id UpdateKubernetesNamespace
// @summary Update a namespace
// @description Update a namespace within the given environment.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace"
// @param body body models.K8sNamespaceDetails true "Namespace details"
// @success 200 {string} string "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 200 {object} portainer.K8sNamespaceInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific namespace."
// @failure 500 "Server error occurred while attempting to update the namespace."
// @router /kubernetes/{id}/namespaces/{namespace} [put]
func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sNamespaceDetails
payload := models.K8sNamespaceDetails{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
return httperror.BadRequest("an error occurred during the UpdateKubernetesNamespace operation, invalid request payload. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
namespaceName := payload.Name
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the UpdateKubernetesNamespace operation for the namespace %s, unable to get a Kubernetes client for the user. Error: ", namespaceName), httpErr)
}
err = cli.UpdateNamespace(payload)
namespace, err := cli.UpdateNamespace(payload)
if err != nil {
return httperror.InternalServerError("Unable to update namespace", err)
return httperror.InternalServerError(fmt.Sprintf("an error occurred during the UpdateKubernetesNamespace operation for the namespace %s, unable to update the Kubernetes namespace. Error: ", namespaceName), err)
}
return nil
return response.JSON(w, namespace)
}

View File

@@ -6,53 +6,72 @@ import (
"github.com/portainer/portainer/api/http/middlewares"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesNodesLimits
// @summary Get CPU and memory limits of all nodes within k8s cluster
// @description Get CPU and memory limits of all nodes within k8s cluster
// @description **Access policy**: authenticated
// @description Get CPU and memory limits of all nodes within k8s cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} portainer.K8sNodesLimits "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve nodes limits."
// @router /kubernetes/{id}/nodes_limits [get]
func (handler *Handler) getKubernetesNodesLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to find an environment on request context")
return httperror.NotFound("Unable to find an environment on request context", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to create Kubernetes client")
return httperror.InternalServerError("Unable to create Kubernetes client", err)
}
nodesLimits, err := cli.GetNodesLimits()
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNodesLimits").Msg("Unable to retrieve nodes limits")
return httperror.InternalServerError("Unable to retrieve nodes limits", err)
}
return response.JSON(w, nodesLimits)
}
// @id GetKubernetesMaxResourceLimits
// @summary Get max CPU and memory limits of all nodes within k8s cluster
// @description Get max CPU and memory limits (unused resources) of all nodes within k8s cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {object} portainer.K8sNodesLimits "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve nodes limits."
// @router /kubernetes/{id}/max_resource_limits [get]
func (handler *Handler) getKubernetesMaxResourceLimits(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to find an environment on request context")
return httperror.NotFound("Unable to find an environment on request context", err)
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("Failed to lookup KubeClient", err)
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to create Kubernetes client")
return httperror.InternalServerError("Unable to create Kubernetes client", err)
}
overCommit := endpoint.Kubernetes.Configuration.EnableResourceOverCommit
@@ -61,6 +80,7 @@ func (handler *Handler) getKubernetesMaxResourceLimits(w http.ResponseWriter, r
// name is set to "" so all namespaces resources are considered when calculating max resource limits
resourceLimit, err := cli.GetMaxResourceLimits("", overCommit, overCommitPercent)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesMaxResourceLimits").Msg("Unable to retrieve max resource limit")
return httperror.InternalServerError("Unable to retrieve max resource limit", err)
}

View File

@@ -5,28 +5,33 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id IsRBACEnabled
// @id GetKubernetesRBACStatus
// @summary Check if RBAC is enabled
// @description Check if RBAC is enabled in the current Kubernetes cluster.
// @description **Access policy**: administrator
// @tags rbac_enabled
// @security ApiKeyAuth
// @security jwt
// @produce text/plain
// @description Check if RBAC is enabled in the specified Kubernetes cluster.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 "Success"
// @failure 500 "Server error"
// @success 200 {boolean} bool "RBAC status"
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the RBAC status."
// @router /kubernetes/{id}/rbac_enabled [get]
func (handler *Handler) isRBACEnabled(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
func (handler *Handler) getKubernetesRBACStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
log.Error().Err(handlerErr).Str("context", "GetKubernetesRBACStatus").Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("Unable to get a Kubernetes client for the user. Error: ", handlerErr)
}
isRBACEnabled, err := cli.IsRBACEnabled()
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesRBACStatus").Msg("Failed to check RBAC status")
return httperror.InternalServerError("Failed to check RBAC status", err)
}

View File

@@ -0,0 +1,40 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesRoleBindings
// @summary Get a list of kubernetes role bindings
// @description Get a list of kubernetes role bindings that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {array} kubernetes.K8sRoleBinding "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of role bindings."
// @router /kubernetes/{id}/rolebindings [get]
func (handler *Handler) getAllKubernetesRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetAllKubernetesRoleBindings").Msg("Unable to prepare kube client")
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
}
rolebindings, err := cli.GetRoleBindings("")
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesRoleBindings").Msg("Unable to fetch rolebindings")
return httperror.InternalServerError("unable to fetch rolebindings. Error: ", err)
}
return response.JSON(w, rolebindings)
}

View File

@@ -0,0 +1,40 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesRoles
// @summary Get a list of kubernetes roles
// @description Get a list of kubernetes roles that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {array} kubernetes.K8sRole "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of roles."
// @router /kubernetes/{id}/roles [get]
func (handler *Handler) getAllKubernetesRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetAllKubernetesRoles").Msg("Unable to prepare kube client")
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
}
roles, err := cli.GetRoles("")
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesRoles").Msg("Unable to fetch roles across all namespaces")
return httperror.InternalServerError("unable to fetch roles across all namespaces. Error: ", err)
}
return response.JSON(w, roles)
}

View File

@@ -0,0 +1,143 @@
package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesSecret
// @summary Get a Secret
// @description Get a Secret by name for a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "The namespace name where the secret is located"
// @param secret path string true "The secret name to get details for"
// @success 200 {object} models.K8sSecret "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve a secret by name belong in a namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/secrets/{secret} [get]
func (handler *Handler) getKubernetesSecret(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
}
secretName, err := request.RetrieveRouteVariableValue(r, "secret")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to retrieve secret identifier route variable")
return httperror.BadRequest("unable to retrieve secret identifier route variable. Error: ", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpErr)
}
secret, err := cli.GetSecret(namespace, secretName)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Str("secret", secretName).Msg("Unable to get secret")
return httperror.InternalServerError("unable to get secret. Error: ", err)
}
secretWithApplication, err := cli.CombineSecretWithApplications(secret)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesSecret").Str("namespace", namespace).Str("secret", secretName).Msg("Unable to combine secret with associated applications")
return httperror.InternalServerError("unable to combine secret with associated applications. Error: ", err)
}
return response.JSON(w, secretWithApplication)
}
// @id GetKubernetesSecrets
// @summary Get a list of Secrets
// @description Get a list of Secrets for a given namespace. If isUsed is set to true, information about the applications that use the secrets is also returned.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param isUsed query bool true "When set to true, associate the Secrets with the applications that use them"
// @success 200 {array} models.K8sSecret "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve all secrets from the cluster."
// @router /kubernetes/{id}/secrets [get]
func (handler *Handler) GetAllKubernetesSecrets(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
secrets, err := handler.getAllKubernetesSecrets(r)
if err != nil {
return err
}
return response.JSON(w, secrets)
}
// @id GetKubernetesSecretsCount
// @summary Get Secrets count
// @description Get the count of Secrets across all namespaces that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the count of all secrets from the cluster."
// @router /kubernetes/{id}/secrets/count [get]
func (handler *Handler) getAllKubernetesSecretsCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
secrets, err := handler.getAllKubernetesSecrets(r)
if err != nil {
return err
}
return response.JSON(w, len(secrets))
}
func (handler *Handler) getAllKubernetesSecrets(r *http.Request) ([]models.K8sSecret, *httperror.HandlerError) {
isUsed, err := request.RetrieveBooleanQueryParameter(r, "isUsed", true)
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to retrieve isUsed query parameter")
return nil, httperror.BadRequest("unable to retrieve isUsed query parameter. Error: ", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetAllKubernetesSecrets").Msg("Unable to prepare kube client")
return nil, httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
}
secrets, err := cli.GetSecrets("")
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to get secrets")
return nil, httperror.InternalServerError("unable to get secrets. Error: ", err)
}
if isUsed {
secretsWithApplications, err := cli.CombineSecretsWithApplications(secrets)
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to combine secrets with associated applications")
return nil, httperror.InternalServerError("unable to combine secrets with associated applications. Error: ", err)
}
return secretsWithApplications, nil
}
return secrets, nil
}

View File

@@ -0,0 +1,40 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id GetKubernetesServiceAccounts
// @summary Get a list of kubernetes service accounts
// @description Get a list of kubernetes service accounts that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {array} kubernetes.K8sServiceAccount "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the list of service accounts."
// @router /kubernetes/{id}/serviceaccounts [get]
func (handler *Handler) getAllKubernetesServiceAccounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetAllKubernetesServiceAccounts").Msg("Unable to prepare kube client")
return httperror.InternalServerError("unable to prepare kube client. Error: ", httpErr)
}
serviceAccounts, err := cli.GetServiceAccounts("")
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesServiceAccounts").Msg("Unable to fetch service accounts across all namespaces")
return httperror.InternalServerError("unable to fetch service accounts. Error: ", err)
}
return response.JSON(w, serviceAccounts)
}

View File

@@ -7,165 +7,298 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id getKubernetesServices
// @summary Get a list of kubernetes services for a given namespace
// @description Get a list of kubernetes services for a given namespace
// @description **Access policy**: authenticated
// @id GetKubernetesServices
// @summary Get a list of services
// @description Get a list of services that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param namespace path string true "Namespace name"
// @param lookupapplications query boolean false "Lookup applications associated with each service"
// @param id path int true "Environment identifier"
// @param withApplications query boolean false "Lookup applications associated with each service"
// @success 200 {array} models.K8sServiceInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve all services."
// @router /kubernetes/{id}/services [get]
func (handler *Handler) GetAllKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
services, err := handler.getAllKubernetesServices(r)
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
}
lookup, err := request.RetrieveBooleanQueryParameter(r, "lookupapplications", true)
if err != nil {
return httperror.BadRequest("Invalid lookupapplications query parameter", err)
}
services, err := cli.GetServices(namespace, lookup)
if err != nil {
return httperror.InternalServerError("Unable to retrieve services", err)
return err
}
return response.JSON(w, services)
}
// @id createKubernetesService
// @summary Create a kubernetes service
// @description Create a kubernetes service within a given namespace
// @description **Access policy**: authenticated
// @id GetAllKubernetesServicesCount
// @summary Get services count
// @description Get the count of services that the user has access to.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the total count of all services."
// @router /kubernetes/{id}/services/count [get]
func (handler *Handler) getAllKubernetesServicesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
services, err := handler.getAllKubernetesServices(r)
if err != nil {
return err
}
return response.JSON(w, len(services))
}
func (handler *Handler) getAllKubernetesServices(r *http.Request) ([]models.K8sServiceInfo, *httperror.HandlerError) {
withApplications, err := request.RetrieveBooleanQueryParameter(r, "withApplications", true)
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to retrieve withApplications identifier")
return nil, httperror.BadRequest("unable to retrieve withApplications query parameter. Error: ", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetAllKubernetesServices").Msg("Unable to get a Kubernetes client for the user")
return nil, httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpErr)
}
services, err := cli.GetServices("")
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unauthorized access to the Kubernetes API")
return nil, httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
}
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to retrieve services from the Kubernetes for a cluster level user")
return nil, httperror.InternalServerError("unable to retrieve services from the Kubernetes for a cluster level user. Error: ", err)
}
if withApplications && len(services) > 0 {
servicesWithApplications, err := cli.CombineServicesWithApplications(services)
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesServices").Msg("Unable to combine services with applications")
return nil, httperror.InternalServerError("unable to combine services with applications. Error: ", err)
}
return servicesWithApplications, nil
}
return services, nil
}
// @id GetKubernetesServicesByNamespace
// @summary Get a list of services for a given namespace
// @description Get a list of services for a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @success 200 {array} models.K8sServiceInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve all services for a namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/services [get]
func (handler *Handler) getKubernetesServicesByNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
}
cli, httpError := handler.getProxyKubeClient(r)
if httpError != nil {
return httpError
}
services, err := cli.GetServices(namespace)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
}
log.Error().Err(err).Str("context", "GetKubernetesServicesByNamespace").Str("namespace", namespace).Msg("Unable to retrieve services from the Kubernetes for a namespace level user")
return httperror.InternalServerError("unable to retrieve services from the Kubernetes for a namespace level user. Error: ", err)
}
return response.JSON(w, services)
}
// @id CreateKubernetesService
// @summary Create a service
// @description Create a service within a given namespace
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sServiceInfo true "Service definition"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to create a service."
// @router /kubernetes/{id}/namespaces/{namespace}/services [post]
func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
}
var payload models.K8sServiceInfo
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
serviceName := payload.Name
cli, httpError := handler.getProxyKubeClient(r)
if httpError != nil {
log.Error().Err(httpError).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpError)
}
err = cli.CreateService(namespace, payload)
if err != nil {
return httperror.InternalServerError("Unable to create sercice", err)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
}
if k8serrors.IsAlreadyExists(err) {
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("A service with the same name already exists in the namespace")
return httperror.Conflict("a service with the same name already exists in the namespace. Error: ", err)
}
log.Error().Err(err).Str("context", "CreateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to create a service")
return httperror.InternalServerError("unable to create a service. Error: ", err)
}
return nil
return response.Empty(w)
}
// @id deleteKubernetesServices
// @summary Delete kubernetes services
// @description Delete the provided list of kubernetes services
// @description **Access policy**: authenticated
// @id DeleteKubernetesServices
// @summary Delete services
// @description Delete the provided list of services.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param body body models.K8sServiceDeleteRequests true "A map where the key is the namespace and the value is an array of services to delete"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find a specific service."
// @failure 500 "Server error occurred while attempting to delete services."
// @router /kubernetes/{id}/services/delete [post]
func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload models.K8sServiceDeleteRequests
payload := models.K8sServiceDeleteRequests{}
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest(
"Invalid request payload",
err,
)
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
cli, httpError := handler.getProxyKubeClient(r)
if httpError != nil {
return httpError
}
err = cli.DeleteServices(payload)
if err != nil {
return httperror.InternalServerError(
"Unable to delete service",
err,
)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to find the services to delete")
return httperror.NotFound("unable to find the services to delete. Error: ", err)
}
log.Error().Err(err).Str("context", "DeleteKubernetesServices").Msg("Unable to delete services")
return httperror.InternalServerError("unable to delete services. Error: ", err)
}
return nil
return response.Empty(w)
}
// @id updateKubernetesService
// @summary Update a kubernetes service
// @description Update a kubernetes service within a given namespace
// @description **Access policy**: authenticated
// @id UpdateKubernetesService
// @summary Update a service
// @description Update a service within a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @security ApiKeyAuth || jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body models.K8sServiceInfo true "Service definition"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the service to update."
// @failure 500 "Server error occurred while attempting to update a service."
// @router /kubernetes/{id}/namespaces/{namespace}/services [put]
func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return httperror.BadRequest("Invalid namespace identifier route variable", err)
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("unable to retrieve namespace identifier route variable. Error: ", err)
}
var payload models.K8sServiceInfo
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return httperror.BadRequest("Invalid request payload", err)
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Msg("Unable to decode and validate the request payload")
return httperror.BadRequest("unable to decode and validate the request payload. Error: ", err)
}
cli, handlerErr := handler.getProxyKubeClient(r)
if handlerErr != nil {
return handlerErr
serviceName := payload.Name
cli, httpError := handler.getProxyKubeClient(r)
if httpError != nil {
log.Error().Err(httpError).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("unable to get a Kubernetes client for the user. Error: ", httpError)
}
err = cli.UpdateService(namespace, payload)
if err != nil {
return httperror.InternalServerError("Unable to update service", err)
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("unauthorized access to the Kubernetes API. Error: ", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to find the service to update")
return httperror.NotFound("unable to find the service to update. Error: ", err)
}
log.Error().Err(err).Str("context", "UpdateKubernetesService").Str("namespace", namespace).Str("service", serviceName).Msg("Unable to update a service")
return httperror.InternalServerError("unable to update a service. Error: ", err)
}
return nil

View File

@@ -20,19 +20,20 @@ func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
// @id KubernetesNamespacesToggleSystem
// @summary Toggle the system state for a namespace
// @description Toggle the system state for a namespace
// @description **Access policy**: administrator or environment(endpoint) admin
// @security ApiKeyAuth
// @security jwt
// @description Toggle the system state for a namespace
// @description **Access policy**: Administrator or environment administrator.
// @security ApiKeyAuth || jwt
// @tags kubernetes
// @accept json
// @param id path int true "Environment(Endpoint) identifier"
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace name"
// @param body body namespacesToggleSystemPayload true "Update details"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @success 204 "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or unable to find the namespace to update."
// @failure 500 "Server error occurred while attempting to update the system state of the namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/system [put]
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
@@ -51,7 +52,7 @@ func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.R
return httperror.BadRequest("Invalid request payload", err)
}
kubeClient, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
kubeClient, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return httperror.InternalServerError("Unable to create kubernetes client", err)
}

View File

@@ -0,0 +1,147 @@
package kubernetes
import (
"net/http"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id GetAllKubernetesVolumes
// @summary Get Kubernetes volumes within the given Portainer environment
// @description Get a list of all kubernetes volumes within the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param withApplications query boolean false "When set to True, include the applications that are using the volumes. It is set to false by default"
// @success 200 {object} map[string]kubernetes.K8sVolumeInfo "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes."
// @router /kubernetes/{id}/volumes [get]
func (handler *Handler) GetAllKubernetesVolumes(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
volumes, err := handler.getKubernetesVolumes(r)
if err != nil {
return err
}
return response.JSON(w, volumes)
}
// @id getAllKubernetesVolumesCount
// @summary Get the total number of kubernetes volumes within the given Portainer environment.
// @description Get the total number of kubernetes volumes within the given environment (Endpoint). The total count depends on the user's role and permissions. The Endpoint ID must be a valid Portainer environment identifier.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @success 200 {integer} integer "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error occurred while attempting to retrieve kubernetes volumes count."
// @router /kubernetes/{id}/volumes/count [get]
func (handler *Handler) getAllKubernetesVolumesCount(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
volumes, err := handler.getKubernetesVolumes(r)
if err != nil {
return err
}
return response.JSON(w, len(volumes))
}
// @id GetKubernetesVolume
// @summary Get a Kubernetes volume within the given Portainer environment
// @description Get a Kubernetes volume within the given environment (Endpoint). The Endpoint ID must be a valid Portainer environment identifier.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "Namespace identifier"
// @param volume path string true "Volume name"
// @success 200 {object} kubernetes.K8sVolumeInfo "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/volumes/{namespace}/{volume} [get]
func (handler *Handler) getKubernetesVolume(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolume").Msg("Unable to retrieve namespace identifier")
return httperror.BadRequest("Invalid namespace identifier", err)
}
volumeName, err := request.RetrieveRouteVariableValue(r, "volume")
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolume").Msg("Unable to retrieve volume name")
return httperror.BadRequest("Invalid volume name", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesVolume").Msg("Unable to get Kubernetes client")
return httperror.InternalServerError("Failed to prepare Kubernetes client", httpErr)
}
volume, err := cli.GetVolume(namespace, volumeName)
if err != nil {
if k8serrors.IsUnauthorized(err) {
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Unauthorized access")
return httperror.Unauthorized("Unauthorized access to volume", err)
}
if k8serrors.IsNotFound(err) {
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Volume not found")
return httperror.NotFound("Volume not found", err)
}
log.Error().Err(err).Str("context", "GetKubernetesVolume").Str("namespace", namespace).Str("volume", volumeName).Msg("Failed to retrieve volume")
return httperror.InternalServerError("Failed to retrieve volume", err)
}
return response.JSON(w, volume)
}
func (handler *Handler) getKubernetesVolumes(r *http.Request) ([]models.K8sVolumeInfo, *httperror.HandlerError) {
withApplications, err := request.RetrieveBooleanQueryParameter(r, "withApplications", true)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Bool("withApplications", withApplications).Msg("Unable to parse query parameter")
return nil, httperror.BadRequest("Invalid 'withApplications' parameter", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesVolumes").Msg("Unable to get Kubernetes client")
return nil, httperror.InternalServerError("Failed to prepare Kubernetes client", httpErr)
}
volumes, err := cli.GetVolumes("")
if err != nil {
if k8serrors.IsUnauthorized(err) {
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Unauthorized access")
return nil, httperror.Unauthorized("Unauthorized access to volumes", err)
}
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Failed to retrieve volumes")
return nil, httperror.InternalServerError("Failed to retrieve volumes", err)
}
if withApplications {
volumesWithApplications, err := cli.CombineVolumesWithApplications(&volumes)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesVolumes").Msg("Failed to combine volumes with applications")
return nil, httperror.InternalServerError("Failed to combine volumes with applications", err)
}
return *volumesWithApplications, nil
}
return volumes, nil
}

View File

@@ -7,12 +7,16 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)
func hideFields(registry *portainer.Registry, hideAccesses bool) {
@@ -83,29 +87,88 @@ func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Re
return hasSameUrl && hasSameCredentials && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath
}
func (handler *Handler) userHasRegistryAccess(r *http.Request) (hasAccess bool, isAdmin bool, err error) {
// this function validates that
//
// 1. user has the appropriate authorizations to perform the request
//
// 2. user has a direct or indirect access to the registry
func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portainer.Registry) (hasAccess bool, isAdmin bool, err error) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return false, false, err
}
user, err := handler.DataStore.User().Read(securityContext.UserID)
if err != nil {
return false, false, err
}
// Portainer admins always have access to everything
if securityContext.IsAdmin {
return true, true, nil
}
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
// mandatory query param that should become a path param
endpointIdStr, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return false, false, err
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
endpointId := portainer.EndpointID(endpointIdStr)
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointId)
if err != nil {
return false, false, err
}
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
// validate that the request is allowed for the user (READ/WRITE authorization on request path)
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); errors.Is(err, security.ErrAuthorizationRequired) {
return false, false, nil
} else if err != nil {
return false, false, err
}
return true, false, nil
memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return false, false, nil
}
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
if endpointutils.IsKubernetesEndpoint(endpoint) {
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
}
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve environment's namespaces policies to validate registry access")
}
authorizedNamespaces := registry.RegistryAccesses[endpointId].Namespaces
for _, namespace := range authorizedNamespaces {
// when the default namespace is authorized to use a registry, all users have the ability to use it
// unless the default namespace is restricted: in this case continue to search for other potential accesses authorizations
if namespace == kubernetes.DefaultNamespace && !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
return true, false, nil
}
namespacePolicy := accessPolicies[namespace]
if security.AuthorizedAccess(user.ID, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies) {
return true, false, nil
}
}
return false, false, nil
}
// validate access for docker environments
// leverage registry.RegistryAccesses[endpointId].UserAccessPolicies (direct access)
// and registry.RegistryAccesses[endpointId].TeamAccessPolicies (indirect access via his teams)
if security.AuthorizedRegistryAccess(registry, user, memberships, endpoint.ID) {
return true, false, nil
}
// when user has no access via their role, direct grant or indirect grant
// then they don't have access to the registry
return false, false, nil
}

View File

@@ -41,7 +41,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
if useAuthentication {
username, err := request.RetrieveMultiPartFormValue(r, "Username", false)
if err != nil {
return errors.New("Invalid username")
return errors.New("invalid username")
}
payload.Username = username
@@ -61,19 +61,19 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
if useTLS && !skipTLSVerify {
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
if err != nil {
return errors.New("Invalid certificate file. Ensure that the file is uploaded correctly")
return errors.New("invalid certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCertFile = cert
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
if err != nil {
return errors.New("Invalid key file. Ensure that the file is uploaded correctly")
return errors.New("invalid key file. Ensure that the file is uploaded correctly")
}
payload.TLSKeyFile = key
ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
if err != nil {
return errors.New("Invalid CA certificate file. Ensure that the file is uploaded correctly")
return errors.New("invalid CA certificate file. Ensure that the file is uploaded correctly")
}
payload.TLSCACertFile = ca
}

View File

@@ -11,8 +11,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type registryCreatePayload struct {
@@ -46,19 +44,19 @@ type registryCreatePayload struct {
}
func (payload *registryCreatePayload) Validate(_ *http.Request) error {
if govalidator.IsNull(payload.Name) {
return errors.New("Invalid registry name")
if len(payload.Name) == 0 {
return errors.New("invalid registry name")
}
if govalidator.IsNull(payload.URL) {
return errors.New("Invalid registry URL")
if len(payload.URL) == 0 {
return errors.New("invalid registry URL")
}
if payload.Authentication {
if govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password) {
return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled")
if len(payload.Username) == 0 || len(payload.Password) == 0 {
return errors.New("invalid credentials. Username and password must be specified when authentication is enabled")
}
if payload.Type == portainer.EcrRegistry {
if govalidator.IsNull(payload.Ecr.Region) {
if len(payload.Ecr.Region) == 0 {
return errors.New("invalid credentials: access key ID, secret access key and region must be specified when authentication is enabled")
}
}
@@ -129,10 +127,10 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
}
for _, r := range registries {
if r.Name == registry.Name {
return httperror.Conflict("Another registry with the same name already exists", errors.New("A registry is already defined with this name"))
return httperror.Conflict("Another registry with the same name already exists", errors.New("a registry is already defined with this name"))
}
if handler.registriesHaveSameURLAndCredentials(&r, registry) {
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials"))
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("a registry is already defined for this URL and credentials"))
}
}

View File

@@ -67,7 +67,7 @@ func (handler *Handler) deleteKubernetesSecrets(registry *portainer.Registry) {
continue
}
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
// Skip environments that can't get a kubeclient from
log.Warn().Err(err).Msgf("Unable to get kubernetes client for environment %d", endpointId)

View File

@@ -26,14 +26,6 @@ import (
// @failure 500 "Server error"
// @router /registries/{id} [get]
func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
hasAccess, isAdmin, err := handler.userHasRegistryAccess(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
if !hasAccess {
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest("Invalid registry identifier route variable", err)
@@ -46,6 +38,14 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to find a registry with the specified identifier inside the database", err)
}
hasAccess, isAdmin, err := handler.userHasRegistryAccess(r, registry)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
if !hasAccess {
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}
hideFields(registry, !isAdmin)
return response.JSON(w, registry)
}

View File

@@ -96,7 +96,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
// See https://portainer.atlassian.net/browse/EE-2706 for more details
for _, r := range registries {
if r.ID != registry.ID && r.Name == registry.Name {
return httperror.Conflict("Another registry with the same name already exists", errors.New("A registry is already defined with this name"))
return httperror.Conflict("Another registry with the same name already exists", errors.New("a registry is already defined with this name"))
}
}
@@ -147,7 +147,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
for _, r := range registries {
if r.ID != registry.ID && handler.registriesHaveSameURLAndCredentials(&r, registry) {
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("A registry is already defined for this URL and credentials"))
return httperror.Conflict("Another registry with the same URL and credentials already exists", errors.New("a registry is already defined for this URL and credentials"))
}
}
}
@@ -193,7 +193,7 @@ func syncConfig(registry *portainer.Registry) *portainer.RegistryManagementConfi
}
func (handler *Handler) updateEndpointRegistryAccess(endpoint *portainer.Endpoint, registry *portainer.Registry, endpointAccess portainer.RegistryAccessPolicies) error {
cli, err := handler.K8sClientFactory.GetKubeClient(endpoint)
cli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return err
}

View File

@@ -8,8 +8,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type resourceControlCreatePayload struct {
@@ -33,7 +31,7 @@ type resourceControlCreatePayload struct {
var errResourceControlAlreadyExists = errors.New("A resource control is already applied on this resource") //http/resourceControl
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ResourceID) {
if len(payload.ResourceID) == 0 {
return errors.New("invalid payload: invalid resource identifier")
}

View File

@@ -32,11 +32,11 @@ type composeStackFromFileContentPayload struct {
}
func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}
return nil
@@ -202,13 +202,13 @@ func createStackPayloadFromComposeGitPayload(name, repoUrl, repoReference, repoU
}
func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {

View File

@@ -88,7 +88,7 @@ func createStackPayloadFromK8sUrlPayload(name, namespace, manifestUrl string, co
}
func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}
@@ -96,15 +96,15 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
}
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ManifestFile) {
if len(payload.ManifestFile) == 0 {
return errors.New("Invalid manifest file in repository")
}
@@ -112,7 +112,7 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
}
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
if len(payload.ManifestURL) == 0 || !govalidator.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL")
}
@@ -163,7 +163,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
// Refresh ECR registry secret if needed
// RefreshEcrSecret method checks if the namespace has any ECR registry
// otherwise return nil
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err == nil {
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, payload.Namespace)
}

View File

@@ -30,13 +30,13 @@ type swarmStackFromFileContentPayload struct {
}
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.SwarmID) {
if len(payload.SwarmID) == 0 {
return errors.New("Invalid Swarm ID")
}
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}
return nil
@@ -136,16 +136,16 @@ type swarmStackFromGitRepositoryPayload struct {
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("Invalid stack name")
}
if govalidator.IsNull(payload.SwarmID) {
if len(payload.SwarmID) == 0 {
return errors.New("Invalid Swarm ID")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
if payload.RepositoryAuthentication && len(payload.RepositoryPassword) == 0 {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {

View File

@@ -1,6 +1,7 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"time"
@@ -95,7 +96,7 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
stack.EndpointID = portainer.EndpointID(endpointID)

View File

@@ -109,7 +109,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
}
if !canManage {
errMsg := "stack deletion is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
// stop scheduler updates of the stack before removal
@@ -307,7 +307,7 @@ func (handler *Handler) stackDeleteKubernetesByName(w http.ResponseWriter, r *ht
}
if !canManage {
errMsg := "stack deletion is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
stacksToDelete = append(stacksToDelete, stack)

View File

@@ -161,7 +161,7 @@ func (handler *Handler) startStack(
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries)
}
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{})
case portainer.DockerSwarmStack:
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)

View File

@@ -14,7 +14,6 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -29,7 +28,7 @@ type updateComposeStackPayload struct {
}
func (payload *updateComposeStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}
@@ -48,7 +47,7 @@ type updateSwarmStackPayload struct {
}
func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}

View File

@@ -16,7 +16,6 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -37,7 +36,7 @@ type kubernetesGitStackUpdatePayload struct {
}
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
if len(payload.StackFileContent) == 0 {
return errors.New("Invalid stack file content")
}
@@ -125,7 +124,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
// Refresh ECR registry secret if needed
// RefreshEcrSecret method checks if the namespace has any ECR registry
// otherwise return nil
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err == nil {
registryutils.RefreshEcrSecret(cli, endpoint, handler.DataStore, stack.Namespace)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/upgrade"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux"
@@ -15,22 +16,25 @@ import (
// Handler is the HTTP handler used to handle status operations.
type Handler struct {
*mux.Router
status *portainer.Status
dataStore dataservices.DataStore
upgradeService upgrade.Service
status *portainer.Status
dataStore dataservices.DataStore
upgradeService upgrade.Service
platformService platform.Service
}
// NewHandler creates a handler to manage status operations.
func NewHandler(bouncer security.BouncerService,
status *portainer.Status,
dataStore dataservices.DataStore,
platformService platform.Service,
upgradeService upgrade.Service) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dataStore: dataStore,
status: status,
upgradeService: upgradeService,
Router: mux.NewRouter(),
dataStore: dataStore,
status: status,
upgradeService: upgradeService,
platformService: platformService,
}
router := h.PathPrefix("/system").Subrouter()

View File

@@ -42,12 +42,11 @@ func (handler *Handler) systemInfo(w http.ResponseWriter, r *http.Request) *http
if endpointutils.IsEdgeEndpoint(&environment) {
edgeAgents++
}
}
platform, err := plf.DetermineContainerPlatform()
platform, err := handler.platformService.GetPlatform()
if err != nil {
return httperror.InternalServerError("Unable to determine container platform", err)
return httperror.InternalServerError("Failed to get platform", err)
}
return response.JSON(w, &systemInfoResponse{

View File

@@ -4,8 +4,6 @@ import (
"net/http"
"regexp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/platform"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -31,12 +29,6 @@ func (payload *systemUpgradePayload) Validate(r *http.Request) error {
return nil
}
var platformToEndpointType = map[platform.ContainerPlatform]portainer.EndpointType{
platform.PlatformDockerStandalone: portainer.DockerEnvironment,
platform.PlatformDockerSwarm: portainer.DockerEnvironment,
platform.PlatformKubernetes: portainer.KubernetesLocalEnvironment,
}
// @id systemUpgrade
// @summary Upgrade Portainer to BE
// @description Upgrade Portainer to BE
@@ -51,40 +43,20 @@ func (handler *Handler) systemUpgrade(w http.ResponseWriter, r *http.Request) *h
return httperror.BadRequest("Invalid request payload", err)
}
environment, err := handler.guessLocalEndpoint()
environment, err := handler.platformService.GetLocalEnvironment()
if err != nil {
return httperror.InternalServerError("Failed to guess local endpoint", err)
return httperror.InternalServerError("Failed to get local environment", err)
}
err = handler.upgradeService.Upgrade(environment, payload.License)
platform, err := handler.platformService.GetPlatform()
if err != nil {
return httperror.InternalServerError("Failed to get platform", err)
}
err = handler.upgradeService.Upgrade(platform, environment, payload.License)
if err != nil {
return httperror.InternalServerError("Failed to upgrade Portainer", err)
}
return response.Empty(w)
}
func (handler *Handler) guessLocalEndpoint() (*portainer.Endpoint, error) {
platform, err := platform.DetermineContainerPlatform()
if err != nil {
return nil, errors.Wrap(err, "failed to determine container platform")
}
endpointType, ok := platformToEndpointType[platform]
if !ok {
return nil, errors.New("failed to determine endpoint type")
}
endpoints, err := handler.dataStore.Endpoint().Endpoints()
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve endpoints")
}
for _, endpoint := range endpoints {
if endpoint.Type == endpointType {
return &endpoint, nil
}
}
return nil, errors.New("failed to find local endpoint")
}

View File

@@ -11,7 +11,7 @@ import (
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/coreos/go-semver/semver"
"github.com/Masterminds/semver/v3"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -119,7 +119,7 @@ func HasNewerVersion(currentVersion, latestVersion string) bool {
return false
}
return currentVersionSemver.LessThan(*latestVersionSemver)
return currentVersionSemver.LessThan(latestVersionSemver)
}
// @id Version

View File

@@ -18,6 +18,60 @@ import (
"github.com/stretchr/testify/assert"
)
func TestHasNewerVersion(t *testing.T) {
// Test cases
tests := []struct {
name string
currentVersion string
latestVersion string
expected bool
}{
{
name: "current version is less than latest version",
currentVersion: "2.22.0",
latestVersion: "v2.22.1",
expected: true,
},
{
name: "current version is equal to latest version",
currentVersion: "2.22.0",
latestVersion: "v2.22.0",
expected: false,
},
{
name: "current version is greater than latest version",
currentVersion: "v2.22.2",
latestVersion: "v2.22.1",
expected: false,
},
{
name: "invalid current version",
currentVersion: "invalid",
latestVersion: "v2.22.0",
expected: false,
},
{
name: "invalid latest version",
currentVersion: "2.22.0",
latestVersion: "invalid",
expected: false,
},
{
name: "both versions are invalid",
currentVersion: "invalid",
latestVersion: "invalid",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HasNewerVersion(tt.currentVersion, tt.latestVersion)
assert.Equal(t, tt.expected, result)
})
}
}
func Test_getSystemVersion(t *testing.T) {
is := assert.New(t)
@@ -39,7 +93,7 @@ func Test_getSystemVersion(t *testing.T) {
apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User())
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
h := NewHandler(requestBouncer, &portainer.Status{}, store, nil)
h := NewHandler(requestBouncer, &portainer.Status{}, store, nil, nil)
// generate standard and admin user tokens
jwt, _, _ := jwtService.GenerateToken(&portainer.TokenData{ID: adminUser.ID, Username: adminUser.Username, Role: adminUser.Role})

View File

@@ -8,8 +8,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/asaskevich/govalidator"
)
type tagCreatePayload struct {
@@ -17,7 +15,7 @@ type tagCreatePayload struct {
}
func (payload *tagCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("invalid tag name")
}

View File

@@ -47,7 +47,7 @@ func (handler *Handler) updateUserServiceAccounts(membership *portainer.TeamMemb
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
// update kubernenets service accounts if the team is associated with a kubernetes environment
if endpointutils.IsKubernetesEndpoint(&endpoint) {
kubecli, err := handler.K8sClientFactory.GetKubeClient(&endpoint)
kubecli, err := handler.K8sClientFactory.GetPrivilegedKubeClient(&endpoint)
if err != nil {
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
continue

View File

@@ -5,11 +5,10 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type teamCreatePayload struct {
@@ -20,9 +19,10 @@ type teamCreatePayload struct {
}
func (payload *teamCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
if len(payload.Name) == 0 {
return errors.New("Invalid team name")
}
return nil
}
@@ -43,26 +43,42 @@ func (payload *teamCreatePayload) Validate(r *http.Request) error {
// @router /teams [post]
func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload teamCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid request payload", err)
}
team, err := handler.DataStore.Team().TeamByName(payload.Name)
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return httperror.InternalServerError("Unable to retrieve teams from the database", err)
var team *portainer.Team
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
var err error
team, err = createTeam(tx, payload)
return err
}); err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
}
return httperror.InternalServerError("Unexpected error", err)
}
return response.JSON(w, team)
}
func createTeam(tx dataservices.DataStoreTx, payload teamCreatePayload) (*portainer.Team, error) {
team, err := tx.Team().TeamByName(payload.Name)
if err != nil && !tx.IsErrObjectNotFound(err) {
return nil, httperror.InternalServerError("Unable to retrieve teams from the database", err)
}
if team != nil {
return httperror.Conflict("A team with the same name already exists", errors.New("Team already exists"))
return nil, httperror.Conflict("A team with the same name already exists", errors.New("Team already exists"))
}
team = &portainer.Team{
Name: payload.Name,
}
team = &portainer.Team{Name: payload.Name}
err = handler.DataStore.Team().Create(team)
if err != nil {
return httperror.InternalServerError("Unable to persist the team inside the database", err)
if err := tx.Team().Create(team); err != nil {
return nil, httperror.InternalServerError("Unable to persist the team inside the database", err)
}
for _, teamLeader := range payload.TeamLeaders {
@@ -72,11 +88,10 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http
Role: portainer.TeamLeader,
}
err = handler.DataStore.TeamMembership().Create(membership)
if err != nil {
return httperror.InternalServerError("Unable to persist team leadership inside the database", err)
if err := tx.TeamMembership().Create(membership); err != nil {
return nil, httperror.InternalServerError("Unable to persist team leadership inside the database", err)
}
}
return response.JSON(w, team)
return team, nil
}

View File

@@ -0,0 +1,65 @@
package teams
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestConcurrentTeamCreation(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, false)
h := &Handler{
DataStore: store,
}
tcp := teamCreatePayload{
Name: "portainer",
}
m, err := json.Marshal(tcp)
require.NoError(t, err)
errGroup := &errgroup.Group{}
n := 100
for range n {
errGroup.Go(func() error {
req, err := http.NewRequest(http.MethodPost, "/teams", bytes.NewReader(m))
if err != nil {
return err
}
if err := h.teamCreate(httptest.NewRecorder(), req); err != nil {
return err
}
return nil
})
}
err = errGroup.Wait()
require.Error(t, err)
teams, err := store.Team().ReadAll()
require.NotEmpty(t, teams)
require.NoError(t, err)
teamCreated := false
for _, team := range teams {
if team.Name == tcp.Name {
require.False(t, teamCreated)
teamCreated = true
}
}
require.True(t, teamCreated)
}

View File

@@ -8,8 +8,6 @@ import (
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
"github.com/asaskevich/govalidator"
)
type filePayload struct {
@@ -20,11 +18,11 @@ type filePayload struct {
}
func (payload *filePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryURL) {
if len(payload.RepositoryURL) == 0 {
return errors.New("Invalid repository url")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
if len(payload.ComposeFilePathInRepository) == 0 {
return errors.New("Invalid file path")
}

View File

@@ -3,13 +3,12 @@ package users
import (
"errors"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type adminInitPayload struct {
@@ -20,10 +19,10 @@ type adminInitPayload struct {
}
func (payload *adminInitPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") {
if len(payload.Username) == 0 || strings.Contains(payload.Username, " ") {
return errors.New("Invalid username. Must not contain any whitespace")
}
if govalidator.IsNull(payload.Password) {
if len(payload.Password) == 0 {
return errors.New("Invalid password")
}
return nil

View File

@@ -3,13 +3,13 @@ package users
import (
"errors"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/asaskevich/govalidator"
)
type userCreatePayload struct {
@@ -20,7 +20,7 @@ type userCreatePayload struct {
}
func (payload *userCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") {
if len(payload.Username) == 0 || strings.Contains(payload.Username, " ") {
return errors.New("Invalid username. Must not contain any whitespace")
}
@@ -54,12 +54,33 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
return httperror.BadRequest("Invalid request payload", err)
}
user, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return httperror.InternalServerError("Unable to retrieve users from the database", err)
var user *portainer.User
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
var err error
user, err = handler.createUser(tx, payload)
return err
}); err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
}
return httperror.InternalServerError("Unexpected error", err)
}
return response.JSON(w, user)
}
func (handler *Handler) createUser(tx dataservices.DataStoreTx, payload userCreatePayload) (*portainer.User, error) {
user, err := tx.User().UserByUsername(payload.Username)
if err != nil && !tx.IsErrObjectNotFound(err) {
return nil, httperror.InternalServerError("Unable to retrieve users from the database", err)
}
if user != nil {
return httperror.Conflict("Another user with the same username already exists", errUserAlreadyExists)
return nil, httperror.Conflict("Another user with the same username already exists", errUserAlreadyExists)
}
user = &portainer.User{
@@ -67,33 +88,33 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
Role: portainer.UserRole(payload.Role),
}
settings, err := handler.DataStore.Settings().Settings()
settings, err := tx.Settings().Settings()
if err != nil {
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
return nil, httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
// when ldap/oauth is on, can only add users without password
// When LDAP/OAuth is on, can only add users without password
if (settings.AuthenticationMethod == portainer.AuthenticationLDAP || settings.AuthenticationMethod == portainer.AuthenticationOAuth) && payload.Password != "" {
errMsg := "A user with password can not be created when authentication method is Oauth or LDAP"
return httperror.BadRequest(errMsg, errors.New(errMsg))
errMsg := "a user with password can not be created when authentication method is Oauth or LDAP"
return nil, httperror.BadRequest(errMsg, errors.New(errMsg))
}
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
if !handler.passwordStrengthChecker.Check(payload.Password) {
return httperror.BadRequest("Password does not meet the requirements", nil)
return nil, httperror.BadRequest("Password does not meet the requirements", nil)
}
user.Password, err = handler.CryptoService.Hash(payload.Password)
if err != nil {
return httperror.InternalServerError("Unable to hash user password", errCryptoHashFailure)
return nil, httperror.InternalServerError("Unable to hash user password", errCryptoHashFailure)
}
}
if err := handler.DataStore.User().Create(user); err != nil {
return httperror.InternalServerError("Unable to persist user inside the database", err)
if err := tx.User().Create(user); err != nil {
return nil, httperror.InternalServerError("Unable to persist user inside the database", err)
}
hideFields(user)
return response.JSON(w, user)
return user, nil
}

View File

@@ -21,7 +21,7 @@ type userAccessTokenCreatePayload struct {
}
func (payload *userAccessTokenCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Description) {
if len(payload.Description) == 0 {
return errors.New("invalid description: cannot be empty")
}
if govalidator.HasWhitespaceOnly(payload.Description) {
@@ -100,7 +100,7 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
if internalAuth {
// Internal auth requires the password field and must not be empty
if govalidator.IsNull(payload.Password) {
if len(payload.Password) == 0 {
return httperror.BadRequest("Invalid request payload", errors.New("invalid password: cannot be empty"))
}

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