Compare commits
4 Commits
fix/EE-684
...
feat/EE-50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b6ccf91f | ||
|
|
659402c3ec | ||
|
|
8b96f2cecd | ||
|
|
c002d460c8 |
@@ -293,15 +293,12 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
|
||||
const deploy = {
|
||||
name: 'kubernetes.deploy',
|
||||
url: '/deploy?templateId',
|
||||
url: '/deploy?templateId&referrer',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesDeployView',
|
||||
},
|
||||
},
|
||||
params: {
|
||||
templateId: '',
|
||||
},
|
||||
};
|
||||
|
||||
const resourcePools = {
|
||||
@@ -397,8 +394,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||
};
|
||||
|
||||
const endpointKubernetesSecurityConstraint = {
|
||||
name: 'kubernetes.cluster.securityConstraint',
|
||||
url: '/securityConstraint',
|
||||
name: 'kubernetes.security',
|
||||
url: '/security',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '../kubernetes/views/security-constraint/constraint.html',
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
ng-if="$ctrl.isPrimary"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary vertical-center !ml-0 h-fit"
|
||||
ui-sref="kubernetes.deploy"
|
||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.applications' })"
|
||||
data-cy="k8sApp-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
|
||||
@@ -37,7 +37,12 @@
|
||||
<button type="button" class="btn btn-sm btn-secondary vertical-center !ml-0" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigWithFormButton">
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary vertical-center !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sConfig-deployFromManifestButton">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary vertical-center !ml-0"
|
||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.configurations' })"
|
||||
data-cy="k8sConfig-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,12 @@
|
||||
<button type="button" class="btn btn-sm btn-secondary !ml-0" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceWithFormButton">
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Add with form
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sNamespace-deployFromManifestButton">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary !ml-0"
|
||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.resourcePools' })"
|
||||
data-cy="k8sNamespace-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>Create from manifest
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,12 @@
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary vertical-center !ml-0" ui-sref="kubernetes.deploy" data-cy="k8sVolume-deployFromManifestButton">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary vertical-center !ml-0"
|
||||
ui-sref="kubernetes.deploy({ referrer: 'kubernetes.volumes' })"
|
||||
data-cy="k8sVolume-deployFromManifestButton"
|
||||
>
|
||||
<pr-icon icon="'plus'" class-name="'!h-3'"></pr-icon>
|
||||
Create from manifest
|
||||
</button>
|
||||
|
||||
@@ -268,6 +268,12 @@ class KubernetesDeployController {
|
||||
|
||||
this.Notifications.success('Success', 'Manifest successfully deployed');
|
||||
this.state.isEditorDirty = false;
|
||||
|
||||
if (this.$state.params.referrer) {
|
||||
this.$state.go(this.$state.params.referrer);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Unable to deploy manifest', err, 'Unable to deploy resources');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<page-header
|
||||
ng-if="state.viewReady"
|
||||
title="'Kubernetes security constraints'"
|
||||
title="'Security'"
|
||||
breadcrumbs="[
|
||||
{ label:'Environments', link:'portainer.endpoints' },
|
||||
{ label:endpoint.Name, link:'portainer.endpoints.endpoint', linkParams:{id: endpoint.Id} },
|
||||
'Security constraints'
|
||||
'Security'
|
||||
]"
|
||||
reload="true"
|
||||
></page-header>
|
||||
@@ -12,29 +12,73 @@
|
||||
<kubernetes-view-loading view-ready="state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="shield" title-text="Pod security constraints"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesSecurityConstraintForm">
|
||||
<!-- main toggle -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="formValues.enabled"
|
||||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable pod security constraints'"
|
||||
feature-id="limitedFeaturePodSecurityPolicy"
|
||||
label-class="'col-sm-3 col-lg-2 px-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
<div class="be-indicator-container limited-be">
|
||||
<div class="overlay">
|
||||
<div class="limited-be-link vertical-center"
|
||||
><be-feature-indicator feature="limitedFeatureAccessControl"></be-feature-indicator
|
||||
><portainer-tooltip message="'This feature is currently limited to Business Edition users only. '"></portainer-tooltip
|
||||
></div>
|
||||
<div class="limited-be-content">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="user" title-text="Access Control"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="inline-flex">
|
||||
<div class="mr-2 inline"><pr-icon icon="'info'" mode="'primary'"></pr-icon></div>
|
||||
<div class="inline">
|
||||
<div>
|
||||
<a class="hyperlink" href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/" target="_blank">Kubernetes authorization</a> generally uses
|
||||
<a class="hyperlink" href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" target="_blank">role based access control</a> (RBAC) to determine if a
|
||||
user or process has the permissions to access or perform certain actions within the cluster.
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
>The <b>Cluster Roles</b> and <b>Roles</b> screens (under this Security sub-menu) list the resources where permissions are defined in your cluster, and the
|
||||
Bindings that grant those permissions to subjects (Users, Groups and Service Accounts). The <b>Service Accounts</b> screens lists existing identities (typically
|
||||
for a Pod or set of Pods).</div
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="be-indicator-container limited-be">
|
||||
<div class="overlay">
|
||||
<div class="limited-be-link vertical-center"
|
||||
><be-feature-indicator feature="limitedFeaturePodSecurityPolicy"></be-feature-indicator
|
||||
><portainer-tooltip message="'This feature is currently limited to Business Edition users only. '"></portainer-tooltip
|
||||
></div>
|
||||
<div class="limited-be-content">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="shield" title-text="Pod security constraints"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesSecurityConstraintForm">
|
||||
<!-- main toggle -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<p class="text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" class-name="'icon icon-sm icon-primary'"></pr-icon>
|
||||
You may configure privilege and access control settings for Pods in your cluster.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="formValues.enabled"
|
||||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable pod security constraints'"
|
||||
feature-id="limitedFeaturePodSecurityPolicy"
|
||||
label-class="'col-sm-3 col-lg-2 px-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ angular.module('portainer.kubernetes').controller('KubernetesSecurityConstraintC
|
||||
'EndpointService',
|
||||
function ($scope, EndpointProvider, EndpointService) {
|
||||
$scope.limitedFeaturePodSecurityPolicy = FeatureId.POD_SECURITY_POLICY_CONSTRAINT;
|
||||
$scope.limitedFeatureAccessControl = FeatureId.K8S_ACCESS_CONTROL;
|
||||
$scope.state = {
|
||||
viewReady: false,
|
||||
actionInProgress: false,
|
||||
|
||||
66
app/react/components/Widget/WidgetTabs.tsx
Normal file
66
app/react/components/Widget/WidgetTabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { RawParams } from '@uirouter/react';
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
export interface Tab {
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
widget: ReactNode;
|
||||
selectedTabParam: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentTabIndex: number;
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
export function WidgetTabs({ currentTabIndex, tabs }: Props) {
|
||||
// ensure that the selectedTab param is always valid
|
||||
const invalidQueryParamValue = tabs.every(
|
||||
(tab) => encodeURIComponent(tab.selectedTabParam) !== tab.selectedTabParam
|
||||
);
|
||||
|
||||
if (invalidQueryParamValue) {
|
||||
throw new Error('Invalid query param value for tab');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12 !mb-0">
|
||||
<div className="pl-2">
|
||||
{tabs.map(({ name, icon }, index) => (
|
||||
<Link
|
||||
to="."
|
||||
params={{ 'selected-tab': tabs[index].selectedTabParam }}
|
||||
key={index}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-2 border-0 border-b-2 border-solid bg-transparent px-4 py-2',
|
||||
currentTabIndex === index
|
||||
? 'border-blue-8 text-blue-8 th-highcontrast:border-blue-6 th-highcontrast:text-blue-6 th-dark:border-blue-6 th-dark:text-blue-6'
|
||||
: 'border-transparent'
|
||||
)}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
{name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// findSelectedTabIndex returns the index of the selected-tab, or 0 if none is selected
|
||||
export function findSelectedTabIndex(
|
||||
{ params }: { params: RawParams },
|
||||
tabs: Tab[]
|
||||
) {
|
||||
const selectedTabParam = params['selected-tab'] || tabs[0].selectedTabParam;
|
||||
const currentTabIndex = tabs.findIndex(
|
||||
(tab) => tab.selectedTabParam === selectedTabParam
|
||||
);
|
||||
return currentTabIndex || 0;
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import { WidgetFooter } from './WidgetFooter';
|
||||
import { WidgetTitle } from './WidgetTitle';
|
||||
import { WidgetTaskbar } from './WidgetTaskbar';
|
||||
import { Loading } from './Loading';
|
||||
import { WidgetTabs } from './WidgetTabs';
|
||||
|
||||
interface WithSubcomponents {
|
||||
Body: typeof WidgetBody;
|
||||
Footer: typeof WidgetFooter;
|
||||
Title: typeof WidgetTitle;
|
||||
Tabs: typeof WidgetTabs;
|
||||
Taskbar: typeof WidgetTaskbar;
|
||||
Loading: typeof Loading;
|
||||
}
|
||||
@@ -18,6 +20,7 @@ const Widget = MainComponent as typeof MainComponent & WithSubcomponents;
|
||||
Widget.Body = WidgetBody;
|
||||
Widget.Footer = WidgetFooter;
|
||||
Widget.Title = WidgetTitle;
|
||||
Widget.Tabs = WidgetTabs;
|
||||
Widget.Taskbar = WidgetTaskbar;
|
||||
Widget.Loading = Loading;
|
||||
|
||||
@@ -26,6 +29,7 @@ export {
|
||||
WidgetBody,
|
||||
WidgetFooter,
|
||||
WidgetTitle,
|
||||
WidgetTabs,
|
||||
WidgetTaskbar,
|
||||
Loading,
|
||||
};
|
||||
|
||||
@@ -179,7 +179,11 @@ function TableActions({ selectedItems }: TableActionsProps) {
|
||||
Remove
|
||||
</Button>
|
||||
|
||||
<Link to="kubernetes.deploy" className="space-left">
|
||||
<Link
|
||||
to="kubernetes.deploy"
|
||||
params={{ referrer: 'kubernetes.services' }}
|
||||
className="space-left hover:no-underline"
|
||||
>
|
||||
<Button className="btn-wrapper" color="primary" icon="plus">
|
||||
Create from manifest
|
||||
</Button>
|
||||
|
||||
@@ -94,7 +94,11 @@ export function IngressDatatable() {
|
||||
</Link>
|
||||
</Authorized>
|
||||
<Authorized authorizations="K8sIngressesW">
|
||||
<Link to="kubernetes.deploy" className="space-left no-decoration">
|
||||
<Link
|
||||
to="kubernetes.deploy"
|
||||
className="space-left no-decoration"
|
||||
params={{ referrer: 'kubernetes.ingresses' }}
|
||||
>
|
||||
<Button icon={Plus} className="btn-wrapper">
|
||||
Create from manifest
|
||||
</Button>
|
||||
|
||||
@@ -40,4 +40,5 @@ export enum FeatureId {
|
||||
K8S_ROLLING_RESTART = 'k8s-rolling-restart',
|
||||
K8SINSTALL = 'k8s-install',
|
||||
K8S_ANNOTATIONS = 'k8s-annotations',
|
||||
K8S_ACCESS_CONTROL = 'k8s-security',
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function init(edition: Edition) {
|
||||
[FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE,
|
||||
[FeatureId.K8S_ROLLING_RESTART]: Edition.BE,
|
||||
[FeatureId.K8S_ANNOTATIONS]: Edition.BE,
|
||||
[FeatureId.K8S_ACCESS_CONTROL]: Edition.BE,
|
||||
};
|
||||
|
||||
state.currentEdition = currentEdition;
|
||||
|
||||
@@ -16,3 +16,4 @@ export const STACK_PULL_IMAGE = 'stack-pull-image';
|
||||
export const STACK_WEBHOOK = 'stack-webhook';
|
||||
export const CONTAINER_WEBHOOK = 'container-webhook';
|
||||
export const K8S_ANNOTATIONS = 'k8s-annotations';
|
||||
export const K8S_ACCESS_CONTROL = 'k8s-security';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Edit, Layers, Lock, Server, Shuffle } from 'lucide-react';
|
||||
import { Box, Edit, Layers, Lock, Server, Shuffle, Shield } from 'lucide-react';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
@@ -100,6 +100,20 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||
data-cy="k8sSidebar-volumes"
|
||||
/>
|
||||
|
||||
<Authorized
|
||||
authorizations="K8sClusterSetupRW"
|
||||
adminOnlyCE
|
||||
environmentId={environmentId}
|
||||
>
|
||||
<SidebarItem
|
||||
to="kubernetes.security"
|
||||
params={{ endpointId: environmentId }}
|
||||
label="Security"
|
||||
data-cy="k8sSidebar-security"
|
||||
icon={Shield}
|
||||
/>
|
||||
</Authorized>
|
||||
|
||||
<SidebarItem
|
||||
label="Cluster"
|
||||
to="kubernetes.cluster"
|
||||
@@ -120,19 +134,6 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||
/>
|
||||
</Authorized>
|
||||
|
||||
<Authorized
|
||||
authorizations="K8sClusterSetupRW"
|
||||
adminOnlyCE
|
||||
environmentId={environmentId}
|
||||
>
|
||||
<SidebarItem
|
||||
to="kubernetes.cluster.securityConstraint"
|
||||
params={{ endpointId: environmentId }}
|
||||
label="Security constraints"
|
||||
data-cy="k8sSidebar-securityConstraints"
|
||||
/>
|
||||
</Authorized>
|
||||
|
||||
<SidebarItem
|
||||
to="kubernetes.registries"
|
||||
params={{ endpointId: environmentId }}
|
||||
|
||||
Reference in New Issue
Block a user