Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f6f43a25 | |||
| 4ad3d70739 | |||
| e6a1c29655 | |||
| 333dfe1ebf | |||
| c59872553a | |||
| 1a39370f5b | |||
| bc44056815 | |||
| 17c92343e0 | |||
| cd6935b07a | |||
| 47d428f3eb | |||
| 2baae7072f | |||
| 2e9e459aa3 | |||
| 7444e2c1c7 | |||
| d6469eb33d | |||
| a2da6f1827 | |||
| e6508140f8 | |||
| a7127bc74f | |||
| 55aa0c0c5d | |||
| d25de4f459 | |||
| 6d31f4876a | |||
| e6577ca269 | |||
| 08d77b4333 | |||
| 1ead121c9b | |||
| ad19b4a421 | |||
| 6bc52dd39c | |||
| fd2b00bf3b | |||
| cd8c6d1ce0 | |||
| e9fc6d5598 | |||
| 8ed7cd80cb | |||
| 81322664ea | |||
| 458d722d47 | |||
| 3c0d25f3bd | |||
| ca7e4dd66e | |||
| c1316532eb | |||
| d418784346 | |||
| 1061601714 | |||
| 2f3d4a5511 | |||
| 9ea62bda28 | |||
| 94b1d446c0 | |||
| 6c57a00a65 | |||
| 8808531cd5 | |||
| 966fca950b | |||
| e528cff615 | |||
| 1d037f2f1f | |||
| b2d67795b3 | |||
| 959c527be7 | |||
| cc75167437 | |||
| 3114d4b5c5 | |||
| ac293cda1c | |||
| 7b88975bcb | |||
| da4b2e3a56 |
@@ -11,6 +11,8 @@ body:
|
||||
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
|
||||
|
||||
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
|
||||
|
||||
Please note that we only provide support for current versions of Portainer. You can find a list of supported versions in our [lifecycle policy](https://docs.portainer.io/start/lifecycle).
|
||||
|
||||
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
|
||||
|
||||
@@ -90,10 +92,13 @@ body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Portainer version
|
||||
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.
|
||||
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. 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.24.0'
|
||||
- '2.23.0'
|
||||
- '2.22.0'
|
||||
- '2.21.4'
|
||||
- '2.21.3'
|
||||
- '2.21.2'
|
||||
- '2.21.1'
|
||||
@@ -114,9 +119,6 @@ body:
|
||||
- '2.18.1'
|
||||
- '2.17.1'
|
||||
- '2.17.0'
|
||||
- '2.16.2'
|
||||
- '2.16.1'
|
||||
- '2.16.0'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Label Conflicts
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- 'release/**'
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mschilde/auto-label-merge-conflicts@master
|
||||
with:
|
||||
CONFLICT_LABEL_NAME: 'has conflicts'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAX_RETRIES: 10
|
||||
WAIT_MS: 60000
|
||||
@@ -1,19 +0,0 @@
|
||||
name: Automatic Rebase
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Close Stale Issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Issue Config
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: 'status/stale'
|
||||
exempt-all-issue-milestones: true # Do not stale issues in a milestone
|
||||
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
|
||||
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
|
||||
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
|
||||
|
||||
# Pull Request Config
|
||||
days-before-pr-stale: -1 # Do not stale pull request
|
||||
days-before-pr-close: -1 # Do not close pull request
|
||||
@@ -9,7 +9,7 @@ ENV=development
|
||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
TAG=local
|
||||
|
||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
|
||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
|
||||
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||
|
||||
# Don't change anything below this line unless you know what you're doing
|
||||
@@ -64,9 +64,6 @@ 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)
|
||||
|
||||
@@ -75,11 +72,11 @@ test-server: ## Run server tests
|
||||
|
||||
##@ Dev
|
||||
.PHONY: dev dev-client dev-server
|
||||
dev: ## Run both the client and server in development mode
|
||||
dev: ## Run both the client and server in development mode
|
||||
make dev-server
|
||||
make dev-client
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
|
||||
dev-server: build-server ## Run the server in development mode
|
||||
@@ -119,7 +116,7 @@ dev-extension: build-server build-client ## Run the extension in development mod
|
||||
##@ Docs
|
||||
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
||||
docs-build: init-dist ## Build docs
|
||||
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
|
||||
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
|
||||
|
||||
docs-validate: docs-build ## Validate docs
|
||||
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||
|
||||
@@ -49,7 +49,6 @@ import (
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/portainer/portainer/pkg/libhelm"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
@@ -166,26 +165,6 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
|
||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||
}
|
||||
|
||||
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating compose manager")
|
||||
}
|
||||
|
||||
return composeWrapper
|
||||
}
|
||||
|
||||
func initSwarmStackManager(
|
||||
assetsPath string,
|
||||
configPath string,
|
||||
signatureService portainer.DigitalSignatureService,
|
||||
fileService portainer.FileService,
|
||||
reverseTunnelService portainer.ReverseTunnelService,
|
||||
dataStore dataservices.DataStore,
|
||||
) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||
}
|
||||
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||
}
|
||||
@@ -433,14 +412,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing compose deployer")
|
||||
}
|
||||
composeDeployer := compose.NewComposeDeployer()
|
||||
|
||||
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
||||
composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||
swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
|
||||
backup["__metadata"] = meta
|
||||
}
|
||||
|
||||
err = connection.View(func(tx *bolt.Tx) error {
|
||||
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
if err := connection.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
var list []any
|
||||
version := make(map[string]string)
|
||||
@@ -84,27 +84,22 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(list) > 0 {
|
||||
if bucketName == "ssl" ||
|
||||
bucketName == "settings" ||
|
||||
bucketName == "tunnel_server" {
|
||||
backup[bucketName] = nil
|
||||
if len(list) > 0 {
|
||||
backup[bucketName] = list[0]
|
||||
}
|
||||
return nil
|
||||
if bucketName == "ssl" ||
|
||||
bucketName == "settings" ||
|
||||
bucketName == "tunnel_server" {
|
||||
backup[bucketName] = nil
|
||||
if len(list) > 0 {
|
||||
backup[bucketName] = list[0]
|
||||
}
|
||||
backup[bucketName] = list
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
backup[bucketName] = list
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
return []byte("{}"), err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"api_key": null,
|
||||
"customtemplates": null,
|
||||
"dockerhub": [
|
||||
{
|
||||
"Authentication": false,
|
||||
"Username": ""
|
||||
}
|
||||
],
|
||||
"edge_stack": null,
|
||||
"edgegroups": null,
|
||||
"edgejobs": null,
|
||||
"endpoint_groups": [
|
||||
{
|
||||
"AuthorizedTeams": null,
|
||||
@@ -103,6 +108,9 @@
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
],
|
||||
"extension": null,
|
||||
"helm_user_repository": null,
|
||||
"pending_actions": null,
|
||||
"registries": [
|
||||
{
|
||||
"Authentication": true,
|
||||
@@ -602,7 +610,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.23.0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.24.0",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -860,6 +868,8 @@
|
||||
"UpdatedBy": ""
|
||||
}
|
||||
],
|
||||
"tags": null,
|
||||
"team_membership": null,
|
||||
"teams": [
|
||||
{
|
||||
"Id": 1,
|
||||
@@ -932,6 +942,7 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.23.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
}
|
||||
"VERSION": "{\"SchemaVersion\":\"2.24.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null
|
||||
}
|
||||
+6
-3
@@ -31,15 +31,18 @@ type (
|
||||
// RegistryCredentials holds the credentials for a Docker registry.
|
||||
// Used only for EE
|
||||
RegistryCredentials []RegistryCredentials
|
||||
// PrePullImage is a flag indicating if the agent should pull the image before deploying the stack.
|
||||
// PrePullImage is a flag indicating if the agent must pull the image before deploying the stack.
|
||||
// Used only for EE
|
||||
PrePullImage bool
|
||||
// RePullImage is a flag indicating if the agent should pull the image if it is already present on the node.
|
||||
// RePullImage is a flag indicating if the agent must pull the image if it is already present on the node.
|
||||
// Used only for EE
|
||||
RePullImage bool
|
||||
// RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails.
|
||||
// RetryDeploy is a flag indicating if the agent must retry to deploy the stack if it fails.
|
||||
// Used only for EE
|
||||
RetryDeploy bool
|
||||
// RetryPeriod specifies the duration, in seconds, for which the agent should continue attempting to deploy the stack after a failure
|
||||
// Used only for EE
|
||||
RetryPeriod int
|
||||
// EdgeUpdateID is the ID of the edge update related to this stack.
|
||||
// Used only for EE
|
||||
EdgeUpdateID int
|
||||
|
||||
+69
-15
@@ -9,27 +9,32 @@ import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ComposeStackManager is a wrapper for docker-compose binary
|
||||
type ComposeStackManager struct {
|
||||
deployer libstack.Deployer
|
||||
proxyManager *proxy.Manager
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
|
||||
|
||||
// NewComposeStackManager returns a Compose stack manager
|
||||
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
|
||||
return &ComposeStackManager{
|
||||
deployer: deployer,
|
||||
proxyManager: proxyManager,
|
||||
}, nil
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||
@@ -60,6 +65,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
},
|
||||
ForceRecreate: options.ForceRecreate,
|
||||
AbortOnContainerExit: options.AbortOnContainerExit,
|
||||
@@ -90,6 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
},
|
||||
Remove: options.Remove,
|
||||
Args: options.Args,
|
||||
@@ -103,14 +110,15 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
} else if proxy != nil {
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
|
||||
WorkingDir: "",
|
||||
Host: url,
|
||||
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.RemoveOptions{
|
||||
Options: libstack.Options{
|
||||
WorkingDir: "",
|
||||
Host: url,
|
||||
},
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
@@ -118,12 +126,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
|
||||
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
|
||||
// but does not start containers based on those images.
|
||||
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
|
||||
url, proxy, err := manager.fetchEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proxy != nil {
|
||||
} else if proxy != nil {
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
@@ -138,6 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
})
|
||||
return errors.Wrap(err, "failed to pull images of the stack")
|
||||
}
|
||||
@@ -176,16 +184,16 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
|
||||
// Copy from default .env file
|
||||
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
|
||||
if err = copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
|
||||
if err := copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Copy from stack env vars
|
||||
if err = copyConfigEnvVars(envfile, stack.Env); err != nil {
|
||||
if err := copyConfigEnvVars(envfile, stack.Env); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "stack.env", nil
|
||||
return envFilePath, nil
|
||||
}
|
||||
|
||||
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
|
||||
@@ -217,3 +225,49 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
|
||||
var authConfigs []types.AuthConfig
|
||||
|
||||
for _, r := range registries {
|
||||
ac := types.AuthConfig{
|
||||
Username: r.Username,
|
||||
Password: r.Password,
|
||||
ServerAddress: r.URL,
|
||||
}
|
||||
|
||||
if r.Authentication {
|
||||
var err error
|
||||
|
||||
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
authConfigs = append(authConfigs, ac)
|
||||
}
|
||||
|
||||
return authConfigs
|
||||
}
|
||||
|
||||
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
|
||||
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to validate registry token. Skip logging with this registry.")
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(registry)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to get effective credential. Skip logging with this registry.")
|
||||
}
|
||||
|
||||
return username, password, err
|
||||
}
|
||||
|
||||
@@ -42,20 +42,13 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||
}
|
||||
|
||||
func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
testhelpers.IntegrationTest(t)
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
deployer, err := compose.NewComposeDeployer("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deployer := compose.NewComposeDeployer()
|
||||
|
||||
w, err := NewComposeStackManager(deployer, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed creating manager: %s", err)
|
||||
}
|
||||
w := NewComposeStackManager(deployer, nil, nil)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -53,7 +54,7 @@ func Test_createEnvFile(t *testing.T) {
|
||||
result, _ := createEnvFile(tt.stack)
|
||||
|
||||
if tt.expected != "" {
|
||||
assert.Equal(t, "stack.env", result)
|
||||
assert.Equal(t, filepath.Join(tt.stack.ProjectPath, "stack.env"), result)
|
||||
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
content, _ := io.ReadAll(f)
|
||||
@@ -77,7 +78,7 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
|
||||
},
|
||||
}
|
||||
result, err := createEnvFile(stack)
|
||||
assert.Equal(t, "stack.env", result)
|
||||
assert.Equal(t, filepath.Join(stack.ProjectPath, "stack.env"), result)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, path.Join(dir, "stack.env"))
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
|
||||
+9
-29
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -46,8 +45,7 @@ func NewSwarmStackManager(
|
||||
dataStore: datastore,
|
||||
}
|
||||
|
||||
err := manager.updateDockerCLIConfiguration(manager.configPath)
|
||||
if err != nil {
|
||||
if err := manager.updateDockerCLIConfiguration(manager.configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -63,33 +61,14 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
err = registryutils.EnsureRegTokenValid(manager.dataStore, ®istry)
|
||||
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, ®istry)
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to validate registry token. Skip logging with this registry.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(®istry)
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to get effective credential. Skip logging with this registry.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
|
||||
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
if err != nil {
|
||||
log.
|
||||
Warn().
|
||||
if err := runCommandAndCaptureStdErr(command, registryArgs, nil, ""); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to login.")
|
||||
@@ -155,6 +134,7 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
||||
|
||||
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
@@ -167,8 +147,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
@@ -192,6 +171,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
endpointURL = "tcp://" + tunnelAddr
|
||||
}
|
||||
|
||||
@@ -216,6 +196,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
configFilePath := path.Join(configPath, "config.json")
|
||||
|
||||
config, err := manager.retrieveConfigurationFromDisk(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -246,8 +227,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
return make(map[string]any), nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(raw, &config)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(raw, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var edgeStack *portainer.EdgeStack
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeStack, err = handler.createSwarmStack(tx, method, dryrun, tokenData.ID, r)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
switch {
|
||||
case httperrors.IsInvalidPayloadError(err):
|
||||
return httperror.BadRequest("Invalid payload", err)
|
||||
|
||||
@@ -57,17 +57,15 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var payload updateEdgeStackPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
var stack *portainer.EdgeStack
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
stack, err = handler.updateEdgeStack(tx, portainer.EdgeStackID(stackID), payload)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr
|
||||
@@ -122,14 +120,12 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
||||
stack.EdgeGroups = groupsIds
|
||||
|
||||
if payload.UpdateVersion {
|
||||
err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds)
|
||||
if err != nil {
|
||||
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||
}
|
||||
|
||||
@@ -160,8 +156,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
||||
|
||||
delete(relation.EdgeStacks, edgeStackID)
|
||||
|
||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||
}
|
||||
}
|
||||
@@ -181,8 +176,7 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
||||
|
||||
relation.EdgeStacks[edgeStackID] = true
|
||||
|
||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,13 +88,11 @@ func (handler *Handler) updateRelations(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if updateRelations {
|
||||
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment")
|
||||
}
|
||||
|
||||
err = handler.updateEdgeRelations(tx, endpoint)
|
||||
if err != nil {
|
||||
if err := handler.updateEdgeRelations(tx, endpoint); err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment relations")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,16 @@ func (handler *Handler) updateEdgeRelations(tx dataservices.DataStoreTx, endpoin
|
||||
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find environment relation inside the database")
|
||||
if !tx.IsErrObjectNotFound(err) {
|
||||
return errors.WithMessage(err, "Unable to retrieve environment relation inside the database")
|
||||
}
|
||||
|
||||
relation = &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
}
|
||||
if err := tx.EndpointRelation().Create(relation); err != nil {
|
||||
return errors.WithMessage(err, "Unable to create environment relation inside the database")
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroup, err := tx.EndpointGroup().Read(endpoint.GroupID)
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
@@ -16,8 +19,10 @@ type Handler struct {
|
||||
// NewHandler creates a handler to serve static files.
|
||||
func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler {
|
||||
h := &Handler{
|
||||
Handler: handlers.CompressHandler(
|
||||
http.FileServer(http.Dir(assetPublicPath)),
|
||||
Handler: security.MWSecureHeaders(
|
||||
handlers.CompressHandler(http.FileServer(http.Dir(assetPublicPath))),
|
||||
featureflags.IsEnabled("hsts"),
|
||||
featureflags.IsEnabled("csp"),
|
||||
),
|
||||
wasInstanceDisabled: wasInstanceDisabled,
|
||||
}
|
||||
@@ -53,7 +58,5 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.23.0
|
||||
// @version 2.24.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
)
|
||||
@@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWrite
|
||||
|
||||
return response.JSON(w, clusterrolebindings)
|
||||
}
|
||||
|
||||
// @id DeleteClusterRoleBindings
|
||||
// @summary Delete cluster role bindings
|
||||
// @description Delete the provided list of cluster role bindings.
|
||||
// @description **Access policy**: Authenticated user.
|
||||
// @tags kubernetes
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @param id path int true "Environment identifier"
|
||||
// @param payload body models.K8sClusterRoleBindingDeleteRequests true "A list of cluster role bindings to delete"
|
||||
// @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 cluster role binding."
|
||||
// @failure 500 "Server error occurred while attempting to delete cluster role bindings."
|
||||
// @router /kubernetes/{id}/cluster_role_bindings/delete [POST]
|
||||
func (handler *Handler) deleteClusterRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload models.K8sClusterRoleBindingDeleteRequests
|
||||
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.DeleteClusterRoleBindings(payload)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to delete cluster role bindings", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
)
|
||||
@@ -43,3 +45,39 @@ func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *h
|
||||
|
||||
return response.JSON(w, clusterroles)
|
||||
}
|
||||
|
||||
// @id DeleteClusterRoles
|
||||
// @summary Delete cluster roles
|
||||
// @description Delete the provided list of cluster roles.
|
||||
// @description **Access policy**: Authenticated user.
|
||||
// @tags kubernetes
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @param id path int true "Environment identifier"
|
||||
// @param payload body models.K8sClusterRoleDeleteRequests true "A list of cluster roles to delete"
|
||||
// @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 cluster role."
|
||||
// @failure 500 "Server error occurred while attempting to delete cluster roles."
|
||||
// @router /kubernetes/{id}/cluster_roles/delete [POST]
|
||||
func (handler *Handler) deleteClusterRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload models.K8sClusterRoleDeleteRequests
|
||||
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.DeleteClusterRoles(payload)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to delete cluster roles", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||
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_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost)
|
||||
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost)
|
||||
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)
|
||||
@@ -72,15 +74,12 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||
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.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)
|
||||
@@ -89,6 +88,16 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||
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)
|
||||
endpointRouter.Handle("/service_accounts", httperror.LoggerHandler(h.getAllKubernetesServiceAccounts)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/service_accounts/delete", httperror.LoggerHandler(h.deleteKubernetesServiceAccounts)).Methods(http.MethodPost)
|
||||
endpointRouter.Handle("/roles", httperror.LoggerHandler(h.getAllKubernetesRoles)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/roles/delete", httperror.LoggerHandler(h.deleteRoles)).Methods(http.MethodPost)
|
||||
endpointRouter.Handle("/role_bindings", httperror.LoggerHandler(h.getAllKubernetesRoleBindings)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/role_bindings/delete", httperror.LoggerHandler(h.deleteRoleBindings)).Methods(http.MethodPost)
|
||||
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/cluster_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost)
|
||||
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
|
||||
endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost)
|
||||
|
||||
// namespaces
|
||||
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
)
|
||||
@@ -38,3 +40,38 @@ func (handler *Handler) getAllKubernetesRoleBindings(w http.ResponseWriter, r *h
|
||||
|
||||
return response.JSON(w, rolebindings)
|
||||
}
|
||||
|
||||
// @id DeleteRoleBindings
|
||||
// @summary Delete role bindings
|
||||
// @description Delete the provided list of role bindings.
|
||||
// @description **Access policy**: Authenticated user.
|
||||
// @tags kubernetes
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @param id path int true "Environment identifier"
|
||||
// @param payload body models.K8sRoleBindingDeleteRequests true "A map where the key is the namespace and the value is an array of role bindings to delete"
|
||||
// @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 role binding."
|
||||
// @failure 500 "Server error occurred while attempting to delete role bindings."
|
||||
// @router /kubernetes/{id}/role_bindings/delete [POST]
|
||||
func (h *Handler) deleteRoleBindings(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload models.K8sRoleBindingDeleteRequests
|
||||
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
cli, handlerErr := h.getProxyKubeClient(r)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
if err := cli.DeleteRoleBindings(payload); err != nil {
|
||||
return httperror.InternalServerError("Failed to delete role bindings", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
)
|
||||
@@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesRoles(w http.ResponseWriter, r *http.Req
|
||||
|
||||
return response.JSON(w, roles)
|
||||
}
|
||||
|
||||
// @id DeleteRoles
|
||||
// @summary Delete roles
|
||||
// @description Delete the provided list of roles.
|
||||
// @description **Access policy**: Authenticated user.
|
||||
// @tags kubernetes
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @param id path int true "Environment identifier"
|
||||
// @param payload body models.K8sRoleDeleteRequests true "A map where the key is the namespace and the value is an array of roles to delete"
|
||||
// @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 role."
|
||||
// @failure 500 "Server error occurred while attempting to delete roles."
|
||||
// @router /kubernetes/{id}/roles/delete [POST]
|
||||
func (h *Handler) deleteRoles(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload models.K8sRoleDeleteRequests
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
cli, handlerErr := h.getProxyKubeClient(r)
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
err = cli.DeleteRoles(payload)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Failed to delete roles", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ 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"
|
||||
)
|
||||
@@ -38,3 +40,39 @@ func (handler *Handler) getAllKubernetesServiceAccounts(w http.ResponseWriter, r
|
||||
|
||||
return response.JSON(w, serviceAccounts)
|
||||
}
|
||||
|
||||
// @id DeleteServiceAccounts
|
||||
// @summary Delete service accounts
|
||||
// @description Delete the provided list of service accounts.
|
||||
// @description **Access policy**: Authenticated user.
|
||||
// @tags kubernetes
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @param id path int true "Environment identifier"
|
||||
// @param payload body models.K8sServiceAccountDeleteRequests true "A map where the key is the namespace and the value is an array of service accounts to delete"
|
||||
// @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 account."
|
||||
// @failure 500 "Server error occurred while attempting to delete service accounts."
|
||||
// @router /kubernetes/{id}/service_accounts/delete [POST]
|
||||
func (handler *Handler) deleteKubernetesServiceAccounts(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload models.K8sServiceAccountDeleteRequests
|
||||
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.DeleteServiceAccounts(payload)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete service accounts", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
}
|
||||
|
||||
var payload stackMigratePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
@@ -79,8 +78,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return httperror.InternalServerError("Unable to find an endpoint with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access endpoint", err)
|
||||
}
|
||||
|
||||
@@ -156,14 +154,12 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
newName := stack.Name
|
||||
stack.Name = oldName
|
||||
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
|
||||
if err != nil {
|
||||
if err := handler.deleteStack(securityContext.UserID, stack, endpoint); err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
stack.Name = newName
|
||||
err = handler.DataStore.Stack().Update(stack.ID, stack)
|
||||
if err != nil {
|
||||
if err := handler.DataStore.Stack().Update(stack.ID, stack); err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||
}
|
||||
|
||||
@@ -210,10 +206,10 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
|
||||
}
|
||||
|
||||
// Deploy the stack
|
||||
err = composeDeploymentConfig.Deploy()
|
||||
if err != nil {
|
||||
if err := composeDeploymentConfig.Deploy(); err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -237,8 +233,7 @@ func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stac
|
||||
}
|
||||
|
||||
// Deploy the stack
|
||||
err = swarmDeploymentConfig.Deploy()
|
||||
if err != nil {
|
||||
if err := swarmDeploymentConfig.Deploy(); err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
|
||||
@@ -197,17 +197,14 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, pul
|
||||
|
||||
switch stack.Type {
|
||||
case portainer.DockerSwarmStack:
|
||||
prune := false
|
||||
if stack.Option != nil {
|
||||
prune = stack.Option.Prune
|
||||
}
|
||||
|
||||
// Create swarm deployment config
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
prune := stack.Option != nil && stack.Option.Prune
|
||||
|
||||
deploymentConfiger, err = deployments.CreateSwarmStackDeploymentConfig(securityContext, stack, endpoint, handler.DataStore, handler.FileService, handler.StackDeployer, prune, pullImage)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/ws"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
@@ -76,14 +74,6 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("unable to retrieve user details from authentication token")
|
||||
return err
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
@@ -98,14 +88,13 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
defer websocketConn.Close()
|
||||
|
||||
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
|
||||
return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID)
|
||||
}
|
||||
|
||||
func hijackAttachStartOperation(
|
||||
websocketConn *websocket.Conn,
|
||||
endpoint *portainer.Endpoint,
|
||||
attachID string,
|
||||
token string,
|
||||
) error {
|
||||
conn, err := initDial(endpoint)
|
||||
if err != nil {
|
||||
@@ -127,7 +116,7 @@ func hijackAttachStartOperation(
|
||||
return err
|
||||
}
|
||||
|
||||
return hijackRequest(websocketConn, conn, attachStartRequest, token)
|
||||
return ws.HijackRequest(websocketConn, conn, attachStartRequest)
|
||||
}
|
||||
|
||||
func createAttachStartRequest(attachID string) (*http.Request, error) {
|
||||
|
||||
@@ -5,13 +5,12 @@ import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/ws"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
@@ -79,14 +78,6 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
|
||||
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("unable to retrieve user details from authentication token")
|
||||
return err
|
||||
}
|
||||
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
@@ -102,14 +93,13 @@ func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request
|
||||
|
||||
defer websocketConn.Close()
|
||||
|
||||
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID, tokenData.Token)
|
||||
return hijackExecStartOperation(websocketConn, params.endpoint, params.ID)
|
||||
}
|
||||
|
||||
func hijackExecStartOperation(
|
||||
websocketConn *websocket.Conn,
|
||||
endpoint *portainer.Endpoint,
|
||||
execID string,
|
||||
token string,
|
||||
) error {
|
||||
conn, err := initDial(endpoint)
|
||||
if err != nil {
|
||||
@@ -121,7 +111,7 @@ func hijackExecStartOperation(
|
||||
return err
|
||||
}
|
||||
|
||||
return hijackRequest(websocketConn, conn, execStartRequest, token)
|
||||
return ws.HijackRequest(websocketConn, conn, execStartRequest)
|
||||
}
|
||||
|
||||
func createExecStartRequest(execID string) (*http.Request, error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/ws"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
@@ -136,8 +137,8 @@ func (handler *Handler) hijackPodExecStartOperation(
|
||||
|
||||
// errorChan is used to propagate errors from the go routines to the caller.
|
||||
errorChan := make(chan error, 1)
|
||||
go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
||||
go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
||||
go ws.StreamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan)
|
||||
go ws.StreamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan)
|
||||
|
||||
// StartExecProcess is a blocking operation which streams IO to/from pod;
|
||||
// this must execute in asynchronously, since the websocketConn could return errors (e.g. client disconnects) before
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const readerBufferSize = 2048
|
||||
|
||||
func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer, errorChan chan error) {
|
||||
for {
|
||||
_, in, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
_, err = writer.Write(in)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
||||
out := make([]byte, readerBufferSize)
|
||||
|
||||
for {
|
||||
n, err := reader.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
processedOutput := validString(string(out[:n]))
|
||||
err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput))
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validString(s string) string {
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
v := make([]rune, 0, len(s))
|
||||
|
||||
for i, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
v = append(v, r)
|
||||
}
|
||||
|
||||
return string(v)
|
||||
}
|
||||
@@ -3,39 +3,41 @@ package kubernetes
|
||||
import (
|
||||
"time"
|
||||
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type K8sApplication struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Image string `json:"Image"`
|
||||
Containers []interface{} `json:"Containers,omitempty"`
|
||||
Services []corev1.Service `json:"Services"`
|
||||
CreationDate time.Time `json:"CreationDate"`
|
||||
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||
StackName string `json:"StackName,omitempty"`
|
||||
ResourcePool string `json:"ResourcePool"`
|
||||
ApplicationType string `json:"ApplicationType"`
|
||||
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||
Status string `json:"Status"`
|
||||
TotalPodsCount int `json:"TotalPodsCount"`
|
||||
RunningPodsCount int `json:"RunningPodsCount"`
|
||||
DeploymentType string `json:"DeploymentType"`
|
||||
Pods []Pod `json:"Pods,omitempty"`
|
||||
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||
Namespace string `json:"Namespace,omitempty"`
|
||||
UID string `json:"Uid,omitempty"`
|
||||
StackID string `json:"StackId,omitempty"`
|
||||
ServiceID string `json:"ServiceId,omitempty"`
|
||||
ServiceName string `json:"ServiceName,omitempty"`
|
||||
ServiceType string `json:"ServiceType,omitempty"`
|
||||
Kind string `json:"Kind,omitempty"`
|
||||
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||
Labels map[string]string `json:"Labels,omitempty"`
|
||||
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Image string `json:"Image"`
|
||||
Containers []interface{} `json:"Containers,omitempty"`
|
||||
Services []corev1.Service `json:"Services"`
|
||||
CreationDate time.Time `json:"CreationDate"`
|
||||
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||
StackName string `json:"StackName,omitempty"`
|
||||
ResourcePool string `json:"ResourcePool"`
|
||||
ApplicationType string `json:"ApplicationType"`
|
||||
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||
Status string `json:"Status"`
|
||||
TotalPodsCount int `json:"TotalPodsCount"`
|
||||
RunningPodsCount int `json:"RunningPodsCount"`
|
||||
DeploymentType string `json:"DeploymentType"`
|
||||
Pods []Pod `json:"Pods,omitempty"`
|
||||
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||
Namespace string `json:"Namespace,omitempty"`
|
||||
UID string `json:"Uid,omitempty"`
|
||||
StackID string `json:"StackId,omitempty"`
|
||||
ServiceID string `json:"ServiceId,omitempty"`
|
||||
ServiceName string `json:"ServiceName,omitempty"`
|
||||
ServiceType string `json:"ServiceType,omitempty"`
|
||||
Kind string `json:"Kind,omitempty"`
|
||||
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||
Labels map[string]string `json:"Labels,omitempty"`
|
||||
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||
HorizontalPodAutoscaler *autoscalingv2.HorizontalPodAutoscaler `json:"HorizontalPodAutoscaler,omitempty"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
K8sClusterRoleBinding struct {
|
||||
Name string `json:"name"`
|
||||
UID types.UID `json:"uid"`
|
||||
Namespace string `json:"namespace"`
|
||||
RoleRef rbacv1.RoleRef `json:"roleRef"`
|
||||
Subjects []rbacv1.Subject `json:"subjects"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
IsSystem bool `json:"isSystem"`
|
||||
}
|
||||
|
||||
// K8sRoleBindingDeleteRequests slice of cluster role cluster bindings.
|
||||
K8sClusterRoleBindingDeleteRequests []string
|
||||
)
|
||||
|
||||
func (r K8sClusterRoleBindingDeleteRequests) Validate(request *http.Request) error {
|
||||
if len(r) == 0 {
|
||||
return errors.New("missing deletion request list in payload")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
package kubernetes
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
type K8sClusterRole struct {
|
||||
Name string `json:"name"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
K8sClusterRole struct {
|
||||
Name string `json:"name"`
|
||||
UID types.UID `json:"uid"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
IsSystem bool `json:"isSystem"`
|
||||
}
|
||||
|
||||
K8sClusterRoleDeleteRequests []string
|
||||
)
|
||||
|
||||
func (r K8sClusterRoleDeleteRequests) Validate(request *http.Request) error {
|
||||
if len(r) == 0 {
|
||||
return errors.New("missing deletion request list in payload")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
K8sRoleBinding struct {
|
||||
Name string `json:"name"`
|
||||
UID types.UID `json:"uid"`
|
||||
Namespace string `json:"namespace"`
|
||||
RoleRef rbacv1.RoleRef `json:"roleRef"`
|
||||
Subjects []rbacv1.Subject `json:"subjects"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
IsSystem bool `json:"isSystem"`
|
||||
}
|
||||
|
||||
// K8sRoleBindingDeleteRequests is a mapping of namespace names to a slice of role bindings.
|
||||
K8sRoleBindingDeleteRequests map[string][]string
|
||||
)
|
||||
|
||||
func (r K8sRoleBindingDeleteRequests) Validate(request *http.Request) error {
|
||||
if len(r) == 0 {
|
||||
return errors.New("missing deletion request list in payload")
|
||||
}
|
||||
|
||||
for ns := range r {
|
||||
if len(ns) == 0 {
|
||||
return errors.New("deletion given with empty namespace")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
package kubernetes
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
type K8sRole struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
K8sRole struct {
|
||||
Name string `json:"name"`
|
||||
UID types.UID `json:"uid"`
|
||||
Namespace string `json:"namespace"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
// isSystem is true if prefixed with "system:" or exists in the kube-system namespace
|
||||
// or is one of the portainer roles
|
||||
IsSystem bool `json:"isSystem"`
|
||||
}
|
||||
|
||||
// K8sRoleDeleteRequests is a mapping of namespace names to a slice of roles.
|
||||
K8sRoleDeleteRequests map[string][]string
|
||||
)
|
||||
|
||||
func (r K8sRoleDeleteRequests) Validate(request *http.Request) error {
|
||||
if len(r) == 0 {
|
||||
return errors.New("missing deletion request list in payload")
|
||||
}
|
||||
for ns := range r {
|
||||
if len(ns) == 0 {
|
||||
return errors.New("deletion given with empty namespace")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
package kubernetes
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
type K8sServiceAccount struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
K8sServiceAccount struct {
|
||||
Name string `json:"name"`
|
||||
UID types.UID `json:"uid"`
|
||||
Namespace string `json:"namespace"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
IsSystem bool `json:"isSystem"`
|
||||
}
|
||||
|
||||
// K8sServiceAcountDeleteRequests is a mapping of namespace names to a slice of service account names.
|
||||
K8sServiceAccountDeleteRequests map[string][]string
|
||||
)
|
||||
|
||||
func (r K8sServiceAccountDeleteRequests) Validate(request *http.Request) error {
|
||||
if len(r) == 0 {
|
||||
return errors.New("missing deletion request list in payload")
|
||||
}
|
||||
for ns := range r {
|
||||
if len(ns) == 0 {
|
||||
return errors.New("deletion given with empty namespace")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -42,6 +43,8 @@ type (
|
||||
jwtService portainer.JWTService
|
||||
apiKeyService apikey.APIKeyService
|
||||
revokedJWT sync.Map
|
||||
hsts bool
|
||||
csp bool
|
||||
}
|
||||
|
||||
// RestrictedRequestContext is a data structure containing information
|
||||
@@ -68,6 +71,8 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
apiKeyService: apiKeyService,
|
||||
hsts: featureflags.IsEnabled("hsts"),
|
||||
csp: featureflags.IsEnabled("csp"),
|
||||
}
|
||||
|
||||
go b.cleanUpExpiredJWT()
|
||||
@@ -78,7 +83,7 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService portainer.JW
|
||||
// PublicAccess defines a security check for public API endpoints.
|
||||
// No authentication is required to access these endpoints.
|
||||
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||
return mwSecureHeaders(h)
|
||||
return MWSecureHeaders(h, bouncer.hsts, bouncer.csp)
|
||||
}
|
||||
|
||||
// AdminAccess defines a security check for API endpoints that require an authorization check.
|
||||
@@ -211,7 +216,7 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler
|
||||
bouncer.CookieAuthLookup,
|
||||
bouncer.JWTAuthLookup,
|
||||
}, h)
|
||||
h = mwSecureHeaders(h)
|
||||
h = MWSecureHeaders(h, bouncer.hsts, bouncer.csp)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -517,10 +522,17 @@ func extractAPIKey(r *http.Request) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
// MWSecureHeaders provides secure headers middleware for handlers.
|
||||
func MWSecureHeaders(next http.Handler, hsts, csp bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
if hsts {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000") // 365 days
|
||||
}
|
||||
|
||||
if csp {
|
||||
w.Header().Set("Content-Security-Policy", "script-src 'self' cdn.matomo.cloud")
|
||||
}
|
||||
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package errorlist
|
||||
|
||||
import "errors"
|
||||
|
||||
// Combine a slice of errors into a single error
|
||||
// to use this, generate errors by appending to errorList in a loop, then return combine(errorList)
|
||||
func Combine(errorList []error) error {
|
||||
if len(errorList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorMsg := "Multiple errors occurred:"
|
||||
for _, err := range errorList {
|
||||
errorMsg += "\n" + err.Error()
|
||||
}
|
||||
|
||||
return errors.New(errorMsg)
|
||||
}
|
||||
@@ -14,47 +14,51 @@ func isRegTokenValid(registry *portainer.Registry) (valid bool) {
|
||||
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix()
|
||||
}
|
||||
|
||||
func doGetRegToken(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
|
||||
func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
registry.AccessToken = *accessToken
|
||||
registry.AccessTokenExpiry = expiryAt.Unix()
|
||||
|
||||
err = dataStore.Registry().Update(registry.ID, registry)
|
||||
|
||||
return
|
||||
return tx.Registry().Update(registry.ID, registry)
|
||||
}
|
||||
|
||||
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||
return ecrClient.ParseAuthorizationToken(registry.AccessToken)
|
||||
return ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
|
||||
ParseAuthorizationToken(registry.AccessToken)
|
||||
}
|
||||
|
||||
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
|
||||
if registry.Type == portainer.EcrRegistry {
|
||||
if isRegTokenValid(registry) {
|
||||
log.Debug().Msg("current ECR token is still valid")
|
||||
} else {
|
||||
err = doGetRegToken(dataStore, registry)
|
||||
if err != nil {
|
||||
log.Debug().Msg("refresh ECR token")
|
||||
}
|
||||
}
|
||||
func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
if registry.Type != portainer.EcrRegistry {
|
||||
return nil
|
||||
}
|
||||
|
||||
return
|
||||
if isRegTokenValid(registry) {
|
||||
log.Debug().Msg("current ECR token is still valid")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := doGetRegToken(tx, registry); err != nil {
|
||||
log.Debug().Msg("refresh ECR token")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRegEffectiveCredential(registry *portainer.Registry) (username, password string, err error) {
|
||||
username = registry.Username
|
||||
password = registry.Password
|
||||
|
||||
if registry.Type == portainer.EcrRegistry {
|
||||
username, password, err = parseRegToken(registry)
|
||||
} else {
|
||||
username = registry.Username
|
||||
password = registry.Password
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
var _ portainer.ComposeStackManager = &composeStackManager{}
|
||||
|
||||
type composeStackManager struct{}
|
||||
|
||||
func NewComposeStackManager() *composeStackManager {
|
||||
@@ -31,6 +33,6 @@ func (manager *composeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
func (manager *composeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
labels "k8s.io/apimachinery/pkg/labels"
|
||||
@@ -31,20 +32,20 @@ func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDepende
|
||||
}
|
||||
if !withDependencies {
|
||||
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, _, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, _, _, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil, nil)
|
||||
}
|
||||
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas)
|
||||
}
|
||||
|
||||
// fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to.
|
||||
@@ -62,20 +63,20 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
|
||||
}
|
||||
|
||||
if !withDependencies {
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil, nil)
|
||||
}
|
||||
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applications, err := kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||
applications, err := kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,7 +93,7 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
|
||||
}
|
||||
|
||||
// convertPodsToApplications processes pods and converts them to applications, ensuring uniqueness by owner reference.
|
||||
func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) ([]models.K8sApplication, error) {
|
||||
func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler) ([]models.K8sApplication, error) {
|
||||
applications := []models.K8sApplication{}
|
||||
processedOwners := make(map[string]struct{})
|
||||
|
||||
@@ -105,7 +106,7 @@ func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets
|
||||
processedOwners[ownerUID] = struct{}{}
|
||||
}
|
||||
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, deployments, statefulSets, daemonSets, services, true)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, deployments, statefulSets, daemonSets, services, hpas, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,7 +134,7 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
podResources := calculateResourceUsage(pod)
|
||||
podResources := calculatePodResourceUsage(pod)
|
||||
resource.CPURequest += podResources.CPURequest
|
||||
resource.CPULimit += podResources.CPULimit
|
||||
resource.MemoryRequest += podResources.MemoryRequest
|
||||
@@ -150,7 +151,7 @@ func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConf
|
||||
for _, pod := range pods {
|
||||
if pod.Namespace == configMap.Namespace {
|
||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -167,7 +168,7 @@ func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, po
|
||||
for _, pod := range pods {
|
||||
if pod.Namespace == secret.Namespace {
|
||||
if isPodUsingSecret(&pod, secret.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -180,18 +181,23 @@ func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, po
|
||||
}
|
||||
|
||||
// ConvertPodToApplication converts a pod to an application, updating owner references if necessary
|
||||
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, withResource bool) (*models.K8sApplication, error) {
|
||||
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler, withResource bool) (*models.K8sApplication, error) {
|
||||
if isReplicaSetOwner(pod) {
|
||||
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
||||
}
|
||||
|
||||
application := createApplication(&pod, deployments, statefulSets, daemonSets, services)
|
||||
application := createApplication(&pod, deployments, statefulSets, daemonSets, services, hpas)
|
||||
if application.ID == "" && application.Name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if withResource {
|
||||
application.Resource = calculateResourceUsage(pod)
|
||||
podResources := calculatePodResourceUsage(pod)
|
||||
// multiply by the number of requested pods in the application (not the running count)
|
||||
application.Resource.CPURequest = podResources.CPURequest * float64(application.TotalPodsCount)
|
||||
application.Resource.CPULimit = podResources.CPULimit * float64(application.TotalPodsCount)
|
||||
application.Resource.MemoryRequest = podResources.MemoryRequest * int64(application.TotalPodsCount)
|
||||
application.Resource.MemoryLimit = podResources.MemoryLimit * int64(application.TotalPodsCount)
|
||||
}
|
||||
|
||||
return &application, nil
|
||||
@@ -199,7 +205,7 @@ func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []app
|
||||
|
||||
// createApplication creates a K8sApplication object from a pod
|
||||
// it sets the application name, namespace, kind, image, stack id, stack name, and labels
|
||||
func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) models.K8sApplication {
|
||||
func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler) models.K8sApplication {
|
||||
kind := "Pod"
|
||||
name := pod.Name
|
||||
|
||||
@@ -319,7 +325,11 @@ func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefu
|
||||
}
|
||||
|
||||
if application.ID != "" && application.Name != "" && len(services) > 0 {
|
||||
return updateApplicationWithService(application, services)
|
||||
updateApplicationWithService(&application, services)
|
||||
}
|
||||
|
||||
if application.ID != "" && application.Name != "" && len(hpas) > 0 {
|
||||
updateApplicationWithHorizontalPodAutoscaler(&application, hpas)
|
||||
}
|
||||
|
||||
return application
|
||||
@@ -327,21 +337,36 @@ func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefu
|
||||
|
||||
// updateApplicationWithService updates the application with the services that match the application's selector match labels
|
||||
// and are in the same namespace as the application
|
||||
func updateApplicationWithService(application models.K8sApplication, services []corev1.Service) models.K8sApplication {
|
||||
func updateApplicationWithService(application *models.K8sApplication, services []corev1.Service) {
|
||||
for _, service := range services {
|
||||
serviceSelector := labels.SelectorFromSet(service.Spec.Selector)
|
||||
|
||||
if service.Namespace == application.ResourcePool && serviceSelector.Matches(labels.Set(application.MatchLabels)) {
|
||||
if service.Namespace == application.ResourcePool && !serviceSelector.Empty() && serviceSelector.Matches(labels.Set(application.MatchLabels)) {
|
||||
application.ServiceType = string(service.Spec.Type)
|
||||
application.Services = append(application.Services, service)
|
||||
}
|
||||
}
|
||||
|
||||
return application
|
||||
}
|
||||
|
||||
// calculateResourceUsage calculates the resource usage for a pod in CPU cores and Bytes
|
||||
func calculateResourceUsage(pod corev1.Pod) models.K8sApplicationResource {
|
||||
func updateApplicationWithHorizontalPodAutoscaler(application *models.K8sApplication, hpas []autoscalingv2.HorizontalPodAutoscaler) {
|
||||
for _, hpa := range hpas {
|
||||
// Check if HPA is in the same namespace as the application
|
||||
if hpa.Namespace != application.ResourcePool {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the scale target ref matches the application
|
||||
scaleTargetRef := hpa.Spec.ScaleTargetRef
|
||||
if scaleTargetRef.Name == application.Name && scaleTargetRef.Kind == application.Kind {
|
||||
hpaCopy := hpa // Create a local copy
|
||||
application.HorizontalPodAutoscaler = &hpaCopy
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculatePodResourceUsage calculates the resource usage for a pod in CPU cores and Bytes
|
||||
func calculatePodResourceUsage(pod corev1.Pod) models.K8sApplicationResource {
|
||||
resource := models.K8sApplicationResource{}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
// CPU cores as a decimal
|
||||
@@ -385,7 +410,7 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromConfigMap(configMap
|
||||
for _, pod := range pods {
|
||||
if pod.Namespace == configMap.Namespace {
|
||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -411,7 +436,7 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromSecret(secret models
|
||||
for _, pod := range pods {
|
||||
if pod.Namespace == secret.Namespace {
|
||||
if isPodUsingSecret(&pod, secret.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/errorlist"
|
||||
"github.com/rs/zerolog/log"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetClusterRoles gets all the clusterRoles for at the cluster level in a k8s endpoint.
|
||||
@@ -21,7 +25,7 @@ func (kcl *KubeClient) GetClusterRoles() ([]models.K8sClusterRole, error) {
|
||||
|
||||
// fetchClusterRoles returns a list of all Roles in the specified namespace.
|
||||
func (kcl *KubeClient) fetchClusterRoles() ([]models.K8sClusterRole, error) {
|
||||
clusterRoles, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{})
|
||||
clusterRoles, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), meta.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,5 +43,61 @@ func parseClusterRole(clusterRole rbacv1.ClusterRole) models.K8sClusterRole {
|
||||
return models.K8sClusterRole{
|
||||
Name: clusterRole.Name,
|
||||
CreationDate: clusterRole.CreationTimestamp.Time,
|
||||
UID: clusterRole.UID,
|
||||
IsSystem: isSystemClusterRole(&clusterRole),
|
||||
}
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) DeleteClusterRoles(req models.K8sClusterRoleDeleteRequests) error {
|
||||
var errors []error
|
||||
for _, name := range req {
|
||||
client := kcl.cli.RbacV1().ClusterRoles()
|
||||
|
||||
clusterRole, err := client.Get(context.Background(), name, meta.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
// this is a more serious error to do with the client so we return right away
|
||||
return err
|
||||
}
|
||||
|
||||
if isSystemClusterRole(clusterRole) {
|
||||
log.Warn().Str("role_name", name).Msg("ignoring delete of 'system' cluster role, not allowed")
|
||||
}
|
||||
|
||||
err = client.Delete(context.Background(), name, meta.DeleteOptions{})
|
||||
if err != nil {
|
||||
log.Err(err).Str("role_name", name).Msg("unable to delete the cluster role")
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errorlist.Combine(errors)
|
||||
}
|
||||
|
||||
func isSystemClusterRole(role *rbacv1.ClusterRole) bool {
|
||||
if role.Namespace == "kube-system" || role.Namespace == "kube-public" ||
|
||||
role.Namespace == "kube-node-lease" || role.Namespace == "portainer" {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(role.Name, "system:") {
|
||||
return true
|
||||
}
|
||||
|
||||
if role.Labels != nil {
|
||||
if role.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
roles := getPortainerDefaultK8sRoleNames()
|
||||
for i := range roles {
|
||||
if role.Name == roles[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/errorlist"
|
||||
"github.com/rs/zerolog/log"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -38,8 +42,70 @@ func (kcl *KubeClient) fetchClusterRoleBindings() ([]models.K8sClusterRoleBindin
|
||||
func parseClusterRoleBinding(clusterRoleBinding rbacv1.ClusterRoleBinding) models.K8sClusterRoleBinding {
|
||||
return models.K8sClusterRoleBinding{
|
||||
Name: clusterRoleBinding.Name,
|
||||
UID: clusterRoleBinding.UID,
|
||||
Namespace: clusterRoleBinding.Namespace,
|
||||
RoleRef: clusterRoleBinding.RoleRef,
|
||||
Subjects: clusterRoleBinding.Subjects,
|
||||
CreationDate: clusterRoleBinding.CreationTimestamp.Time,
|
||||
IsSystem: isSystemClusterRoleBinding(&clusterRoleBinding),
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteClusterRoleBindings processes a K8sClusterRoleBindingDeleteRequest
|
||||
// by deleting each cluster role binding in its given namespace. If deleting a specific cluster role binding
|
||||
// fails, the error is logged and we continue to delete the remaining cluster role bindings.
|
||||
func (kcl *KubeClient) DeleteClusterRoleBindings(reqs models.K8sClusterRoleBindingDeleteRequests) error {
|
||||
var errors []error
|
||||
|
||||
for _, name := range reqs {
|
||||
client := kcl.cli.RbacV1().ClusterRoleBindings()
|
||||
|
||||
clusterRoleBinding, err := client.Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This is a more serious error to do with the client so we return right away
|
||||
return err
|
||||
}
|
||||
|
||||
if isSystemClusterRoleBinding(clusterRoleBinding) {
|
||||
log.Warn().Str("role_name", name).Msg("ignoring delete of 'system' cluster role binding, not allowed")
|
||||
}
|
||||
|
||||
if err := client.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
||||
log.Err(err).Str("role_name", name).Msg("unable to delete the cluster role binding")
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errorlist.Combine(errors)
|
||||
}
|
||||
|
||||
func isSystemClusterRoleBinding(binding *rbacv1.ClusterRoleBinding) bool {
|
||||
if strings.HasPrefix(binding.Name, "system:") {
|
||||
return true
|
||||
}
|
||||
|
||||
if binding.Labels != nil {
|
||||
if binding.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range binding.Subjects {
|
||||
if strings.HasPrefix(sub.Name, "system:") {
|
||||
return true
|
||||
}
|
||||
|
||||
if sub.Namespace == "kube-system" ||
|
||||
sub.Namespace == "kube-public" ||
|
||||
sub.Namespace == "kube-node-lease" ||
|
||||
sub.Namespace == "portainer" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func parseConfigMap(configMap *corev1.ConfigMap, withData bool) models.K8sConfig
|
||||
func (kcl *KubeClient) CombineConfigMapsWithApplications(configMaps []models.K8sConfigMap) ([]models.K8sConfigMap, error) {
|
||||
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
|
||||
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func parseNamespace(namespace *corev1.Namespace) portainer.K8sNamespaceInfo {
|
||||
Status: namespace.Status,
|
||||
CreationDate: namespace.CreationTimestamp.Format(time.RFC3339),
|
||||
NamespaceOwner: namespace.Labels[namespaceOwnerLabel],
|
||||
IsSystem: isSystemNamespace(*namespace),
|
||||
IsSystem: isSystemNamespace(namespace),
|
||||
IsDefault: namespace.Name == defaultNamespace,
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) (*corev1
|
||||
return namespace, nil
|
||||
}
|
||||
|
||||
func isSystemNamespace(namespace corev1.Namespace) bool {
|
||||
func isSystemNamespace(namespace *corev1.Namespace) bool {
|
||||
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
|
||||
if hasSystemLabel {
|
||||
return systemLabelValue == "true"
|
||||
@@ -184,6 +184,15 @@ func isSystemNamespace(namespace corev1.Namespace) bool {
|
||||
return isSystem
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) isSystemNamespace(namespace string) bool {
|
||||
ns, err := kcl.cli.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return isSystemNamespace(ns)
|
||||
}
|
||||
|
||||
// ToggleSystemState will set a namespace as a system namespace, or remove this state
|
||||
// if isSystem is true it will set `systemNamespaceLabel` to "true" and false otherwise
|
||||
// this will skip if namespace is "default" or if the required state is already set
|
||||
@@ -199,7 +208,7 @@ func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) er
|
||||
return errors.Wrap(err, "failed fetching namespace object")
|
||||
}
|
||||
|
||||
if isSystemNamespace(*namespace) == isSystem {
|
||||
if isSystemNamespace(namespace) == isSystem {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ func Test_ToggleSystemState(t *testing.T) {
|
||||
ns, err := kcl.cli.CoreV1().Namespaces().Get(context.Background(), nsName, metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.isSystem, isSystemNamespace(*ns))
|
||||
assert.Equal(t, test.isSystem, isSystemNamespace(ns))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
+17
-11
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -172,24 +173,24 @@ func (kcl *KubeClient) waitForPodStatus(ctx context.Context, phase corev1.PodPha
|
||||
}
|
||||
|
||||
// fetchAllPodsAndReplicaSets fetches all pods and replica sets across the cluster, i.e. all namespaces
|
||||
func (kcl *KubeClient) fetchAllPodsAndReplicaSets(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchAllPodsAndReplicaSets(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, false, false)
|
||||
}
|
||||
|
||||
// fetchAllApplicationsListResources fetches all pods, replica sets, stateful sets, and daemon sets across the cluster, i.e. all namespaces
|
||||
// this is required for the applications list view
|
||||
func (kcl *KubeClient) fetchAllApplicationsListResources(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchAllApplicationsListResources(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, true, true)
|
||||
}
|
||||
|
||||
// fetchResourcesWithOwnerReferences fetches pods and other resources based on owner references
|
||||
func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podListOptions metav1.ListOptions, includeStatefulSets, includeDaemonSets bool) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podListOptions metav1.ListOptions, includeStatefulSets, includeDaemonSets bool) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, nil
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil
|
||||
}
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list pods across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list pods across the cluster: %w", err)
|
||||
}
|
||||
|
||||
// if replicaSet owner reference exists, fetch the replica sets
|
||||
@@ -199,12 +200,12 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
||||
if containsReplicaSetOwnerReference(pods) {
|
||||
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list replica sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list replica sets across the cluster: %w", err)
|
||||
}
|
||||
|
||||
deployments, err = kcl.cli.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list deployments across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list deployments across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +213,7 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
||||
if includeStatefulSets && containsStatefulSetOwnerReference(pods) {
|
||||
statefulSets, err = kcl.cli.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list stateful sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list stateful sets across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,16 +221,21 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
||||
if includeDaemonSets && containsDaemonSetOwnerReference(pods) {
|
||||
daemonSets, err = kcl.cli.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list daemon sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list daemon sets across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
services, err := kcl.cli.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list services across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list services across the cluster: %w", err)
|
||||
}
|
||||
|
||||
return pods.Items, replicaSets.Items, deployments.Items, statefulSets.Items, daemonSets.Items, services.Items, nil
|
||||
hpas, err := kcl.cli.AutoscalingV2().HorizontalPodAutoscalers(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list horizontal pod autoscalers across the cluster: %w", err)
|
||||
}
|
||||
|
||||
return pods.Items, replicaSets.Items, deployments.Items, statefulSets.Items, daemonSets.Items, services.Items, hpas.Items, nil
|
||||
}
|
||||
|
||||
// isPodUsingConfigMap checks if a pod is using a specific ConfigMap
|
||||
|
||||
+188
-5
@@ -2,17 +2,200 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/randomstring"
|
||||
"github.com/rs/zerolog/log"
|
||||
authv1 "k8s.io/api/authorization/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
authv1types "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
corev1types "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
rbacv1types "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
||||
)
|
||||
|
||||
// IsRBACEnabled checks if RBAC is enabled in the current Kubernetes cluster by listing cluster roles.
|
||||
// if the cluster roles can be listed, RBAC is enabled.
|
||||
// otherwise, RBAC is not enabled.
|
||||
const maxRetries = 5
|
||||
|
||||
// IsRBACEnabled checks if RBAC is enabled in the cluster by creating a service account, then checking it's access to a resourcequota before and after setting a cluster role and cluster role binding
|
||||
func (kcl *KubeClient) IsRBACEnabled() (bool, error) {
|
||||
_, err := kcl.cli.RbacV1().ClusterRoles().List(context.TODO(), metav1.ListOptions{})
|
||||
namespace := "default"
|
||||
verb := "list"
|
||||
resource := "resourcequotas"
|
||||
|
||||
saClient := kcl.cli.CoreV1().ServiceAccounts(namespace)
|
||||
uniqueString := randomstring.RandomString(4) // Append a unique string to resource names, in case they already exist
|
||||
saName := "portainer-rbac-test-sa-" + uniqueString
|
||||
if err := createServiceAccount(saClient, saName, namespace); err != nil {
|
||||
log.Error().Err(err).Msg("Error creating service account")
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer deleteServiceAccount(saClient, saName)
|
||||
|
||||
accessReviewClient := kcl.cli.AuthorizationV1().LocalSubjectAccessReviews(namespace)
|
||||
allowed, err := checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error checking service account access")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If the service account with no authorizations is allowed, RBAC must be disabled
|
||||
if allowed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
|
||||
// Otherwise give the service account an rbac authorisation and check again
|
||||
roleClient := kcl.cli.RbacV1().Roles(namespace)
|
||||
roleName := "portainer-rbac-test-role-" + uniqueString
|
||||
if err := createRole(roleClient, roleName, verb, resource, namespace); err != nil {
|
||||
log.Error().Err(err).Msg("Error creating role")
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer deleteRole(roleClient, roleName)
|
||||
|
||||
roleBindingClient := kcl.cli.RbacV1().RoleBindings(namespace)
|
||||
roleBindingName := "portainer-rbac-test-role-binding-" + uniqueString
|
||||
if err := createRoleBinding(roleBindingClient, roleBindingName, roleName, saName, namespace); err != nil {
|
||||
log.Error().Err(err).Msg("Error creating role binding")
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer deleteRoleBinding(roleBindingClient, roleBindingName)
|
||||
|
||||
allowed, err = checkServiceAccountAccess(accessReviewClient, saName, verb, resource, namespace)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error checking service account access with authorizations added")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If the service account allowed to list resource quotas after given rbac role, then RBAC is enabled
|
||||
return allowed, nil
|
||||
}
|
||||
|
||||
func createServiceAccount(saClient corev1types.ServiceAccountInterface, name string, namespace string) error {
|
||||
serviceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := saClient.Create(context.Background(), serviceAccount, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteServiceAccount(saClient corev1types.ServiceAccountInterface, name string) {
|
||||
if err := saClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
||||
log.Error().Err(err).Msg("Error deleting service account: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
func createRole(roleClient rbacv1types.RoleInterface, name string, verb string, resource string, namespace string) error {
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Verbs: []string{verb},
|
||||
Resources: []string{resource},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := roleClient.Create(context.Background(), role, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteRole(roleClient rbacv1types.RoleInterface, name string) {
|
||||
if err := roleClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
||||
log.Error().Err(err).Msg("Error deleting role: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
func createRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, clusterRoleBindingName string, roleName string, serviceAccountName string, namespace string) error {
|
||||
clusterRoleBinding := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleBindingName,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: serviceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
Kind: "Role",
|
||||
Name: roleName,
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
},
|
||||
}
|
||||
|
||||
roleBinding, err := roleBindingClient.Create(context.Background(), clusterRoleBinding, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error creating role binding: " + clusterRoleBindingName)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Retry checkRoleBinding a maximum of 5 times with a 100ms wait after each attempt
|
||||
for range maxRetries {
|
||||
err = checkRoleBinding(roleBindingClient, roleBinding.Name)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Wait for 100ms, even if the check passes
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func checkRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, name string) error {
|
||||
if _, err := roleBindingClient.Get(context.Background(), name, metav1.GetOptions{}); err != nil {
|
||||
log.Error().Err(err).Msg("Error finding rolebinding: " + name)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteRoleBinding(roleBindingClient rbacv1types.RoleBindingInterface, name string) {
|
||||
if err := roleBindingClient.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil {
|
||||
log.Error().Err(err).Msg("Error deleting role binding: " + name)
|
||||
}
|
||||
}
|
||||
|
||||
func checkServiceAccountAccess(accessReviewClient authv1types.LocalSubjectAccessReviewInterface, serviceAccountName string, verb string, resource string, namespace string) (bool, error) {
|
||||
subjectAccessReview := &authv1.LocalSubjectAccessReview{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: authv1.SubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authv1.ResourceAttributes{
|
||||
Namespace: namespace,
|
||||
Verb: verb,
|
||||
Resource: resource,
|
||||
},
|
||||
User: "system:serviceaccount:default:" + serviceAccountName, // a workaround to be able to use the service account as a user
|
||||
},
|
||||
}
|
||||
|
||||
result, err := accessReviewClient.Create(context.Background(), subjectAccessReview, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return result.Status.Allowed, nil
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/errorlist"
|
||||
"github.com/rs/zerolog/log"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetRoles gets all the roles for either at the cluster level or a given namespace in a k8s endpoint.
|
||||
@@ -48,18 +52,20 @@ func (kcl *KubeClient) fetchRoles(namespace string) ([]models.K8sRole, error) {
|
||||
|
||||
results := make([]models.K8sRole, 0)
|
||||
for _, role := range roles.Items {
|
||||
results = append(results, parseRole(role))
|
||||
results = append(results, kcl.parseRole(role))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// parseRole converts a rbacv1.Role object to a models.K8sRole object.
|
||||
func parseRole(role rbacv1.Role) models.K8sRole {
|
||||
func (kcl *KubeClient) parseRole(role rbacv1.Role) models.K8sRole {
|
||||
return models.K8sRole{
|
||||
Name: role.Name,
|
||||
UID: role.UID,
|
||||
Namespace: role.Namespace,
|
||||
CreationDate: role.CreationTimestamp.Time,
|
||||
IsSystem: kcl.isSystemRole(&role),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,3 +114,48 @@ func (kcl *KubeClient) upsertPortainerK8sClusterRoles() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPortainerDefaultK8sRoleNames() []string {
|
||||
return []string{
|
||||
string(portainerUserCRName),
|
||||
}
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) isSystemRole(role *rbacv1.Role) bool {
|
||||
if strings.HasPrefix(role.Name, "system:") {
|
||||
return true
|
||||
}
|
||||
|
||||
return kcl.isSystemNamespace(role.Namespace)
|
||||
}
|
||||
|
||||
// DeleteRoles processes a K8sServiceDeleteRequest by deleting each role
|
||||
// in its given namespace.
|
||||
func (kcl *KubeClient) DeleteRoles(reqs models.K8sRoleDeleteRequests) error {
|
||||
var errors []error
|
||||
for namespace := range reqs {
|
||||
for _, name := range reqs[namespace] {
|
||||
client := kcl.cli.RbacV1().Roles(namespace)
|
||||
|
||||
role, err := client.Get(context.Background(), name, v1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This is a more serious error to do with the client so we return right away
|
||||
return err
|
||||
}
|
||||
|
||||
if kcl.isSystemRole(role) {
|
||||
log.Error().Str("role_name", name).Msg("ignoring delete of 'system' role, not allowed")
|
||||
}
|
||||
|
||||
if err := client.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorlist.Combine(errors)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/errorlist"
|
||||
"github.com/rs/zerolog/log"
|
||||
corev1 "k8s.io/api/rbac/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetRoleBindings gets all the roleBindings for either at the cluster level or a given namespace in a k8s endpoint.
|
||||
@@ -47,19 +53,82 @@ func (kcl *KubeClient) fetchRoleBindings(namespace string) ([]models.K8sRoleBind
|
||||
|
||||
results := make([]models.K8sRoleBinding, 0)
|
||||
for _, roleBinding := range roleBindings.Items {
|
||||
results = append(results, parseRoleBinding(roleBinding))
|
||||
results = append(results, kcl.parseRoleBinding(roleBinding))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// parseRoleBinding converts a rbacv1.RoleBinding object to a models.K8sRoleBinding object.
|
||||
func parseRoleBinding(roleBinding rbacv1.RoleBinding) models.K8sRoleBinding {
|
||||
func (kcl *KubeClient) parseRoleBinding(roleBinding rbacv1.RoleBinding) models.K8sRoleBinding {
|
||||
return models.K8sRoleBinding{
|
||||
Name: roleBinding.Name,
|
||||
UID: roleBinding.UID,
|
||||
Namespace: roleBinding.Namespace,
|
||||
RoleRef: roleBinding.RoleRef,
|
||||
Subjects: roleBinding.Subjects,
|
||||
CreationDate: roleBinding.CreationTimestamp.Time,
|
||||
IsSystem: kcl.isSystemRoleBinding(&roleBinding),
|
||||
}
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) isSystemRoleBinding(rb *rbacv1.RoleBinding) bool {
|
||||
if strings.HasPrefix(rb.Name, "system:") {
|
||||
return true
|
||||
}
|
||||
|
||||
if rb.Labels != nil {
|
||||
if rb.Labels["kubernetes.io/bootstrapping"] == "rbac-defaults" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if rb.RoleRef.Name != "" {
|
||||
role, err := kcl.getRole(rb.Namespace, rb.RoleRef.Name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Linked to a role that is marked a system role
|
||||
if kcl.isSystemRole(role) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) getRole(namespace, name string) (*corev1.Role, error) {
|
||||
client := kcl.cli.RbacV1().Roles(namespace)
|
||||
return client.Get(context.Background(), name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// DeleteRoleBindings processes a K8sServiceDeleteRequest by deleting each service
|
||||
// in its given namespace.
|
||||
func (kcl *KubeClient) DeleteRoleBindings(reqs models.K8sRoleBindingDeleteRequests) error {
|
||||
var errors []error
|
||||
for namespace := range reqs {
|
||||
for _, name := range reqs[namespace] {
|
||||
client := kcl.cli.RbacV1().RoleBindings(namespace)
|
||||
|
||||
roleBinding, err := client.Get(context.Background(), name, v1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This is a more serious error to do with the client so we return right away
|
||||
return err
|
||||
}
|
||||
|
||||
if kcl.isSystemRoleBinding(roleBinding) {
|
||||
log.Error().Str("role_name", name).Msg("ignoring delete of 'system' role binding, not allowed")
|
||||
}
|
||||
|
||||
if err := client.Delete(context.Background(), name, v1.DeleteOptions{}); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorlist.Combine(errors)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func parseSecret(secret *corev1.Secret, withData bool) models.K8sSecret {
|
||||
func (kcl *KubeClient) CombineSecretsWithApplications(secrets []models.K8sSecret) ([]models.K8sSecret, error) {
|
||||
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
||||
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInf
|
||||
func (kcl *KubeClient) CombineServicesWithApplications(services []models.K8sServiceInfo) ([]models.K8sServiceInfo, error) {
|
||||
if containsServiceWithSelector(services) {
|
||||
updatedServices := make([]models.K8sServiceInfo, len(services))
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/errorlist"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -50,18 +53,20 @@ func (kcl *KubeClient) fetchServiceAccounts(namespace string) ([]models.K8sServi
|
||||
|
||||
results := make([]models.K8sServiceAccount, 0)
|
||||
for _, serviceAccount := range serviceAccounts.Items {
|
||||
results = append(results, parseServiceAccount(serviceAccount))
|
||||
results = append(results, kcl.parseServiceAccount(serviceAccount))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// parseServiceAccount converts a corev1.ServiceAccount object to a models.K8sServiceAccount object.
|
||||
func parseServiceAccount(serviceAccount corev1.ServiceAccount) models.K8sServiceAccount {
|
||||
func (kcl *KubeClient) parseServiceAccount(serviceAccount corev1.ServiceAccount) models.K8sServiceAccount {
|
||||
return models.K8sServiceAccount{
|
||||
Name: serviceAccount.Name,
|
||||
UID: serviceAccount.UID,
|
||||
Namespace: serviceAccount.Namespace,
|
||||
CreationDate: serviceAccount.CreationTimestamp.Time,
|
||||
IsSystem: kcl.isSystemServiceAccount(serviceAccount.Namespace),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +86,40 @@ func (kcl *KubeClient) GetPortainerUserServiceAccount(tokenData *portainer.Token
|
||||
return serviceAccount, nil
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) isSystemServiceAccount(namespace string) bool {
|
||||
return kcl.isSystemNamespace(namespace)
|
||||
}
|
||||
|
||||
// DeleteServices processes a K8sServiceDeleteRequest by deleting each service
|
||||
// in its given namespace.
|
||||
func (kcl *KubeClient) DeleteServiceAccounts(reqs kubernetes.K8sServiceAccountDeleteRequests) error {
|
||||
var errors []error
|
||||
for namespace := range reqs {
|
||||
for _, serviceName := range reqs[namespace] {
|
||||
client := kcl.cli.CoreV1().ServiceAccounts(namespace)
|
||||
|
||||
sa, err := client.Get(context.Background(), serviceName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if kcl.isSystemServiceAccount(sa.Namespace) {
|
||||
return fmt.Errorf("cannot delete system service account %q", namespace+"/"+serviceName)
|
||||
}
|
||||
|
||||
if err := client.Delete(context.Background(), serviceName, metav1.DeleteOptions{}); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorlist.Combine(errors)
|
||||
}
|
||||
|
||||
// GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user.
|
||||
func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) {
|
||||
serviceAccountName := UserServiceAccountName(userID, kcl.instanceID)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -264,7 +265,7 @@ func (kcl *KubeClient) updateVolumesWithOwningApplications(volumes *[]models.K8s
|
||||
if pod.Spec.Volumes != nil {
|
||||
for _, podVolume := range pod.Spec.Volumes {
|
||||
if podVolume.VolumeSource.PersistentVolumeClaim != nil && podVolume.VolumeSource.PersistentVolumeClaim.ClaimName == volume.PersistentVolumeClaim.Name && pod.Namespace == volume.PersistentVolumeClaim.Namespace {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, deploymentItems, statefulSetItems, daemonSetItems, []corev1.Service{}, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, deploymentItems, statefulSetItems, daemonSetItems, []corev1.Service{}, []autoscalingv2.HorizontalPodAutoscaler{}, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to convert pod to application")
|
||||
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to convert pod to application. Error: %w", err)
|
||||
|
||||
+27
-36
@@ -3,10 +3,12 @@ package oauth
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"maps"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
@@ -29,28 +31,28 @@ func NewService() *Service {
|
||||
// On success, it will then return the username and token expiry time associated to authenticated user by fetching this information
|
||||
// from the resource server and matching it with the user identifier setting.
|
||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
token, err := getOAuthToken(code, configuration)
|
||||
token, err := GetOAuthToken(code, configuration)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed retrieving oauth token")
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
idToken, err := getIdToken(token)
|
||||
idToken, err := GetIdToken(token)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed parsing id_token")
|
||||
}
|
||||
|
||||
resource, err := getResource(token.AccessToken, configuration)
|
||||
resource, err := GetResource(token.AccessToken, configuration.ResourceURI)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed retrieving resource")
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
resource = mergeSecondIntoFirst(idToken, resource)
|
||||
maps.Copy(idToken, resource)
|
||||
|
||||
username, err := getUsername(resource, configuration)
|
||||
username, err := GetUsername(resource, configuration.UserIdentifier)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed retrieving username")
|
||||
|
||||
@@ -60,34 +62,24 @@ func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings
|
||||
return username, nil
|
||||
}
|
||||
|
||||
// mergeSecondIntoFirst merges the overlap map into the base overwriting any existing values.
|
||||
func mergeSecondIntoFirst(base map[string]any, overlap map[string]any) map[string]any {
|
||||
for k, v := range overlap {
|
||||
base[k] = v
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
||||
func GetOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
||||
unescapedCode, err := url.QueryUnescape(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := buildConfig(configuration)
|
||||
token, err := config.Exchange(context.Background(), unescapedCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
return config.Exchange(ctx, unescapedCode)
|
||||
}
|
||||
|
||||
// getIdToken retrieves parsed id_token from the OAuth token response.
|
||||
// GetIdToken retrieves parsed id_token from the OAuth token response.
|
||||
// This is necessary for OAuth providers like Azure
|
||||
// that do not provide information about user groups on the user resource endpoint.
|
||||
func getIdToken(token *oauth2.Token) (map[string]any, error) {
|
||||
func GetIdToken(token *oauth2.Token) (map[string]any, error) {
|
||||
tokenData := make(map[string]any)
|
||||
|
||||
idToken := token.Extra("id_token")
|
||||
@@ -113,8 +105,8 @@ func getIdToken(token *oauth2.Token) (map[string]any, error) {
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
func getResource(token string, configuration *portainer.OAuthSettings) (map[string]any, error) {
|
||||
req, err := http.NewRequest("GET", configuration.ResourceURI, nil)
|
||||
func GetResource(token string, resourceURI string) (map[string]any, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, resourceURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -159,6 +151,7 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
||||
datamap[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return datamap, nil
|
||||
}
|
||||
|
||||
@@ -170,18 +163,16 @@ func getResource(token string, configuration *portainer.OAuthSettings) (map[stri
|
||||
return datamap, nil
|
||||
}
|
||||
|
||||
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
|
||||
endpoint := oauth2.Endpoint{
|
||||
AuthURL: configuration.AuthorizationURI,
|
||||
TokenURL: configuration.AccessTokenURI,
|
||||
AuthStyle: configuration.AuthStyle,
|
||||
}
|
||||
|
||||
func buildConfig(config *portainer.OAuthSettings) *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: configuration.ClientID,
|
||||
ClientSecret: configuration.ClientSecret,
|
||||
Endpoint: endpoint,
|
||||
RedirectURL: configuration.RedirectURI,
|
||||
Scopes: strings.Split(configuration.Scopes, ","),
|
||||
ClientID: config.ClientID,
|
||||
ClientSecret: config.ClientSecret,
|
||||
RedirectURL: config.RedirectURI,
|
||||
Scopes: strings.Split(config.Scopes, ","),
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: config.AuthorizationURI,
|
||||
TokenURL: config.AccessTokenURI,
|
||||
AuthStyle: config.AuthStyle,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@ package oauth
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func getUsername(datamap map[string]any, configuration *portainer.OAuthSettings) (string, error) {
|
||||
username, ok := datamap[configuration.UserIdentifier].(string)
|
||||
func GetUsername(datamap map[string]any, userIdentifier string) (string, error) {
|
||||
username, ok := datamap[userIdentifier].(string)
|
||||
if ok && username != "" {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
username, ok := datamap[configuration.UserIdentifier].(float64)
|
||||
username, ok := datamap[userIdentifier].(float64)
|
||||
if ok && username != 0 {
|
||||
return strconv.Itoa(int(username)), nil
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"name": "john"}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err == nil {
|
||||
t.Errorf("getUsername should fail if user identifier doesn't exist as key in oauth userinfo object")
|
||||
}
|
||||
})
|
||||
@@ -21,8 +20,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": ""}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is empty string")
|
||||
}
|
||||
})
|
||||
@@ -31,8 +29,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": 0}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is 0 val int")
|
||||
}
|
||||
})
|
||||
@@ -41,8 +38,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": -1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is -1 (negative) int")
|
||||
}
|
||||
})
|
||||
@@ -51,8 +47,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": "john"}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err != nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err != nil {
|
||||
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-empty")
|
||||
}
|
||||
})
|
||||
@@ -62,8 +57,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": 1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object matched is positive int")
|
||||
}
|
||||
})
|
||||
@@ -72,8 +66,7 @@ func Test_getUsername(t *testing.T) {
|
||||
oauthSettings := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]any{"username": 1.1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err != nil {
|
||||
if _, err := GetUsername(datamap, oauthSettings.UserIdentifier); err != nil {
|
||||
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-zero (or negative)")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/oauth/oauthtest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
@@ -16,14 +17,14 @@ func Test_getOAuthToken(t *testing.T) {
|
||||
|
||||
t.Run("getOAuthToken fails upon invalid code", func(t *testing.T) {
|
||||
code := ""
|
||||
if _, err := getOAuthToken(code, config); err == nil {
|
||||
if _, err := GetOAuthToken(code, config); err == nil {
|
||||
t.Errorf("getOAuthToken should fail upon providing invalid code; code=%v", code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("getOAuthToken succeeds upon providing valid code", func(t *testing.T) {
|
||||
code := validCode
|
||||
token, err := getOAuthToken(code, config)
|
||||
token, err := GetOAuthToken(code, config)
|
||||
|
||||
if token == nil || err != nil {
|
||||
t.Errorf("getOAuthToken should successfully return access token upon providing valid code")
|
||||
@@ -78,7 +79,7 @@ func Test_getIdToken(t *testing.T) {
|
||||
token = token.WithExtra(map[string]any{"id_token": tc.idToken})
|
||||
}
|
||||
|
||||
result, err := getIdToken(token)
|
||||
result, err := GetIdToken(token)
|
||||
assert.Equal(t, err, tc.expectedError)
|
||||
assert.Equal(t, result, tc.expectedResult)
|
||||
})
|
||||
@@ -90,19 +91,19 @@ func Test_getResource(t *testing.T) {
|
||||
defer srv.Close()
|
||||
|
||||
t.Run("should fail upon missing Authorization Bearer header", func(t *testing.T) {
|
||||
if _, err := getResource("", config); err == nil {
|
||||
if _, err := GetResource("", config.ResourceURI); err == nil {
|
||||
t.Errorf("getResource should fail if access token is not provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail upon providing incorrect Authorization Bearer header", func(t *testing.T) {
|
||||
if _, err := getResource("incorrect-token", config); err == nil {
|
||||
if _, err := GetResource("incorrect-token", config.ResourceURI); err == nil {
|
||||
t.Errorf("getResource should fail if incorrect access token provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should succeed upon providing correct Authorization Bearer header", func(t *testing.T) {
|
||||
if _, err := getResource(oauthtest.AccessToken, config); err != nil {
|
||||
if _, err := GetResource(oauthtest.AccessToken, config.ResourceURI); err != nil {
|
||||
t.Errorf("getResource should succeed if correct access token provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
|
||||
+49
-3
@@ -2,6 +2,7 @@ package portainer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -1366,7 +1367,13 @@ type (
|
||||
ValidateFlags(flags *CLIFlags) error
|
||||
}
|
||||
|
||||
ComposeOptions struct {
|
||||
Registries []Registry
|
||||
}
|
||||
|
||||
ComposeUpOptions struct {
|
||||
ComposeOptions
|
||||
|
||||
// ForceRecreate forces to recreate containers
|
||||
ForceRecreate bool
|
||||
// AbortOnContainerExit will stop the deployment if a container exits.
|
||||
@@ -1374,9 +1381,12 @@ type (
|
||||
//
|
||||
// When this is set, docker compose will output its logs to stdout
|
||||
AbortOnContainerExit bool
|
||||
Prune bool
|
||||
}
|
||||
|
||||
ComposeRunOptions struct {
|
||||
ComposeOptions
|
||||
|
||||
// Remove will remove the container after it has stopped
|
||||
Remove bool
|
||||
// Args are the arguments to pass to the container
|
||||
@@ -1392,7 +1402,7 @@ type (
|
||||
Run(ctx context.Context, stack *Stack, endpoint *Endpoint, serviceName string, options ComposeRunOptions) error
|
||||
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeUpOptions) error
|
||||
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
||||
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error
|
||||
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint, options ComposeOptions) error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data
|
||||
@@ -1498,6 +1508,8 @@ type (
|
||||
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
|
||||
IsRBACEnabled() (bool, error)
|
||||
GetPortainerUserServiceAccount(tokendata *TokenData) (*corev1.ServiceAccount, error)
|
||||
GetServiceAccounts(namespace string) ([]models.K8sServiceAccount, error)
|
||||
DeleteServiceAccounts(reqs models.K8sServiceAccountDeleteRequests) error
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
|
||||
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
|
||||
@@ -1531,6 +1543,16 @@ type (
|
||||
CreateRegistrySecret(registry *Registry, namespace string) error
|
||||
IsRegistrySecret(namespace, secretName string) (bool, error)
|
||||
ToggleSystemState(namespace string, isSystem bool) error
|
||||
|
||||
GetClusterRoles() ([]models.K8sClusterRole, error)
|
||||
DeleteClusterRoles(models.K8sClusterRoleDeleteRequests) error
|
||||
GetClusterRoleBindings() ([]models.K8sClusterRoleBinding, error)
|
||||
DeleteClusterRoleBindings(models.K8sClusterRoleBindingDeleteRequests) error
|
||||
|
||||
GetRoles(namespace string) ([]models.K8sRole, error)
|
||||
DeleteRoles(models.K8sRoleDeleteRequests) error
|
||||
GetRoleBindings(namespace string) ([]models.K8sRoleBinding, error)
|
||||
DeleteRoleBindings(models.K8sRoleBindingDeleteRequests) error
|
||||
}
|
||||
|
||||
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
|
||||
@@ -1595,7 +1617,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.23.0"
|
||||
APIVersion = "2.24.0"
|
||||
// Edition is what this edition of Portainer is called
|
||||
Edition = PortainerCE
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
@@ -1646,7 +1668,7 @@ const (
|
||||
)
|
||||
|
||||
// List of supported features
|
||||
var SupportedFeatureFlags = []featureflags.Feature{}
|
||||
var SupportedFeatureFlags = []featureflags.Feature{"hsts", "csp"}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
@@ -1722,6 +1744,30 @@ const (
|
||||
EdgeStackStatusCompleted
|
||||
)
|
||||
|
||||
var edgeStackStatusTypeStr = map[EdgeStackStatusType]string{
|
||||
EdgeStackStatusPending: "Pending",
|
||||
EdgeStackStatusDeploymentReceived: "DeploymentReceived",
|
||||
EdgeStackStatusError: "Error",
|
||||
EdgeStackStatusAcknowledged: "Acknowledged",
|
||||
EdgeStackStatusRemoved: "Removed",
|
||||
EdgeStackStatusRemoteUpdateSuccess: "RemoteUpdateSuccess",
|
||||
EdgeStackStatusImagesPulled: "ImagesPulled",
|
||||
EdgeStackStatusRunning: "Running",
|
||||
EdgeStackStatusDeploying: "Deploying",
|
||||
EdgeStackStatusRemoving: "Removing",
|
||||
EdgeStackStatusPausedDeploying: "PausedDeploying",
|
||||
EdgeStackStatusRollingBack: "RollingBack",
|
||||
EdgeStackStatusRolledBack: "RolledBack",
|
||||
EdgeStackStatusCompleted: "Completed",
|
||||
}
|
||||
|
||||
func (s EdgeStackStatusType) String() string {
|
||||
if str, ok := edgeStackStatusTypeStr[s]; ok {
|
||||
return fmt.Sprintf("%d (%s)", s, str)
|
||||
}
|
||||
return fmt.Sprintf("%d (UNKNOWN)", s)
|
||||
}
|
||||
|
||||
const (
|
||||
_ EndpointStatus = iota
|
||||
// EndpointStatusUp is used to represent an available environment(endpoint)
|
||||
|
||||
@@ -76,11 +76,11 @@ vJUUCFYm8+9p6gTVOcoMit+eGSwa81PCPEs1TnU1PV/PaDFeUhn/mg==
|
||||
type noopDeployer struct{}
|
||||
|
||||
// without unpacker
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error {
|
||||
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage, forceRecreate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (s *noopDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *p
|
||||
}
|
||||
|
||||
// with unpacker
|
||||
func (s *noopDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error {
|
||||
func (s *noopDeployer) DeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage, forceRecreate bool) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) UndeployRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
@@ -101,7 +101,7 @@ func (s *noopDeployer) StartRemoteComposeStack(stack *portainer.Stack, endpoint
|
||||
func (s *noopDeployer) StopRemoteComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (s *noopDeployer) DeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
||||
return nil
|
||||
}
|
||||
func (s *noopDeployer) UndeployRemoteSwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
|
||||
@@ -4,17 +4,17 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
k "github.com/portainer/portainer/api/kubernetes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BaseStackDeployer interface {
|
||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
|
||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error
|
||||
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error
|
||||
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage, forceRecreate bool) error
|
||||
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStac
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
|
||||
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune, pullImage bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
@@ -54,28 +54,29 @@ func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *porta
|
||||
return d.swarmStackManager.Deploy(stack, prune, pullImage, endpoint)
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRecreate bool) error {
|
||||
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage, forceRecreate bool) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
options := portainer.ComposeOptions{Registries: registries}
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
if err != nil {
|
||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
||||
ForceRecreate: forceRecreate,
|
||||
})
|
||||
if err != nil {
|
||||
if err := d.composeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{
|
||||
ComposeOptions: options,
|
||||
ForceRecreate: forceRecreate,
|
||||
}); err != nil {
|
||||
d.composeStackManager.Down(context.TODO(), stack, endpoint)
|
||||
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
|
||||
@@ -99,8 +100,7 @@ func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *
|
||||
return errors.Wrap(err, "failed to create temp kub deployment files")
|
||||
}
|
||||
|
||||
err = k8sDeploymentConfig.Deploy()
|
||||
if err != nil {
|
||||
if err := k8sDeploymentConfig.Deploy(); err != nil {
|
||||
return errors.Wrap(err, "failed to deploy kubernetes application")
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,7 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
|
||||
if err != nil {
|
||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, portainer.ComposeOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package deployments
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"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/stacks/stackutils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ComposeStackDeploymentConfig struct {
|
||||
@@ -25,7 +25,6 @@ type ComposeStackDeploymentConfig struct {
|
||||
}
|
||||
|
||||
func CreateComposeStackDeploymentConfig(securityContext *security.RestrictedRequestContext, stack *portainer.Stack, endpoint *portainer.Endpoint, dataStore dataservices.DataStore, fileService portainer.FileService, deployer StackDeployer, forcePullImage, forceCreate bool) (*ComposeStackDeploymentConfig, error) {
|
||||
|
||||
user, err := dataStore.User().Read(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load user information from the database: %w", err)
|
||||
@@ -81,11 +80,11 @@ func (config *ComposeStackDeploymentConfig) Deploy() error {
|
||||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
err = stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService)
|
||||
if err != nil {
|
||||
if err := stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if stackutils.IsRelativePathStack(config.stack) {
|
||||
return config.StackDeployer.DeployRemoteComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package websocket
|
||||
package ws
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -16,18 +16,13 @@ import (
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer
|
||||
writeWait = 10 * time.Second
|
||||
WriteWait = 10 * time.Second
|
||||
|
||||
// Send pings to peer with this period
|
||||
pingPeriod = 50 * time.Second
|
||||
PingPeriod = 50 * time.Second
|
||||
)
|
||||
|
||||
func hijackRequest(
|
||||
websocketConn *websocket.Conn,
|
||||
conn net.Conn,
|
||||
request *http.Request,
|
||||
token string,
|
||||
) error {
|
||||
func HijackRequest(websocketConn *websocket.Conn, conn net.Conn, request *http.Request) error {
|
||||
resp, err := sendHTTPRequest(conn, request)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -39,17 +34,21 @@ func hijackRequest(
|
||||
return fmt.Errorf("unexpected response status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
errorChan := make(chan error, 1)
|
||||
go readWebSocketToTCP(websocketConn, conn, errorChan)
|
||||
go writeTCPToWebSocket(websocketConn, conn, errorChan)
|
||||
go StreamFromWebsocketToWriter(websocketConn, conn, errorChan)
|
||||
go WriteReaderToWebSocket(websocketConn, &mu, conn, errorChan)
|
||||
|
||||
err = <-errorChan
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
log.Debug().Msgf("Unexpected close error: %v\n", err)
|
||||
log.Debug().Err(err).Msg("unexpected close error")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Msgf("session ended")
|
||||
log.Info().Msg("session ended")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,60 +68,40 @@ func sendHTTPRequest(conn net.Conn, req *http.Request) (*http.Response, error) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func readWebSocketToTCP(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) {
|
||||
for {
|
||||
messageType, p, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Debug().Msgf("Unexpected close error: %v\n", err)
|
||||
}
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if messageType == websocket.TextMessage || messageType == websocket.BinaryMessage {
|
||||
_, err := tcpConn.Write(p)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Error writing to TCP connection: %v\n", err)
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeTCPToWebSocket(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) {
|
||||
var mu sync.Mutex
|
||||
out := make([]byte, readerBufferSize)
|
||||
func WriteReaderToWebSocket(websocketConn *websocket.Conn, mu *sync.Mutex, reader io.Reader, errorChan chan error) {
|
||||
out := make([]byte, ReaderBufferSize)
|
||||
input := make(chan string)
|
||||
pingTicker := time.NewTicker(pingPeriod)
|
||||
pingTicker := time.NewTicker(PingPeriod)
|
||||
defer pingTicker.Stop()
|
||||
defer websocketConn.Close()
|
||||
|
||||
websocketConn.SetReadLimit(2048)
|
||||
mu.Lock()
|
||||
websocketConn.SetReadLimit(ReaderBufferSize)
|
||||
websocketConn.SetPongHandler(func(string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
websocketConn.SetPingHandler(func(data string) error {
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(WriteWait))
|
||||
|
||||
return websocketConn.WriteMessage(websocket.PongMessage, []byte(data))
|
||||
})
|
||||
|
||||
reader := bufio.NewReader(tcpConn)
|
||||
mu.Unlock()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
n, err := reader.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
if !errors.Is(err, io.EOF) {
|
||||
log.Debug().Msgf("error reading from server: %v", err)
|
||||
log.Debug().Err(err).Msg("error reading from server")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
processedOutput := validString(string(out[:n]))
|
||||
processedOutput := ValidString(string(out[:n]))
|
||||
input <- processedOutput
|
||||
}
|
||||
}()
|
||||
@@ -130,34 +109,37 @@ func writeTCPToWebSocket(websocketConn *websocket.Conn, tcpConn net.Conn, errorC
|
||||
for {
|
||||
select {
|
||||
case msg := <-input:
|
||||
err := wswrite(websocketConn, &mu, msg)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("error writing to websocket: %v", err)
|
||||
if err := wsWrite(websocketConn, mu, msg); err != nil {
|
||||
log.Debug().Err(err).Msg("error writing to websocket")
|
||||
errorChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
if err := wsping(websocketConn, &mu); err != nil {
|
||||
log.Debug().Msgf("error writing to websocket during pong response: %v", err)
|
||||
if err := wsPing(websocketConn, mu); err != nil {
|
||||
log.Debug().Err(err).Msg("error writing to websocket during pong response")
|
||||
errorChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func wswrite(websocketConn *websocket.Conn, mu *sync.Mutex, msg string) error {
|
||||
func wsWrite(websocketConn *websocket.Conn, mu *sync.Mutex, msg string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(WriteWait))
|
||||
|
||||
return websocketConn.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
}
|
||||
|
||||
func wsping(websocketConn *websocket.Conn, mu *sync.Mutex) error {
|
||||
func wsPing(websocketConn *websocket.Conn, mu *sync.Mutex) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
websocketConn.SetWriteDeadline(time.Now().Add(WriteWait))
|
||||
|
||||
return websocketConn.WriteMessage(websocket.PingMessage, nil)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const ReaderBufferSize = 2048
|
||||
|
||||
func StreamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer, errorChan chan error) {
|
||||
for {
|
||||
messageType, in, err := websocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Debug().Err(err).Msg("unexpected close error")
|
||||
}
|
||||
errorChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if messageType != websocket.TextMessage && messageType != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := writer.Write(in); err != nil {
|
||||
log.Debug().Err(err).Msg("writing error")
|
||||
errorChan <- err
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StreamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) {
|
||||
out := make([]byte, ReaderBufferSize)
|
||||
|
||||
for {
|
||||
n, err := reader.Read(out)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
processedOutput := ValidString(string(out[:n]))
|
||||
if err := websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput)); err != nil {
|
||||
errorChan <- err
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ValidString(s string) string {
|
||||
if utf8.ValidString(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
v := make([]rune, 0, len(s))
|
||||
|
||||
for i, r := range s {
|
||||
if r == utf8.RuneError {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
v = append(v, r)
|
||||
}
|
||||
|
||||
return string(v)
|
||||
}
|
||||
@@ -144,12 +144,7 @@ div.input-mask {
|
||||
}
|
||||
.widget .widget-footer {
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.widget .widget-footer {
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
.widget .widget-title .pagination,
|
||||
.widget .widget-footer .pagination {
|
||||
|
||||
@@ -32,6 +32,34 @@
|
||||
border-top: 1px solid var(--border-table-top-color);
|
||||
}
|
||||
|
||||
/* the first cell in the table should have 20px padding instead of 5px to match all other widgets */
|
||||
.widget .table:not(td .table) > thead > tr > th:first-child,
|
||||
.widget .table:not(td .table) > tbody > tr > th:first-child,
|
||||
.widget .table:not(td .table) > tfoot > tr > th:first-child,
|
||||
.widget .table:not(td .table) > thead > tr > td:first-child,
|
||||
.widget .table:not(td .table) > tbody > tr > td:first-child,
|
||||
.widget .table:not(td .table) > tfoot > tr > td:first-child {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* the last cell in the table should have 20px padding instead of 5px to match all other widgets */
|
||||
.widget .table:not(td .table) > thead > tr > th:last-child,
|
||||
.widget .table:not(td .table) > tbody > tr > th:last-child,
|
||||
.widget .table:not(td .table) > tfoot > tr > th:last-child,
|
||||
.widget .table:not(td .table) > thead > tr > td:last-child,
|
||||
.widget .table:not(td .table) > tbody > tr > td:last-child,
|
||||
.widget .table:not(td .table) > tfoot > tr > td:last-child {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
/* tables inside widgets should extend the full width of the widget, with 20px table cell padding padding on the left and right */
|
||||
.widget-body:not(.no-padding) > .table,
|
||||
.widget-body:not(.no-padding) > .widget-content > .table {
|
||||
margin: 0 -20px;
|
||||
width: calc(100% + 40px);
|
||||
max-width: calc(100% + 40px);
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
color: var(--text-input-group-addon-color);
|
||||
background-color: var(--bg-input-group-addon-color);
|
||||
|
||||
@@ -56,11 +56,12 @@ function ServiceServiceFactory(AngularToReact) {
|
||||
* @param {string?} rollback
|
||||
*/
|
||||
async function updateServiceAngularJS(environmentId, service, config, rollback) {
|
||||
const data = await getServiceAngularJS(environmentId, service.Id);
|
||||
return updateService({
|
||||
environmentId,
|
||||
config,
|
||||
serviceId: service.Id,
|
||||
version: service.Version,
|
||||
version: data.Version,
|
||||
registryId: config.registryId,
|
||||
rollback,
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
</tr>
|
||||
<tr authorization="DockerContainerLogs, DockerContainerInspect, DockerContainerStats, DockerExecStart">
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<div class="btn-group -ml-3" role="group" aria-label="...">
|
||||
<a authorization="DockerContainerLogs" class="btn" type="button" ui-sref="docker.containers.container.logs({ id: container.Id })"
|
||||
><pr-icon icon="'file-text'" class-name="'space-right'"></pr-icon>Logs</a
|
||||
>
|
||||
|
||||
@@ -166,7 +166,7 @@ angular.module('portainer.docker').controller('ImageController', [
|
||||
$scope.state.exportInProgress = true;
|
||||
ImageService.downloadImages([{ tags: image.RepoTags, id: image.Id }])
|
||||
.then(function success(data) {
|
||||
var downloadData = new Blob([data.file], { type: 'application/x-tar' });
|
||||
var downloadData = new Blob([data], { type: 'application/x-tar' });
|
||||
FileSaver.saveAs(downloadData, 'images.tar');
|
||||
Notifications.success('Success', 'Image successfully downloaded');
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="list" title-text="Configs"> </rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
|
||||
<div class="form-inline" style="padding: 10px 20px" authorization="DockerServiceUpdate">
|
||||
Add a config:
|
||||
<select
|
||||
class="form-control !h-[30px] !text-[13px]"
|
||||
@@ -12,7 +12,7 @@
|
||||
>
|
||||
<option selected disabled hidden value="">Select a config</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'"></pr-icon> add config </a>
|
||||
<a class="btn btn-default btn-sm" ng-click="addConfig(service, newConfig)"> <pr-icon icon="'plus'"></pr-icon> Add config </a>
|
||||
</div>
|
||||
<table class="table" style="margin-top: 5px">
|
||||
<thead>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="list" title-text="Logging driver"> </rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
|
||||
<div class="form-inline" style="padding: 10px 20px" authorization="DockerServiceUpdate">
|
||||
Driver:
|
||||
<select
|
||||
class="form-control !h-[30px] !text-[13px]"
|
||||
@@ -16,7 +16,7 @@
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="!service.LogDriverName || service.LogDriverName === 'none' || addLogDriverOpt(service)">
|
||||
<pr-icon icon="'plus'"></pr-icon> add logging driver option
|
||||
<pr-icon icon="'plus'"></pr-icon> Add logging driver option
|
||||
</a>
|
||||
</div>
|
||||
<table class="table">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="list" title-text="Secrets"> </rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px" authorization="DockerServiceUpdate">
|
||||
<div class="form-inline" style="padding: 10px 20px" authorization="DockerServiceUpdate">
|
||||
Add a secret:
|
||||
<select
|
||||
class="form-control !h-[30px] !text-[13px]"
|
||||
@@ -20,7 +20,7 @@
|
||||
<label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="false">Default location</label>
|
||||
<label class="btn btn-light" ng-model="state.addSecret.override" uib-btn-radio="true">Override</label>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)"> <pr-icon icon="'plus'"></pr-icon> add secret </a>
|
||||
<a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)"> <pr-icon icon="'plus'"></pr-icon> Add secret </a>
|
||||
</div>
|
||||
<table class="table" style="margin-top: 5px">
|
||||
<thead>
|
||||
|
||||
@@ -10,19 +10,6 @@
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<base id="base" />
|
||||
<script>
|
||||
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
|
||||
if (window.origin == 'http://localhost:49000') {
|
||||
// we are loading the app from a local file as in docker extension
|
||||
document.getElementById('base').href = 'http://localhost:49000/';
|
||||
|
||||
window.ddExtension = true;
|
||||
} else {
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
|
||||
@@ -21,6 +21,18 @@ import { onStartupAngular } from './app';
|
||||
import { configApp } from './config';
|
||||
import { constantsModule } from './ng-constants';
|
||||
|
||||
// http://localhost:49000 is a docker extension specific url (see /build/docker-extension/docker-compose.yml)
|
||||
if (window.origin == 'http://localhost:49000') {
|
||||
// we are loading the app from a local file as in docker extension
|
||||
document.getElementById('base').href = 'http://localhost:49000/';
|
||||
|
||||
window.ddExtension = true;
|
||||
} else {
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
}
|
||||
|
||||
initFeatureService(Edition[process.env.PORTAINER_EDITION]);
|
||||
|
||||
angular
|
||||
|
||||
@@ -207,7 +207,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
|
||||
const applications = {
|
||||
name: 'kubernetes.applications',
|
||||
url: '/applications',
|
||||
url: '/applications?tab',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesApplicationsView',
|
||||
@@ -233,7 +233,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
|
||||
const application = {
|
||||
name: 'kubernetes.applications.application',
|
||||
url: '/:namespace/:name?resource-type&tab',
|
||||
url: '/:namespace/:name?resource-type',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'applicationDetailsView',
|
||||
@@ -489,12 +489,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
},
|
||||
};
|
||||
|
||||
const resourcePoolAccess = {
|
||||
const namespaceAccess = {
|
||||
name: 'kubernetes.resourcePools.resourcePool.access',
|
||||
url: '/access',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesResourcePoolAccessView',
|
||||
component: 'kubernetesNamespaceAccessView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
@@ -647,7 +647,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
$stateRegistryProvider.register(resourcePools);
|
||||
$stateRegistryProvider.register(namespaceCreation);
|
||||
$stateRegistryProvider.register(resourcePool);
|
||||
$stateRegistryProvider.register(resourcePoolAccess);
|
||||
$stateRegistryProvider.register(namespaceAccess);
|
||||
$stateRegistryProvider.register(volumes);
|
||||
$stateRegistryProvider.register(volume);
|
||||
$stateRegistryProvider.register(registries);
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@
|
||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.configurations' })"
|
||||
data-cy="k8sConfig-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from file
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings">
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
Show custom values
|
||||
</button>
|
||||
<span class="small interactive vertical-center" ng-if="$ctrl.state.loadingValues" role="status">
|
||||
<pr-icon icon="'refresh-cw'" class="mr-1"></pr-icon>
|
||||
Loading values.yaml...
|
||||
<inline-loader children="'Loading values.yaml...'" />
|
||||
</span>
|
||||
<button ng-if="$ctrl.state.showCustomValues" class="btn btn-xs btn-default vertical-center !ml-0 mr-2" ng-click="$ctrl.state.showCustomValues = false;">
|
||||
<pr-icon icon="'minus'" class="vertical-center"></pr-icon>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesConfigMap, KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
||||
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
|
||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
|
||||
import { ConfigurationOwnerUsernameLabel } from '@/react/kubernetes/configs/constants';
|
||||
class KubernetesConfigMapConverter {
|
||||
static apiToPortainerAccessConfigMap(data) {
|
||||
const res = new KubernetesPortainerAccessConfigMap();
|
||||
@@ -35,7 +34,7 @@ class KubernetesConfigMapConverter {
|
||||
res.Id = data.metadata.uid;
|
||||
res.Name = data.metadata.name;
|
||||
res.Namespace = data.metadata.namespace;
|
||||
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[ConfigurationOwnerUsernameLabel] : '';
|
||||
res.CreationDate = data.metadata.creationTimestamp;
|
||||
res.Yaml = yaml ? yaml.data : '';
|
||||
res.Labels = data.metadata.labels;
|
||||
@@ -79,7 +78,7 @@ class KubernetesConfigMapConverter {
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace.Namespace.Name;
|
||||
const configurationOwner = _.truncate(data.ConfigurationOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||
res.metadata.labels[ConfigurationOwnerUsernameLabel] = configurationOwner;
|
||||
|
||||
_.forEach(data.Data, (entry) => {
|
||||
if (entry.IsBinary) {
|
||||
@@ -100,7 +99,7 @@ class KubernetesConfigMapConverter {
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.metadata.labels = data.Labels || {};
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
|
||||
res.metadata.labels[ConfigurationOwnerUsernameLabel] = data.ConfigurationOwner;
|
||||
_.forEach(data.Data, (entry) => {
|
||||
if (entry.IsBinary) {
|
||||
res.binaryData[entry.Key] = entry.Value;
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash-es';
|
||||
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
|
||||
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
|
||||
import { KubernetesPortainerConfigurationDataAnnotation } from 'Kubernetes/models/configuration/models';
|
||||
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
|
||||
import { ConfigurationOwnerUsernameLabel } from '@/react/kubernetes/configs/constants';
|
||||
import { KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesSecretTypeOptions } from 'Kubernetes/models/configuration/models';
|
||||
class KubernetesSecretConverter {
|
||||
@@ -12,7 +12,7 @@ class KubernetesSecretConverter {
|
||||
res.metadata.namespace = secret.Namespace.Namespace.Name;
|
||||
res.type = secret.Type;
|
||||
const configurationOwner = _.truncate(secret.ConfigurationOwner, { length: 63, omission: '' });
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = configurationOwner;
|
||||
res.metadata.labels[ConfigurationOwnerUsernameLabel] = configurationOwner;
|
||||
|
||||
let annotation = '';
|
||||
_.forEach(secret.Data, (entry) => {
|
||||
@@ -40,7 +40,7 @@ class KubernetesSecretConverter {
|
||||
res.metadata.namespace = secret.Namespace;
|
||||
res.type = secret.Type;
|
||||
res.metadata.labels = secret.Labels || {};
|
||||
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
|
||||
res.metadata.labels[ConfigurationOwnerUsernameLabel] = secret.ConfigurationOwner;
|
||||
|
||||
let annotation = '';
|
||||
_.forEach(secret.Data, (entry) => {
|
||||
@@ -69,7 +69,7 @@ class KubernetesSecretConverter {
|
||||
res.Namespace = payload.metadata.namespace;
|
||||
res.Type = payload.type;
|
||||
res.Labels = payload.metadata.labels || {};
|
||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
|
||||
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[ConfigurationOwnerUsernameLabel] : '';
|
||||
res.CreationDate = payload.metadata.creationTimestamp;
|
||||
res.Annotations = payload.metadata.annotations;
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { ApplicationsDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsDatatable/ApplicationsDatatable';
|
||||
|
||||
export const applicationsModule = angular
|
||||
.module('portainer.kubernetes.react.components.applications', [])
|
||||
|
||||
.component(
|
||||
'kubernetesApplicationsDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(ApplicationsDatatable)), [
|
||||
'namespace',
|
||||
'namespaces',
|
||||
'onNamespaceChange',
|
||||
'onRefresh',
|
||||
'onRemove',
|
||||
'hideStacks',
|
||||
])
|
||||
).name;
|
||||
@@ -23,7 +23,6 @@ import { ApplicationSummarySection } from '@/react/kubernetes/applications/compo
|
||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
|
||||
import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable';
|
||||
import { NodesDatatable } from '@/react/kubernetes/cluster/HomeView/NodesDatatable';
|
||||
import { StackName } from '@/react/kubernetes/DeployView/StackName/StackName';
|
||||
import { StackNameLabelInsight } from '@/react/kubernetes/DeployView/StackName/StackNameLabelInsight';
|
||||
@@ -61,14 +60,12 @@ import { EnvironmentVariablesFormSection } from '@/react/kubernetes/applications
|
||||
import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/components/EnvironmentVariablesFormSection/kubeEnvVarValidationSchema';
|
||||
import { IntegratedAppsDatatable } from '@/react/kubernetes/components/IntegratedAppsDatatable/IntegratedAppsDatatable';
|
||||
|
||||
import { applicationsModule } from './applications';
|
||||
import { namespacesModule } from './namespaces';
|
||||
import { clusterManagementModule } from './clusterManagement';
|
||||
import { registriesModule } from './registries';
|
||||
|
||||
export const ngModule = angular
|
||||
.module('portainer.kubernetes.react.components', [
|
||||
applicationsModule,
|
||||
namespacesModule,
|
||||
clusterManagementModule,
|
||||
registriesModule,
|
||||
@@ -208,15 +205,6 @@ export const ngModule = angular
|
||||
['formValues', 'oldFormValues']
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'kubernetesApplicationsStacksDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [
|
||||
'onRemove',
|
||||
'namespace',
|
||||
'namespaces',
|
||||
'onNamespaceChange',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'kubernetesIntegratedApplicationsDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(IntegratedAppsDatatable)), [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { NamespacesDatatable } from '@/react/kubernetes/namespaces/ListView/NamespacesDatatable';
|
||||
import { NamespaceAppsDatatable } from '@/react/kubernetes/namespaces/ItemView/NamespaceAppsDatatable';
|
||||
import { NamespaceAccessDatatable } from '@/react/kubernetes/namespaces/AccessView/AccessDatatable';
|
||||
import { AccessDatatable } from '@/react/kubernetes/namespaces/AccessView/AccessDatatable/AccessDatatable';
|
||||
|
||||
export const namespacesModule = angular
|
||||
.module('portainer.kubernetes.react.components.namespaces', [])
|
||||
@@ -24,8 +24,5 @@ export const namespacesModule = angular
|
||||
)
|
||||
.component(
|
||||
'namespaceAccessDatatable',
|
||||
r2a(withUIRouter(withReactQuery(NamespaceAccessDatatable)), [
|
||||
'dataset',
|
||||
'onRemove',
|
||||
])
|
||||
r2a(withUIRouter(withReactQuery(AccessDatatable)), [])
|
||||
).name;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ServicesView } from '@/react/kubernetes/services/ServicesView';
|
||||
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
|
||||
import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/ConfigmapsAndSecretsView';
|
||||
import { CreateNamespaceView } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceView';
|
||||
import { ApplicationsView } from '@/react/kubernetes/applications/ListView/ApplicationsView';
|
||||
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
|
||||
import { ConfigureView } from '@/react/kubernetes/cluster/ConfigureView';
|
||||
import { NamespacesView } from '@/react/kubernetes/namespaces/ListView/NamespacesView';
|
||||
@@ -18,6 +19,7 @@ import { ServiceAccountsView } from '@/react/kubernetes/more-resources/ServiceAc
|
||||
import { ClusterRolesView } from '@/react/kubernetes/more-resources/ClusterRolesView';
|
||||
import { RolesView } from '@/react/kubernetes/more-resources/RolesView';
|
||||
import { VolumesView } from '@/react/kubernetes/volumes/ListView/VolumesView';
|
||||
import { AccessView } from '@/react/kubernetes/namespaces/AccessView/AccessView';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.kubernetes.react.views', [])
|
||||
@@ -29,6 +31,10 @@ export const viewsModule = angular
|
||||
'kubernetesNamespacesView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(NamespacesView))), [])
|
||||
)
|
||||
.component(
|
||||
'kubernetesNamespaceAccessView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(AccessView))), [])
|
||||
)
|
||||
.component(
|
||||
'kubernetesServicesView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ServicesView))), [])
|
||||
@@ -55,6 +61,10 @@ export const viewsModule = angular
|
||||
[]
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'kubernetesApplicationsView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ApplicationsView))), [])
|
||||
)
|
||||
.component(
|
||||
'applicationDetailsView',
|
||||
r2a(
|
||||
|
||||
@@ -3,7 +3,6 @@ import _ from 'lodash-es';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap';
|
||||
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||
import { KubernetesPortainerAccessConfigMap } from 'Kubernetes/models/config-map/models';
|
||||
|
||||
class KubernetesConfigMapService {
|
||||
/* @ngInject */
|
||||
@@ -18,22 +17,6 @@ class KubernetesConfigMapService {
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
getAccess(namespace, name) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
const raw = await this.KubernetesConfigMaps(namespace).get(params).$promise;
|
||||
return KubernetesConfigMapConverter.apiToPortainerAccessConfigMap(raw);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return new KubernetesPortainerAccessConfigMap();
|
||||
}
|
||||
throw new PortainerError('Unable to retrieve Portainer accesses', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createAccess(config) {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<page-header ng-if="ctrl.state.viewReady" title="'Application list'" breadcrumbs="['Applications']" reload="true"></page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12" data-cy="k8sApp-appList">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading class="vertical-center"> <pr-icon icon="'box'"></pr-icon> Applications </uib-tab-heading>
|
||||
|
||||
<kubernetes-applications-datatable
|
||||
dataset="ctrl.state.applications"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" classes="btn-sm" select="ctrl.selectTab(2)">
|
||||
<uib-tab-heading class="vertical-center"> <pr-icon icon="'list'"></pr-icon> Stacks </uib-tab-heading>
|
||||
|
||||
<kubernetes-applications-stacks-datatable
|
||||
dataset="ctrl.state.stacks"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
on-remove="(ctrl.removeStacksAction)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
show-system="ctrl.state.isSystemResources"
|
||||
set-system-resources="(ctrl.setSystemResources)"
|
||||
>
|
||||
</kubernetes-applications-stacks-datatable>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
<kubernetes-applications-datatable
|
||||
ng-if="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
dataset="ctrl.state.applications"
|
||||
on-refresh="(ctrl.getApplications)"
|
||||
namespaces="ctrl.state.namespaces"
|
||||
namespace="ctrl.state.namespaceName"
|
||||
on-namespace-change="(ctrl.onChangeNamespaceDropdown)"
|
||||
is-loading="ctrl.state.isAppsLoading"
|
||||
on-remove="(ctrl.removeAction)"
|
||||
hide-stacks="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
|
||||
templateUrl: './applications.html',
|
||||
controller: 'KubernetesApplicationsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
|
||||
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
||||
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
||||
import { getStacksFromApplications } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/getStacksFromApplications';
|
||||
import { getApplications } from '@/react/kubernetes/applications/application.queries.ts';
|
||||
import { getNamespaces } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
class KubernetesApplicationsController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
$scope,
|
||||
Authentication,
|
||||
Notifications,
|
||||
KubernetesApplicationService,
|
||||
EndpointService,
|
||||
HelmService,
|
||||
KubernetesConfigurationService,
|
||||
LocalStorage,
|
||||
StackService,
|
||||
KubernetesNamespaceService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.Authentication = Authentication;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.HelmService = HelmService;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.Authentication = Authentication;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.StackService = StackService;
|
||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||
this.removeStacksAction = this.removeStacksAction.bind(this);
|
||||
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
||||
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
||||
this.onChangeNamespaceDropdown = this.onChangeNamespaceDropdown.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('applications', index);
|
||||
}
|
||||
|
||||
async removeStacksActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const stack of selectedItems) {
|
||||
try {
|
||||
const isAppFormCreated = stack.Applications.some((x) => !x.ApplicationKind);
|
||||
|
||||
if (isAppFormCreated) {
|
||||
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
await this.StackService.removeKubernetesStacksByName(stack.Name, stack.ResourcePool, false, this.endpoint.Id);
|
||||
|
||||
this.Notifications.success('Stack successfully removed', stack.Name);
|
||||
_.remove(this.state.stacks, { Name: stack.Name });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove stack');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload(this.$state.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeStacksAction(selectedItems) {
|
||||
return this.$async(this.removeStacksActionAsync, selectedItems);
|
||||
}
|
||||
|
||||
async removeActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const application of selectedItems) {
|
||||
try {
|
||||
if (application.ApplicationType === KubernetesApplicationTypes.Helm) {
|
||||
await this.HelmService.uninstall(this.endpoint.Id, application);
|
||||
} else {
|
||||
if (application.Metadata.labels && application.Metadata.labels[KubernetesPortainerApplicationStackNameLabel]) {
|
||||
// remove stack if no app left in the stack
|
||||
const appsInNamespace = await getApplications(this.endpoint.Id, { namespace: application.ResourcePool, withDependencies: false });
|
||||
const stacksInNamespace = getStacksFromApplications(appsInNamespace);
|
||||
const stack = stacksInNamespace.find((x) => x.Name === application.StackName);
|
||||
if (stack.Applications.length === 0 && application.StackId) {
|
||||
await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id);
|
||||
}
|
||||
}
|
||||
await this.KubernetesApplicationService.delete(application);
|
||||
}
|
||||
this.Notifications.success('Application successfully removed', application.Name);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove application');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload(this.$state.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
this.$async(() => this.removeActionAsync(selectedItems));
|
||||
}
|
||||
|
||||
onPublishingModeClick(application) {
|
||||
this.state.activeTab = 1;
|
||||
_.forEach(this.state.ports, (item) => {
|
||||
item.Expanded = false;
|
||||
item.Highlighted = false;
|
||||
if (item.Name === application.Name && item.Ports.length > 1) {
|
||||
item.Expanded = true;
|
||||
item.Highlighted = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeNamespaceDropdown(namespaceName) {
|
||||
return this.$async(async () => {
|
||||
this.state.namespaceName = namespaceName;
|
||||
// save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}'
|
||||
this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName);
|
||||
});
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: this.LocalStorage.getActiveTab('applications'),
|
||||
currentName: this.$state.$current.name,
|
||||
isAdmin: this.Authentication.isAdmin(),
|
||||
viewReady: false,
|
||||
applications: [],
|
||||
stacks: [],
|
||||
ports: [],
|
||||
namespaces: [],
|
||||
namespaceName: '',
|
||||
};
|
||||
|
||||
this.deploymentOptions = await getDeploymentOptions();
|
||||
|
||||
this.user = this.Authentication.getUserDetails();
|
||||
this.state.namespaces = await getNamespaces(this.endpoint.Id);
|
||||
|
||||
const savedNamespace = this.LocalStorage.getNamespaceFilter(this.endpoint.Id, this.user.ID); // could be null if not found, and '' if all namepsaces is selected
|
||||
const preferredNamespace = savedNamespace === null ? 'default' : savedNamespace;
|
||||
|
||||
this.state.namespaces = this.state.namespaces.filter((n) => n.Status.phase === 'Active');
|
||||
this.state.namespaces = _.sortBy(this.state.namespaces, 'Name');
|
||||
// set all namespaces ('') if there are no namespaces, or if all namespaces is selected
|
||||
if (!this.state.namespaces.length || preferredNamespace === '') {
|
||||
this.state.namespaceName = '';
|
||||
} else {
|
||||
// otherwise, set the preferred namespaceName if it exists, otherwise set the first namespaceName
|
||||
this.state.namespaceName = this.state.namespaces.find((n) => n.Name === preferredNamespace) ? preferredNamespace : this.state.namespaces[0].Name;
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('applications', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationsController', KubernetesApplicationsController);
|
||||
@@ -424,7 +424,7 @@ class KubernetesCreateApplicationController {
|
||||
const ingressNamesLoaded = this.ingresses.map((i) => i.Name);
|
||||
const areAllIngressesLoaded = uniqueIngressNamesUsed.every((ingressNameUsed) => ingressNamesLoaded.includes(ingressNameUsed));
|
||||
if (!areAllIngressesLoaded) {
|
||||
this.refreshIngresses();
|
||||
this.refreshIngresses(this.application.ResourcePool);
|
||||
}
|
||||
}
|
||||
// update the services
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
<table class="table">
|
||||
<tbody class="release-table">
|
||||
<tr>
|
||||
<td class="vertical-center !pl-0">Name</td>
|
||||
<td class="vertical-center">Name</td>
|
||||
<td class="vertical-center !p-2" data-cy="k8sAppDetail-appName">
|
||||
{{ $ctrl.state.release.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="vertical-center !pl-0">Chart</td>
|
||||
<td class="vertical-center">Chart</td>
|
||||
<td class="vertical-center !p-2">
|
||||
{{ $ctrl.state.release.chart }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="vertical-center !pl-0">App version</td>
|
||||
<td class="vertical-center">App version</td>
|
||||
<td class="vertical-center !p-2">
|
||||
{{ $ctrl.state.release.app_version }}
|
||||
</td>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading class="flex-center gap-1"> <pr-icon icon="'hard-drive'" size="'sm'"></pr-icon> Node </uib-tab-heading>
|
||||
|
||||
<form class="form-horizontal" name="kubernetesNodeUpdateForm" style="padding: 20px" autocomplete="off">
|
||||
<form class="form-horizontal widget-body" name="kubernetesNodeUpdateForm" autocomplete="off">
|
||||
<table class="table">
|
||||
<tbody ng-if="ctrl.node">
|
||||
<tr>
|
||||
@@ -73,7 +73,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="padding: 8px">
|
||||
<div class="mt-5">
|
||||
<kubernetes-resource-reservation
|
||||
ng-if="ctrl.resourceReservation"
|
||||
cpu-reservation="ctrl.resourceReservation.CpuRequest"
|
||||
@@ -88,7 +88,7 @@
|
||||
</kubernetes-resource-reservation>
|
||||
</div>
|
||||
|
||||
<div style="padding: 8px">
|
||||
<div>
|
||||
<!-- #region labels -->
|
||||
<div class="col-sm-12 form-section-title"> Labels </div>
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@
|
||||
<pr-icon icon="'file-code'"></pr-icon>
|
||||
ConfigMap
|
||||
</uib-tab-heading>
|
||||
<div style="padding: 20px">
|
||||
<div class="widget-body">
|
||||
<table class="table" data-cy="k8sConfigDetail-configTable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-[40%] !border-none !pl-0">Name</td>
|
||||
<td class="w-[40%] !border-none">Name</td>
|
||||
<td class="!border-none"> {{ ctrl.configuration.Name }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="!pl-0">Namespace</td>
|
||||
<td>Namespace</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a>
|
||||
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
|
||||
|
||||
@@ -28,25 +28,25 @@
|
||||
<pr-icon icon="'lock'"></pr-icon>
|
||||
Secret
|
||||
</uib-tab-heading>
|
||||
<div style="padding: 20px">
|
||||
<div class="widget-body">
|
||||
<table class="table" data-cy="k8sConfigDetail-configTable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-[40%] !border-none !pl-0">Name</td>
|
||||
<td class="w-[40%] !border-none">Name</td>
|
||||
<td class="!border-none">
|
||||
{{ ctrl.configuration.Name }}
|
||||
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.configuration.IsRegistrySecret">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="!pl-0">Namespace</td>
|
||||
<td>Namespace</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a>
|
||||
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="ctrl.secretTypeName">
|
||||
<td class="!pl-0">Secret Type</td>
|
||||
<td>Secret Type</td>
|
||||
<td> {{ ctrl.secretTypeName }} </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<page-header ng-if="ctrl.state.viewReady" title="'Create from manifest'" breadcrumbs="['Deploy Kubernetes resources']" reload="true"></page-header>
|
||||
<page-header ng-if="ctrl.state.viewReady" title="'Create from file'" breadcrumbs="['Deploy Kubernetes resources']" reload="true"></page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<portainer-tooltip message="'If you have defined namespaces in your deployment file turning this on will enforce the use of those only in the deployment'">
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 vertical-center pt-3">
|
||||
<div class="col-sm-8 vertical-center pt-1">
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="toggle_logo" ng-model="ctrl.formValues.namespace_toggle" data-cy="use-namespce-from-menifest" />
|
||||
<span class="slider round"></span>
|
||||
@@ -125,6 +125,7 @@
|
||||
stack-type="3"
|
||||
on-change="(ctrl.onChangeTemplateId)"
|
||||
value="ctrl.state.templateId"
|
||||
is-load-failed="ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId && ctrl.state.templateLoadFailed"
|
||||
></custom-template-selector>
|
||||
|
||||
<custom-templates-variables-field
|
||||
@@ -135,16 +136,6 @@
|
||||
></custom-templates-variables-field>
|
||||
</div>
|
||||
|
||||
<span ng-if="ctrl.state.BuildMethod === ctrl.BuildMethods.CUSTOM_TEMPLATE && ctrl.state.templateId && ctrl.state.templateLoadFailed">
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please
|
||||
<a ui-sref="kubernetes.templates.custom.edit({id: ctrl.state.templateId})">click here</a> for configuration.</p
|
||||
>
|
||||
<p class="small vertical-center text-danger mb-5" ng-if="!(ctrl.currentUser.isAdmin || ctrl.currentUser.id === ctrl.state.template.CreatedByUserId)">
|
||||
<pr-icon icon="'alert-triangle'" mode="'danger'" size="'md'" feather="true"></pr-icon>Custom template could not be loaded, please contact your administrator.</p
|
||||
>
|
||||
</span>
|
||||
|
||||
<!-- editor -->
|
||||
<div class="mt-4">
|
||||
<web-editor-form
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<page-header
|
||||
ng-if="ctrl.state.viewReady"
|
||||
title="'Namespace access management'"
|
||||
breadcrumbs="[
|
||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
||||
{
|
||||
label:ctrl.pool.Namespace.Name,
|
||||
link: 'kubernetes.resourcePools.resourcePool',
|
||||
linkParams:{id: ctrl.pool.Namespace.Name}
|
||||
},
|
||||
'Access management'
|
||||
]"
|
||||
reload="true"
|
||||
>
|
||||
</page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row" ng-if="ctrl.pool">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="layers" title-text="Namespace"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="!pl-0">Name</td>
|
||||
<td>
|
||||
{{ ctrl.pool.Namespace.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget ng-if="ctrl.availableUsersAndTeams">
|
||||
<rd-widget-header icon="user-x" title-text="Create access"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div
|
||||
ng-if="!ctrl.isRBACEnabled"
|
||||
class="small mx-[15px] mb-6 flex gap-1 rounded-lg border border-solid border-warning-5 bg-warning-2 p-4 text-warning-8 th-highcontrast:bg-yellow-11 th-highcontrast:text-white th-dark:bg-yellow-11 th-dark:text-white"
|
||||
>
|
||||
<div class="mt-0.5">
|
||||
<pr-icon icon="'alert-triangle'" feather="true" class-name="'text-warning-7 th-dark:text-white th-highcontrast:text-white'"></pr-icon>
|
||||
</div>
|
||||
<div>
|
||||
<p> Your cluster does not have Kubernetes role-based access control (RBAC) enabled. </p>
|
||||
<p> This means you can't use Portainer RBAC functionality to regulate access to environment resources based on user roles. </p>
|
||||
<p class="mb-0">
|
||||
To enable RBAC, start the <a
|
||||
class="th-highcontrast:text-blue-4 th-dark:text-blue-7"
|
||||
href="https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver"
|
||||
target="_blank"
|
||||
>API server</a
|
||||
> with the <code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">--authorization-mode</code> flag set to a
|
||||
comma-separated list that includes <code class="bg-gray-4 th-highcontrast:bg-black th-dark:bg-black">RBAC</code>, for example:
|
||||
<code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">kube-apiserver --authorization-mode=Example1,RBAC,Example2</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col-sm-12 small text-warning">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
Adding user access will require the affected user(s) to logout and login for the changes to be taken into account.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="users-selector"> Select user(s) and/or team(s) </label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
|
||||
No user nor team access has been set on the environment. Head over to the
|
||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage them.
|
||||
</span>
|
||||
<namespace-access-users-selector
|
||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
||||
input-id="users-selector"
|
||||
value="ctrl.formValues.multiselectOutput"
|
||||
options="ctrl.availableUsersAndTeams"
|
||||
on-change="(ctrl.onUsersAndTeamsChange)"
|
||||
></namespace-access-users-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm vertical-center !ml-0"
|
||||
ng-disabled="ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
|
||||
ng-click="ctrl.authorizeAccess()"
|
||||
button-spinner="ctrl.actionInProgress"
|
||||
>
|
||||
<span class="vertical-center" ng-hide="ctrl.state.actionInProgress"><pr-icon icon="'plus'" class="vertical-center"></pr-icon> Create access</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creating access...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<namespace-access-datatable ng-if="ctrl.authorizedUsersAndTeams" dataset="ctrl.authorizedUsersAndTeams" on-remove="(ctrl.unauthorizeAccess)"> </namespace-access-datatable>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolAccessView', {
|
||||
templateUrl: './resourcePoolAccess.html',
|
||||
controller: 'KubernetesResourcePoolAccessController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
endpoint: '<',
|
||||
},
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
|
||||
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
|
||||
import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
||||
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/getIsRBACEnabled';
|
||||
|
||||
class KubernetesResourcePoolAccessController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $scope, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService, EndpointProvider) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||
|
||||
this.GroupService = GroupService;
|
||||
this.AccessService = AccessService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.authorizeAccessAsync = this.authorizeAccessAsync.bind(this);
|
||||
this.unauthorizeAccessAsync = this.unauthorizeAccessAsync.bind(this);
|
||||
this.onUsersAndTeamsChange = this.onUsersAndTeamsChange.bind(this);
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
}
|
||||
|
||||
initAccessConfigMap(configMap) {
|
||||
configMap.Name = KubernetesPortainerConfigMapConfigName;
|
||||
configMap.Namespace = KubernetesPortainerConfigMapNamespace;
|
||||
configMap.Data[KubernetesPortainerConfigMapAccessKey] = {};
|
||||
return configMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*/
|
||||
async onInit() {
|
||||
const endpoint = this.endpoint;
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
multiselectOutput: [],
|
||||
};
|
||||
|
||||
// default to true if error is thrown
|
||||
this.isRBACEnabled = true;
|
||||
|
||||
try {
|
||||
const name = this.$transition$.params().id;
|
||||
let [pool, configMap] = await Promise.all([
|
||||
this.KubernetesResourcePoolService.get(name),
|
||||
this.KubernetesConfigMapService.getAccess(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
||||
]);
|
||||
this.isRBACEnabled = await getIsRBACEnabled(this.EndpointProvider.endpointID());
|
||||
const group = await this.GroupService.group(endpoint.GroupId);
|
||||
const roles = [];
|
||||
const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles);
|
||||
this.pool = pool;
|
||||
if (configMap.Id === 0) {
|
||||
configMap = this.initAccessConfigMap(configMap);
|
||||
}
|
||||
configMap = KubernetesConfigMapHelper.parseJSONData(configMap);
|
||||
|
||||
this.authorizedUsersAndTeams = [];
|
||||
this.accessConfigMap = configMap;
|
||||
const poolAccesses = configMap.Data[KubernetesPortainerConfigMapAccessKey][name];
|
||||
if (poolAccesses) {
|
||||
this.authorizedUsersAndTeams = _.filter(endpointAccesses.authorizedUsersAndTeams, (item) => {
|
||||
if (item instanceof UserAccessViewModel && poolAccesses.UserAccessPolicies) {
|
||||
return poolAccesses.UserAccessPolicies[item.Id] !== undefined;
|
||||
} else if (item instanceof TeamAccessViewModel && poolAccesses.TeamAccessPolicies) {
|
||||
return poolAccesses.TeamAccessPolicies[item.Id] !== undefined;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve namespace information');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize access
|
||||
*/
|
||||
async authorizeAccessAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
||||
this.Notifications.success('Success', 'Access successfully created');
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create accesses');
|
||||
}
|
||||
}
|
||||
|
||||
onUsersAndTeamsChange(value) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.multiselectOutput = value;
|
||||
});
|
||||
}
|
||||
|
||||
authorizeAccess() {
|
||||
return this.$async(this.authorizeAccessAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async unauthorizeAccessAsync(selectedItems) {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
|
||||
this.Notifications.success('Success', 'Access successfully removed');
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove accesses');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
unauthorizeAccess(selectedItems) {
|
||||
return this.$async(this.unauthorizeAccessAsync, selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolAccessController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolAccessController', KubernetesResourcePoolAccessController);
|
||||
@@ -15,23 +15,18 @@
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading class="vertical-center"> <pr-icon icon="'layers'"></pr-icon> Namespace </uib-tab-heading>
|
||||
<form class="form-horizontal" autocomplete="off" name="resourcePoolEditForm" style="padding: 20px; margin-top: 10px">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.pool.Namespace.Name }}
|
||||
<span class="label label-info image-tag label-margins" ng-if="ctrl.isSystem">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal widget-body" autocomplete="off" name="resourcePoolEditForm" style="margin-top: 10px">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-[40%]">Name</td>
|
||||
<td>
|
||||
{{ ctrl.pool.Namespace.Name }}
|
||||
<span class="label label-info image-tag label-margins" ng-if="ctrl.isSystem">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="col-sm-12 !p-0">
|
||||
<annotations-be-teaser></annotations-be-teaser>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<pr-icon icon="'database'"></pr-icon>
|
||||
Volume
|
||||
</uib-tab-heading>
|
||||
<div style="padding: 20px">
|
||||
<div class="widget-body">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user