Split UserManagement into seperate Cluster and Namespace Roles and RoleBindings (#2755)

This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-06-09 13:40:55 -04:00
1 parent 1ed7892b26
commit a1a1c240e9
69 files changed
+2896 -901

No files matched your search

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.RoleBindingDetails {
}
@@ -0,0 +1,157 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./details.scss";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { KubeEventDetails } from "../../+events/kube-event-details";
import type { ClusterRoleBinding, ClusterRoleBindingSubject } from "../../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
import { autoBind, ObservableHashSet, prevDefault } from "../../../utils";
import { AddRemoveButtons } from "../../add-remove-buttons";
import { ConfirmDialog } from "../../confirm-dialog";
import { DrawerTitle } from "../../drawer";
import type { KubeObjectDetailsProps } from "../../kube-object";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingsStore } from "./store";
import { hashClusterRoleBindingSubject } from "./hashers";
interface Props extends KubeObjectDetailsProps<ClusterRoleBinding> {
}
@observer
export class ClusterRoleBindingDetails extends React.Component<Props> {
selectedSubjects = new ObservableHashSet<ClusterRoleBindingSubject>([], hashClusterRoleBindingSubject);
constructor(props: Props) {
super(props);
autoBind(this);
}
async componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.selectedSubjects.clear();
})
]);
}
removeSelectedSubjects() {
const { object: clusterRoleBinding } = this.props;
const { selectedSubjects } = this;
ConfirmDialog.open({
ok: () => clusterRoleBindingsStore.removeSubjects(clusterRoleBinding, selectedSubjects),
labelOk: `Remove`,
message: (
<p>Remove selected bindings for <b>{clusterRoleBinding.getName()}</b>?</p>
)
});
}
render() {
const { selectedSubjects } = this;
const { object: clusterRoleBinding } = this.props;
if (!clusterRoleBinding) {
return null;
}
const { roleRef } = clusterRoleBinding;
const subjects = clusterRoleBinding.getSubjects();
return (
<div className="RoleBindingDetails">
<KubeObjectMeta object={clusterRoleBinding} />
<DrawerTitle title="Reference" />
<Table>
<TableHead>
<TableCell>Kind</TableCell>
<TableCell>Name</TableCell>
<TableCell>API Group</TableCell>
</TableHead>
<TableRow>
<TableCell>{roleRef.kind}</TableCell>
<TableCell>{roleRef.name}</TableCell>
<TableCell>{roleRef.apiGroup}</TableCell>
</TableRow>
</Table>
<DrawerTitle title="Bindings" />
{subjects.length > 0 && (
<Table selectable className="bindings box grow">
<TableHead>
<TableCell checkbox />
<TableCell className="binding">Name</TableCell>
<TableCell className="type">Type</TableCell>
</TableHead>
{
subjects.map((subject, i) => {
const { kind, name } = subject;
const isSelected = selectedSubjects.has(subject);
return (
<TableRow
key={i}
selected={isSelected}
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
>
<TableCell checkbox isChecked={isSelected} />
<TableCell className="binding">{name}</TableCell>
<TableCell className="type">{kind}</TableCell>
</TableRow>
);
})
}
</Table>
)}
<AddRemoveButtons
onAdd={() => ClusterRoleBindingDialog.open(clusterRoleBinding)}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
addTooltip={`Add bindings to ${roleRef.name}`}
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
</div>
);
}
}
kubeObjectDetailRegistry.add({
kind: "ClusterRoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <ClusterRoleBindingDetails {...props} />
}
});
kubeObjectDetailRegistry.add({
kind: "ClusterRoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
priority: 5,
components: {
Details: (props) => <KubeEventDetails {...props} />
}
});
@@ -0,0 +1,11 @@
.AddClusterRoleBindingDialog {
.Select + .Select {
margin-top: $margin /2;
}
.Checkbox {
margin-top: $margin;
}
.name {
color: #a0a0a0;
}
}
@@ -0,0 +1,286 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./dialog.scss";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { serviceAccountsStore } from "../+service-accounts/store";
import { ClusterRole, ClusterRoleBinding, ClusterRoleBindingSubject, ServiceAccount } from "../../../api/endpoints";
import { Dialog, DialogProps } from "../../dialog";
import { EditableList } from "../../editable-list";
import { Icon } from "../../icon";
import { showDetails } from "../../kube-object";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Select, SelectOption } from "../../select";
import { Wizard, WizardStep } from "../../wizard";
import { clusterRoleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options";
import { ObservableHashSet, nFircate } from "../../../utils";
import { Input } from "../../input";
interface Props extends Partial<DialogProps> {
}
interface DialogState {
isOpen: boolean;
data?: ClusterRoleBinding;
}
@observer
export class ClusterRoleBindingDialog extends React.Component<Props> {
static state = observable.object<DialogState>({
isOpen: false,
});
constructor(props: Props) {
super(props);
makeObservable(this);
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.isEditing, () => {
this.bindingName = ClusterRoleBindingDialog.state.data?.getName();
}),
]);
}
static open(roleBinding?: ClusterRoleBinding) {
ClusterRoleBindingDialog.state.isOpen = true;
ClusterRoleBindingDialog.state.data = roleBinding;
}
static close() {
ClusterRoleBindingDialog.state.isOpen = false;
}
get clusterRoleBinding(): ClusterRoleBinding {
return ClusterRoleBindingDialog.state.data;
}
@computed get isEditing() {
return !!this.clusterRoleBinding;
}
@observable selectedRoleRef: ClusterRole | undefined = undefined;
@observable bindingName = "";
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
selectedUsers = observable.set<string>([]);
selectedGroups = observable.set<string>([]);
@computed get selectedBindings(): ClusterRoleBindingSubject[] {
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount" as const,
namespace: sa.getNs(),
}));
const users = Array.from(this.selectedUsers, user => ({
name: user,
kind: "User" as const,
}));
const groups = Array.from(this.selectedGroups, group => ({
name: group,
kind: "Group" as const,
}));
return [
...serviceAccounts,
...users,
...groups,
];
}
@computed get clusterRoleRefoptions(): SelectOption<ClusterRole>[] {
return clusterRolesStore.items.map(getRoleRefSelectOption);
}
@computed get serviceAccountOptions(): ServiceAccountOption[] {
return serviceAccountsStore.items.map(account => {
const name = account.getName();
const namespace = account.getNs();
return {
value: `${account.getName()}%${account.getNs()}`,
account,
label: <><Icon small material="account_box" /> {name} ({namespace})</>
};
});
}
@computed get selectedServiceAccountOptions(): ServiceAccountOption[] {
return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account));
}
@action
onOpen = () => {
const binding = this.clusterRoleBinding;
if (!binding) {
return this.reset();
}
this.selectedRoleRef = clusterRolesStore
.items
.find(item => item.getName() === binding.roleRef.name);
this.bindingName = this.clusterRoleBinding.getName();
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
const accountNames = new Set(saSubjects.map(acc => acc.name));
this.selectedAccounts.replace(
serviceAccountsStore.items
.filter(sa => accountNames.has(sa.getName()))
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
this.selectedGroups.replace(gSubjects.map(group => group.name));
};
@action
reset = () => {
this.selectedRoleRef = undefined;
this.bindingName = "";
this.selectedAccounts.clear();
this.selectedUsers.clear();
this.selectedGroups.clear();
};
createBindings = async () => {
const { selectedRoleRef, selectedBindings, bindingName } = this;
try {
const { selfLink } = this.isEditing
? await clusterRoleBindingsStore.updateSubjects(this.clusterRoleBinding, selectedBindings)
: await clusterRoleBindingsStore.create({ name: bindingName }, {
subjects: selectedBindings,
roleRef: {
name: selectedRoleRef.getName(),
kind: selectedRoleRef.kind,
}
});
showDetails(selfLink);
ClusterRoleBindingDialog.close();
} catch (err) {
Notifications.error(err);
}
};
renderContents() {
return (
<>
<SubTitle title="Cluster Role Reference" />
<Select
themeName="light"
placeholder="Select cluster role ..."
isDisabled={this.isEditing}
options={this.clusterRoleRefoptions}
value={this.selectedRoleRef}
onChange={({ value }: SelectOption<ClusterRole> ) => {
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = value.getName();
}
this.selectedRoleRef = value;
}}
/>
<SubTitle title="Binding Name" />
<Input
placeholder="Name of ClusterRoleBinding ..."
disabled={this.isEditing}
value={this.bindingName}
onChange={val => this.bindingName = val}
/>
<SubTitle title="Binding targets" />
<b>Users</b>
<EditableList
placeholder="Bind to User Account ..."
add={(newUser) => this.selectedUsers.add(newUser)}
items={Array.from(this.selectedUsers)}
remove={({ oldItem }) => this.selectedUsers.delete(oldItem)}
/>
<b>Groups</b>
<EditableList
placeholder="Bind to User Group ..."
add={(newGroup) => this.selectedGroups.add(newGroup)}
items={Array.from(this.selectedGroups)}
remove={({ oldItem }) => this.selectedGroups.delete(oldItem)}
/>
<b>Service Accounts</b>
<Select
isMulti
themeName="light"
placeholder="Select service accounts ..."
autoConvertOptions={false}
options={this.serviceAccountOptions}
value={this.selectedServiceAccountOptions}
onChange={(selected: ServiceAccountOption[] | null) => {
if (selected) {
this.selectedAccounts.replace(selected.map(opt => opt.account));
} else {
this.selectedAccounts.clear();
}
}}
maxMenuHeight={200}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const [action, nextLabel] = this.isEditing ? ["Edit", "Update"] : ["Add", "Create"];
const disableNext = !this.selectedRoleRef || !this.selectedBindings.length || !this.bindingName;
return (
<Dialog
{...dialogProps}
className="AddClusterRoleBindingDialog"
isOpen={ClusterRoleBindingDialog.state.isOpen}
close={ClusterRoleBindingDialog.close}
onClose={this.reset}
onOpen={this.onOpen}
>
<Wizard
header={<h5>{action} ClusterRoleBinding</h5>}
done={ClusterRoleBindingDialog.close}
>
<WizardStep
nextLabel={nextLabel}
next={this.createBindings}
disabledNext={disableNext}
>
{this.renderContents()}
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { MD5 } from "crypto-js";
import type { ClusterRoleBindingSubject } from "../../../api/endpoints";
export function hashClusterRoleBindingSubject(subject: ClusterRoleBindingSubject): string {
return MD5(JSON.stringify([
["kind", subject.kind],
["name", subject.name],
["apiGroup", subject.apiGroup],
])).toString();
}
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./view";
export * from "./details";
export * from "./dialog";
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { apiManager } from "../../../api/api-manager";
import { ClusterRoleBinding, clusterRoleBindingApi, ClusterRoleBindingSubject } from "../../../api/endpoints";
import { KubeObjectStore } from "../../../kube-object.store";
import { autoBind, HashSet } from "../../../utils";
import { hashClusterRoleBindingSubject } from "./hashers";
export class ClusterRoleBindingsStore extends KubeObjectStore<ClusterRoleBinding> {
api = clusterRoleBindingApi;
constructor() {
super();
autoBind(this);
}
protected sortItems(items: ClusterRoleBinding[]) {
return super.sortItems(items, [
clusterRoleBinding => clusterRoleBinding.kind,
clusterRoleBinding => clusterRoleBinding.getName()
]);
}
async updateSubjects(clusterRoleBinding: ClusterRoleBinding, subjects: ClusterRoleBindingSubject[]) {
return this.update(clusterRoleBinding, {
roleRef: clusterRoleBinding.roleRef,
subjects,
});
}
async removeSubjects(clusterRoleBinding: ClusterRoleBinding, subjectsToRemove: Iterable<ClusterRoleBindingSubject>) {
const currentSubjects = new HashSet(clusterRoleBinding.getSubjects(), hashClusterRoleBindingSubject);
for (const subject of subjectsToRemove) {
currentSubjects.delete(subject);
}
return this.updateSubjects(clusterRoleBinding, currentSubjects.toJSON());
}
}
export const clusterRoleBindingsStore = new ClusterRoleBindingsStore();
apiManager.registerStore(clusterRoleBindingsStore);
@@ -0,0 +1,11 @@
.ClusterRoleBindings {
.help-icon {
margin-left: $margin / 2;
}
.TableCell {
&.warning {
@include table-cell-warning;
}
}
}
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./view.scss";
import { observer } from "mobx-react";
import React from "react";
import type { RouteComponentProps } from "react-router";
import type { ClusterRoleBinding } from "../../../api/endpoints";
import { KubeObjectListLayout } from "../../kube-object";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { ClusterRoleBindingsRouteParams } from "../user-management.route";
import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
enum columnId {
name = "name",
namespace = "namespace",
bindings = "bindings",
age = "age",
}
interface Props extends RouteComponentProps<ClusterRoleBindingsRouteParams> {
}
@observer
export class ClusterRoleBindings extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="access_cluster_role_bindings"
className="ClusterRoleBindings"
store={clusterRoleBindingsStore}
dependentStores={[clusterRolesStore, serviceAccountsStore]}
sortingCallbacks={{
[columnId.name]: (binding: ClusterRoleBinding) => binding.getName(),
[columnId.bindings]: (binding: ClusterRoleBinding) => binding.getSubjectNames(),
[columnId.age]: (binding: ClusterRoleBinding) => binding.getTimeDiffFromNow(),
}}
searchFilters={[
(binding: ClusterRoleBinding) => binding.getSearchFields(),
(binding: ClusterRoleBinding) => binding.getSubjectNames(),
]}
renderHeaderTitle="Cluster Role Bindings"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(binding: ClusterRoleBinding) => [
binding.getName(),
<KubeObjectStatusIcon key="icon" object={binding} />,
binding.getSubjectNames(),
binding.getAge(),
]}
addRemoveButtons={{
onAdd: () => ClusterRoleBindingDialog.open(),
addTooltip: "Create new ClusterRoleBinding",
}}
/>
<ClusterRoleBindingDialog />
</>
);
}
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.AddRoleDialog {
.AceEditor {
min-height: 200px;
}
}
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./add-dialog.scss";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { Dialog, DialogProps } from "../../dialog";
import { Input } from "../../input";
import { showDetails } from "../../kube-object";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { clusterRolesStore } from "./store";
interface Props extends Partial<DialogProps> {
}
@observer
export class AddClusterRoleDialog extends React.Component<Props> {
static isOpen = observable.box(false);
@observable clusterRoleName = "";
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open() {
AddClusterRoleDialog.isOpen.set(true);
}
static close() {
AddClusterRoleDialog.isOpen.set(false);
}
reset = () => {
this.clusterRoleName = "";
};
createRole = async () => {
try {
const role = await clusterRolesStore.create({ name: this.clusterRoleName });
showDetails(role.selfLink);
this.reset();
AddClusterRoleDialog.close();
} catch (err) {
Notifications.error(err.toString());
}
};
render() {
const { ...dialogProps } = this.props;
return (
<Dialog
{...dialogProps}
className="AddRoleDialog"
isOpen={AddClusterRoleDialog.isOpen.get()}
close={AddClusterRoleDialog.close}
>
<Wizard
header={<h5>Create ClusterRole</h5>}
done={AddClusterRoleDialog.close}
>
<WizardStep
contentClass="flex gaps column"
nextLabel="Create"
next={this.createRole}
>
<SubTitle title="ClusterRole Name" />
<Input
required autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={this.clusterRoleName}
onChange={v => this.clusterRoleName = v}
/>
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.RoleDetails {
.rule {
display: grid;
grid-template-columns: min-content auto;
gap: $margin;
border: 1px solid $borderColor;
border-radius: $radius;
padding: $padding * 1.5;
> .name {
color: $textColorSecondary;
text-align: right;
white-space: nowrap;
}
&:not(:last-child) {
margin-bottom: $margin * 2;
}
}
}
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./details.scss";
import { observer } from "mobx-react";
import React from "react";
import { KubeEventDetails } from "../../+events/kube-event-details";
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
import { DrawerTitle } from "../../drawer";
import type { KubeObjectDetailsProps } from "../../kube-object";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
import type { ClusterRole } from "../../../api/endpoints";
interface Props extends KubeObjectDetailsProps<ClusterRole> {
}
@observer
export class ClusterRoleDetails extends React.Component<Props> {
render() {
const { object: clusterRole } = this.props;
if (!clusterRole) return null;
const rules = clusterRole.getRules();
return (
<div className="ClusterRoleDetails">
<KubeObjectMeta object={clusterRole}/>
<DrawerTitle title="Rules"/>
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
return (
<div className="rule" key={index}>
{resources && (
<>
<div className="name">Resources</div>
<div className="value">{resources.join(", ")}</div>
</>
)}
{verbs && (
<>
<div className="name">Verbs</div>
<div className="value">{verbs.join(", ")}</div>
</>
)}
{apiGroups && (
<>
<div className="name">Api Groups</div>
<div className="value">
{apiGroups
.map(apiGroup => apiGroup === "" ? `'${apiGroup}'` : apiGroup)
.join(", ")
}
</div>
</>
)}
{resourceNames && (
<>
<div className="name">Resource Names</div>
<div className="value">{resourceNames.join(", ")}</div>
</>
)}
</div>
);
})}
</div>
);
}
}
kubeObjectDetailRegistry.add({
kind: "ClusterRole",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <ClusterRoleDetails {...props}/>
}
});
kubeObjectDetailRegistry.add({
kind: "ClusterRole",
apiVersions: ["rbac.authorization.k8s.io/v1"],
priority: 5,
components: {
Details: (props) => <KubeEventDetails {...props}/>
}
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./view";
export * from "./details";
export * from "./add-dialog";
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { apiManager } from "../../../api/api-manager";
import { ClusterRole, clusterRoleApi } from "../../../api/endpoints";
import { KubeObjectStore } from "../../../kube-object.store";
import { autoBind } from "../../../utils";
export class ClusterRolesStore extends KubeObjectStore<ClusterRole> {
api = clusterRoleApi;
constructor() {
super();
autoBind(this);
}
protected sortItems(items: ClusterRole[]) {
return super.sortItems(items, [
clusterRole => clusterRole.kind,
clusterRole => clusterRole.getName(),
]);
}
}
export const clusterRolesStore = new ClusterRolesStore();
apiManager.registerStore(clusterRolesStore);
@@ -0,0 +1,11 @@
.ClusterRoles {
.help-icon {
margin-left: $margin / 2;
}
.TableCell {
&.warning {
@include table-cell-warning;
}
}
}
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./view.scss";
import { observer } from "mobx-react";
import React from "react";
import type { RouteComponentProps } from "react-router";
import type { ClusterRole } from "../../../api/endpoints";
import { KubeObjectListLayout } from "../../kube-object";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { ClusterRolesRouteParams } from "../user-management.route";
import { AddClusterRoleDialog } from "./add-dialog";
import { clusterRolesStore } from "./store";
enum columnId {
name = "name",
namespace = "namespace",
age = "age",
}
interface Props extends RouteComponentProps<ClusterRolesRouteParams> {
}
@observer
export class ClusterRoles extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="access_cluster_roles"
className="ClusterRoles"
store={clusterRolesStore}
sortingCallbacks={{
[columnId.name]: (clusterRole: ClusterRole) => clusterRole.getName(),
[columnId.age]: (clusterRole: ClusterRole) => clusterRole.getTimeDiffFromNow(),
}}
searchFilters={[
(clusterRole: ClusterRole) => clusterRole.getSearchFields(),
]}
renderHeaderTitle="Cluster Roles"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(clusterRole: ClusterRole) => [
clusterRole.getName(),
<KubeObjectStatusIcon key="icon" object={clusterRole} />,
clusterRole.getAge(),
]}
addRemoveButtons={{
onAdd: () => AddClusterRoleDialog.open(),
addTooltip: "Create new ClusterRole",
}}
/>
<AddClusterRoleDialog/>
</>
);
}
}
@@ -0,0 +1,2 @@
.RoleBindingDetails {
}
@@ -0,0 +1,155 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./details.scss";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { KubeEventDetails } from "../../+events/kube-event-details";
import type { RoleBinding, RoleBindingSubject } from "../../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
import { prevDefault, boundMethod } from "../../../utils";
import { AddRemoveButtons } from "../../add-remove-buttons";
import { ConfirmDialog } from "../../confirm-dialog";
import { DrawerTitle } from "../../drawer";
import type { KubeObjectDetailsProps } from "../../kube-object";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../../table";
import { RoleBindingDialog } from "./dialog";
import { roleBindingsStore } from "./store";
import { ObservableHashSet } from "../../../../common/utils/hash-set";
import { hashRoleBindingSubject } from "./hashers";
interface Props extends KubeObjectDetailsProps<RoleBinding> {
}
@observer
export class RoleBindingDetails extends React.Component<Props> {
selectedSubjects = new ObservableHashSet<RoleBindingSubject>([], hashRoleBindingSubject);
async componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.props.object, () => {
this.selectedSubjects.clear();
})
]);
}
@boundMethod
removeSelectedSubjects() {
const { object: roleBinding } = this.props;
const { selectedSubjects } = this;
ConfirmDialog.open({
ok: () => roleBindingsStore.removeSubjects(roleBinding, selectedSubjects.toJSON()),
labelOk: `Remove`,
message: (
<p>Remove selected bindings for <b>{roleBinding.getName()}</b>?</p>
)
});
}
render() {
const { selectedSubjects } = this;
const { object: roleBinding } = this.props;
if (!roleBinding) {
return null;
}
const { roleRef } = roleBinding;
const subjects = roleBinding.getSubjects();
return (
<div className="RoleBindingDetails">
<KubeObjectMeta object={roleBinding} />
<DrawerTitle title="Reference" />
<Table>
<TableHead>
<TableCell>Kind</TableCell>
<TableCell>Name</TableCell>
<TableCell>API Group</TableCell>
</TableHead>
<TableRow>
<TableCell>{roleRef.kind}</TableCell>
<TableCell>{roleRef.name}</TableCell>
<TableCell>{roleRef.apiGroup}</TableCell>
</TableRow>
</Table>
<DrawerTitle title="Bindings" />
{subjects.length > 0 && (
<Table selectable className="bindings box grow">
<TableHead>
<TableCell checkbox />
<TableCell className="binding">Name</TableCell>
<TableCell className="type">Type</TableCell>
<TableCell className="type">Namespace</TableCell>
</TableHead>
{
subjects.map((subject, i) => {
const { kind, name, namespace } = subject;
const isSelected = selectedSubjects.has(subject);
return (
<TableRow
key={i}
selected={isSelected}
onClick={prevDefault(() => this.selectedSubjects.toggle(subject))}
>
<TableCell checkbox isChecked={isSelected} />
<TableCell className="binding">{name}</TableCell>
<TableCell className="type">{kind}</TableCell>
<TableCell className="ns">{namespace || "-"}</TableCell>
</TableRow>
);
})
}
</Table>
)}
<AddRemoveButtons
onAdd={() => RoleBindingDialog.open(roleBinding)}
onRemove={selectedSubjects.size ? this.removeSelectedSubjects : null}
addTooltip={`Edit bindings of ${roleRef.name}`}
removeTooltip={`Remove selected bindings from ${roleRef.name}`}
/>
</div>
);
}
}
kubeObjectDetailRegistry.add({
kind: "RoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleBindingDetails {...props} />
}
});
kubeObjectDetailRegistry.add({
kind: "RoleBinding",
apiVersions: ["rbac.authorization.k8s.io/v1"],
priority: 5,
components: {
Details: (props) => <KubeEventDetails {...props} />
}
});
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.AddRoleBindingDialog {
.Select + .Select {
margin-top: $margin /2;
}
.Checkbox {
margin-top: $margin;
}
.name {
color: #a0a0a0;
}
}
@@ -0,0 +1,303 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./dialog.scss";
import { computed, observable, makeObservable, action } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { rolesStore } from "../+roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { ClusterRole, Role, roleApi, RoleBinding, RoleBindingSubject, ServiceAccount } from "../../../api/endpoints";
import { Dialog, DialogProps } from "../../dialog";
import { EditableList } from "../../editable-list";
import { Icon } from "../../icon";
import { showDetails } from "../../kube-object";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Select, SelectOption } from "../../select";
import { Wizard, WizardStep } from "../../wizard";
import { roleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { Input } from "../../input";
import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options";
import { ObservableHashSet, nFircate } from "../../../utils";
interface Props extends Partial<DialogProps> {
}
interface DialogState {
isOpen: boolean;
data?: RoleBinding;
}
@observer
export class RoleBindingDialog extends React.Component<Props> {
static state = observable.object<DialogState>({
isOpen: false,
});
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open(roleBinding?: RoleBinding) {
RoleBindingDialog.state.isOpen = true;
RoleBindingDialog.state.data = roleBinding;
}
static close() {
RoleBindingDialog.state.isOpen = false;
RoleBindingDialog.state.data = undefined;
}
get roleBinding(): RoleBinding {
return RoleBindingDialog.state.data;
}
@computed get isEditing() {
return !!this.roleBinding;
}
@observable.ref selectedRoleRef: Role | ClusterRole | undefined = undefined;
@observable bindingName = "";
@observable bindingNamespace = "";
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
selectedUsers = observable.set<string>([]);
selectedGroups = observable.set<string>([]);
@computed get selectedBindings(): RoleBindingSubject[] {
const serviceAccounts = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount" as const,
namespace: this.bindingNamespace,
}));
const users = Array.from(this.selectedUsers, user => ({
name: user,
kind: "User" as const,
namespace: this.bindingNamespace,
}));
const groups = Array.from(this.selectedGroups, group => ({
name: group,
kind: "Group" as const,
namespace: this.bindingNamespace,
}));
return [
...serviceAccounts,
...users,
...groups,
];
}
@computed get roleRefOptions(): SelectOption<Role | ClusterRole>[] {
const roles = rolesStore.items
.filter(role => role.getNs() === this.bindingNamespace)
.map(getRoleRefSelectOption);
const clusterRoles = clusterRolesStore.items
.map(getRoleRefSelectOption);
return [
...roles,
...clusterRoles,
];
}
@computed get serviceAccountOptions(): ServiceAccountOption[] {
return serviceAccountsStore.items.map(account => {
const name = account.getName();
const namespace = account.getNs();
return {
value: `${account.getName()}%${account.getNs()}`,
account,
label: <><Icon small material="account_box" /> {name} ({namespace})</>
};
});
}
@computed get selectedServiceAccountOptions(): ServiceAccountOption[] {
return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account));
}
@action
onOpen = () => {
const binding = this.roleBinding;
if (!binding) {
return this.reset();
}
this.selectedRoleRef = (binding.roleRef.kind === roleApi.kind ? rolesStore : clusterRolesStore)
.items
.find(item => item.getName() === binding.roleRef.name);
this.bindingName = binding.getName();
this.bindingNamespace = binding.getNs();
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
const accountNames = new Set(saSubjects.map(acc => acc.name));
this.selectedAccounts.replace(
serviceAccountsStore.items
.filter(sa => accountNames.has(sa.getName()))
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
this.selectedGroups.replace(gSubjects.map(group => group.name));
};
@action
reset = () => {
this.selectedRoleRef = undefined;
this.bindingName = "";
this.bindingNamespace = "";
this.selectedAccounts.clear();
this.selectedUsers.clear();
this.selectedGroups.clear();
};
createBindings = async () => {
const { selectedRoleRef, bindingNamespace: namespace, selectedBindings } = this;
try {
const roleBinding = this.isEditing
? await roleBindingsStore.updateSubjects(this.roleBinding, selectedBindings)
: await roleBindingsStore.create({ name: this.bindingName, namespace }, {
subjects: selectedBindings,
roleRef: {
name: selectedRoleRef.getName(),
kind: selectedRoleRef.kind,
}
});
showDetails(roleBinding.selfLink);
RoleBindingDialog.close();
} catch (err) {
Notifications.error(err);
}
};
renderContents() {
return (
<>
<SubTitle title="Namespace" />
<NamespaceSelect
themeName="light"
isDisabled={this.isEditing}
value={this.bindingNamespace}
onChange={({ value }) => this.bindingNamespace = value}
/>
<SubTitle title="Role Reference" />
<Select
themeName="light"
placeholder="Select role or cluster role ..."
isDisabled={this.isEditing}
options={this.roleRefOptions}
value={this.selectedRoleRef}
onChange={({ value }) => {
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = value.getName();
}
this.selectedRoleRef = value;
}}
/>
<SubTitle title="Binding Name" />
<Input
disabled={this.isEditing}
value={this.bindingName}
onChange={value => this.bindingName = value}
/>
<SubTitle title="Binding targets" />
<b>Users</b>
<EditableList
placeholder="Bind to User Account ..."
add={(newUser) => this.selectedUsers.add(newUser)}
items={Array.from(this.selectedUsers)}
remove={({ oldItem }) => this.selectedUsers.delete(oldItem)}
/>
<b>Groups</b>
<EditableList
placeholder="Bind to User Group ..."
add={(newGroup) => this.selectedGroups.add(newGroup)}
items={Array.from(this.selectedGroups)}
remove={({ oldItem }) => this.selectedGroups.delete(oldItem)}
/>
<b>Service Accounts</b>
<Select
isMulti
themeName="light"
placeholder="Select service accounts ..."
autoConvertOptions={false}
options={this.serviceAccountOptions}
value={this.selectedServiceAccountOptions}
onChange={(selected: ServiceAccountOption[] | null) => {
if (selected) {
this.selectedAccounts.replace(selected.map(opt => opt.account));
} else {
this.selectedAccounts.clear();
}
}}
maxMenuHeight={200}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const [action, nextLabel] = this.isEditing ? ["Edit", "Update"] : ["Add", "Create"];
const disableNext = !this.selectedRoleRef || !this.selectedBindings.length || !this.bindingNamespace || !this.bindingName;
return (
<Dialog
{...dialogProps}
className="AddRoleBindingDialog"
isOpen={RoleBindingDialog.state.isOpen}
close={RoleBindingDialog.close}
onClose={this.reset}
onOpen={this.onOpen}
>
<Wizard
header={<h5>{action} RoleBinding</h5>}
done={RoleBindingDialog.close}
>
<WizardStep
nextLabel={nextLabel}
next={this.createBindings}
disabledNext={disableNext}
>
{this.renderContents()}
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { MD5 } from "crypto-js";
import type { RoleBindingSubject } from "../../../api/endpoints";
export function hashRoleBindingSubject(subject: RoleBindingSubject): string {
return MD5(JSON.stringify([
["kind", subject.kind],
["name", subject.name],
["namespace", subject.namespace],
["apiGroup", subject.apiGroup],
])).toString();
}
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./view";
export * from "./details";
export * from "./dialog";
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { apiManager } from "../../../api/api-manager";
import { RoleBinding, roleBindingApi, RoleBindingSubject } from "../../../api/endpoints";
import { KubeObjectStore } from "../../../kube-object.store";
import { HashSet } from "../../../utils";
import { hashRoleBindingSubject } from "./hashers";
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
api = roleBindingApi;
protected sortItems(items: RoleBinding[]) {
return super.sortItems(items, [
roleBinding => roleBinding.kind,
roleBinding => roleBinding.getName()
]);
}
protected async createItem(params: { name: string; namespace: string }, data?: Partial<RoleBinding>) {
return roleBindingApi.create(params, data);
}
async updateSubjects(roleBinding: RoleBinding, subjects: RoleBindingSubject[]) {
return this.update(roleBinding, {
roleRef: roleBinding.roleRef,
subjects,
});
}
async removeSubjects(roleBinding: RoleBinding, subjectsToRemove: Iterable<RoleBindingSubject>) {
const currentSubjects = new HashSet(roleBinding.getSubjects(), hashRoleBindingSubject);
for (const subject of subjectsToRemove) {
currentSubjects.delete(subject);
}
return this.updateSubjects(roleBinding, currentSubjects.toJSON());
}
}
export const roleBindingsStore = new RoleBindingsStore();
apiManager.registerStore(roleBindingsStore);
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.RoleBindings {
.help-icon {
margin-left: $margin / 2;
}
.TableCell {
&.warning {
@include table-cell-warning;
}
}
}
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./view.scss";
import { observer } from "mobx-react";
import React from "react";
import type { RouteComponentProps } from "react-router";
import type { RoleBinding } from "../../../api/endpoints";
import { KubeObjectListLayout } from "../../kube-object";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { RoleBindingsRouteParams } from "../user-management.route";
import { RoleBindingDialog } from "./dialog";
import { roleBindingsStore } from "./store";
import { rolesStore } from "../+roles/store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
enum columnId {
name = "name",
namespace = "namespace",
bindings = "bindings",
age = "age",
}
interface Props extends RouteComponentProps<RoleBindingsRouteParams> {
}
@observer
export class RoleBindings extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="access_role_bindings"
className="RoleBindings"
store={roleBindingsStore}
dependentStores={[rolesStore, clusterRolesStore, serviceAccountsStore]}
sortingCallbacks={{
[columnId.name]: (binding: RoleBinding) => binding.getName(),
[columnId.namespace]: (binding: RoleBinding) => binding.getNs(),
[columnId.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
[columnId.age]: (binding: RoleBinding) => binding.getTimeDiffFromNow(),
}}
searchFilters={[
(binding: RoleBinding) => binding.getSearchFields(),
(binding: RoleBinding) => binding.getSubjectNames(),
]}
renderHeaderTitle="Role Bindings"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Bindings", className: "bindings", sortBy: columnId.bindings, id: columnId.bindings },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(binding: RoleBinding) => [
binding.getName(),
<KubeObjectStatusIcon key="icon" object={binding} />,
binding.getNs(),
binding.getSubjectNames(),
binding.getAge(),
]}
addRemoveButtons={{
onAdd: () => RoleBindingDialog.open(),
addTooltip: "Create new RoleBinding",
}}
/>
<RoleBindingDialog />
</>
);
}
}
@@ -0,0 +1,5 @@
.AddRoleDialog {
.AceEditor {
min-height: 200px;
}
}
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./add-dialog.scss";
import React from "react";
import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { Dialog, DialogProps } from "../../dialog";
import { Input } from "../../input";
import { showDetails } from "../../kube-object";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { rolesStore } from "./store";
interface Props extends Partial<DialogProps> {
}
@observer
export class AddRoleDialog extends React.Component<Props> {
static isOpen = observable.box(false);
@observable roleName = "";
@observable namespace = "";
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open() {
AddRoleDialog.isOpen.set(true);
}
static close() {
AddRoleDialog.isOpen.set(false);
}
reset = () => {
this.roleName = "";
this.namespace = "";
};
createRole = async () => {
try {
const role = await rolesStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink);
this.reset();
AddRoleDialog.close();
} catch (err) {
Notifications.error(err.toString());
}
};
render() {
const { ...dialogProps } = this.props;
const header = <h5>Create Role</h5>;
return (
<Dialog
{...dialogProps}
className="AddRoleDialog"
isOpen={AddRoleDialog.isOpen.get()}
close={AddRoleDialog.close}
>
<Wizard header={header} done={AddRoleDialog.close}>
<WizardStep
contentClass="flex gaps column"
nextLabel="Create"
next={this.createRole}
>
<SubTitle title="Role Name" />
<Input
required autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={this.roleName}
onChange={v => this.roleName = v}
/>
<SubTitle title="Namespace" />
<NamespaceSelect
themeName="light"
value={this.namespace}
onChange={({ value }) => this.namespace = value}
/>
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -0,0 +1,21 @@
.RoleDetails {
.rule {
display: grid;
grid-template-columns: min-content auto;
gap: $margin;
border: 1px solid $borderColor;
border-radius: $radius;
padding: $padding * 1.5;
> .name {
color: $textColorSecondary;
text-align: right;
white-space: nowrap;
}
&:not(:last-child) {
margin-bottom: $margin * 2;
}
}
}
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./details.scss";
import { observer } from "mobx-react";
import React from "react";
import { KubeEventDetails } from "../../+events/kube-event-details";
import type { Role } from "../../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
import { DrawerTitle } from "../../drawer";
import type { KubeObjectDetailsProps } from "../../kube-object";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps<Role> {
}
@observer
export class RoleDetails extends React.Component<Props> {
render() {
const { object: role } = this.props;
if (!role) return null;
const rules = role.getRules();
return (
<div className="RoleDetails">
<KubeObjectMeta object={role}/>
<DrawerTitle title="Rules"/>
{rules.map(({ resourceNames, apiGroups, resources, verbs }, index) => {
return (
<div className="rule" key={index}>
{resources && (
<>
<div className="name">Resources</div>
<div className="value">{resources.join(", ")}</div>
</>
)}
{verbs && (
<>
<div className="name">Verbs</div>
<div className="value">{verbs.join(", ")}</div>
</>
)}
{apiGroups && (
<>
<div className="name">Api Groups</div>
<div className="value">
{apiGroups
.map(apiGroup => apiGroup === "" ? `'${apiGroup}'` : apiGroup)
.join(", ")
}
</div>
</>
)}
{resourceNames && (
<>
<div className="name">Resource Names</div>
<div className="value">{resourceNames.join(", ")}</div>
</>
)}
</div>
);
})}
</div>
);
}
}
kubeObjectDetailRegistry.add({
kind: "Role",
apiVersions: ["rbac.authorization.k8s.io/v1"],
components: {
Details: (props) => <RoleDetails {...props}/>
}
});
kubeObjectDetailRegistry.add({
kind: "Role",
apiVersions: ["rbac.authorization.k8s.io/v1"],
priority: 5,
components: {
Details: (props) => <KubeEventDetails {...props} />
}
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./view";
export * from "./details";
export * from "./add-dialog";
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { apiManager } from "../../../api/api-manager";
import { Role, roleApi } from "../../../api/endpoints";
import { KubeObjectStore } from "../../../kube-object.store";
import { autoBind } from "../../../utils";
export class RolesStore extends KubeObjectStore<Role> {
api = roleApi;
constructor() {
super();
autoBind(this);
}
protected sortItems(items: Role[]) {
return super.sortItems(items, [
role => role.kind,
role => role.getName(),
]);
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<Role>) {
return roleApi.create(params, data);
}
}
export const rolesStore = new RolesStore();
apiManager.registerStore(rolesStore);
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.Roles {
.help-icon {
margin-left: $margin / 2;
}
.TableCell {
&.warning {
@include table-cell-warning;
}
}
}
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./view.scss";
import { observer } from "mobx-react";
import React from "react";
import type { RouteComponentProps } from "react-router";
import type { Role } from "../../../api/endpoints";
import { KubeObjectListLayout } from "../../kube-object";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { RolesRouteParams } from "../user-management.route";
import { AddRoleDialog } from "./add-dialog";
import { rolesStore } from "./store";
enum columnId {
name = "name",
namespace = "namespace",
age = "age",
}
interface Props extends RouteComponentProps<RolesRouteParams> {
}
@observer
export class Roles extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="access_roles"
className="Roles"
store={rolesStore}
sortingCallbacks={{
[columnId.name]: (role: Role) => role.getName(),
[columnId.namespace]: (role: Role) => role.getNs(),
[columnId.age]: (role: Role) => role.getTimeDiffFromNow(),
}}
searchFilters={[
(role: Role) => role.getSearchFields(),
]}
renderHeaderTitle="Roles"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(role: Role) => [
role.getName(),
<KubeObjectStatusIcon key="icon" object={role} />,
role.getNs(),
role.getAge(),
]}
addRemoveButtons={{
onAdd: () => AddRoleDialog.open(),
addTooltip: "Create new Role",
}}
/>
<AddRoleDialog/>
</>
);
}
}
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.CreateServiceAccountDialog {
}
@@ -0,0 +1,107 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./create-dialog.scss";
import React from "react";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import { Dialog, DialogProps } from "../../dialog";
import { Input } from "../../input";
import { systemName } from "../../input/input_validators";
import { showDetails } from "../../kube-object";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { serviceAccountsStore } from "./store";
interface Props extends Partial<DialogProps> {
}
@observer
export class CreateServiceAccountDialog extends React.Component<Props> {
static isOpen = observable.box(false);
@observable name = "";
@observable namespace = "default";
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open() {
CreateServiceAccountDialog.isOpen.set(true);
}
static close() {
CreateServiceAccountDialog.isOpen.set(false);
}
createAccount = async () => {
const { name, namespace } = this;
try {
const serviceAccount = await serviceAccountsStore.create({ namespace, name });
this.name = "";
showDetails(serviceAccount.selfLink);
CreateServiceAccountDialog.close();
} catch (err) {
Notifications.error(err);
}
};
render() {
const { ...dialogProps } = this.props;
const { name, namespace } = this;
const header = <h5>Create Service Account</h5>;
return (
<Dialog
{...dialogProps}
className="CreateServiceAccountDialog"
isOpen={CreateServiceAccountDialog.isOpen.get()}
close={CreateServiceAccountDialog.close}
>
<Wizard header={header} done={CreateServiceAccountDialog.close}>
<WizardStep nextLabel="Create" next={this.createAccount}>
<SubTitle title="Account Name" />
<Input
autoFocus required
placeholder="Enter a name"
validators={systemName}
value={name} onChange={v => this.name = v.toLowerCase()}
/>
<SubTitle title="Namespace" />
<NamespaceSelect
themeName="light"
value={namespace}
onChange={({ value }) => this.namespace = value}
/>
</WizardStep>
</Wizard>
</Dialog>
);
}
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.ServiceAccountsDetails {
.links {
a {
margin-right: $margin;
}
}
}
@@ -0,0 +1,182 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./details.scss";
import { autorun, observable, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Link } from "react-router-dom";
import { secretsStore } from "../../+config-secrets/secrets.store";
import { KubeEventDetails } from "../../+events/kube-event-details";
import { Secret, ServiceAccount } from "../../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../../api/kube-object-detail-registry";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { Icon } from "../../icon";
import { getDetailsUrl, KubeObjectDetailsProps } from "../../kube-object";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
import { Spinner } from "../../spinner";
import { ServiceAccountsSecret } from "./secret";
interface Props extends KubeObjectDetailsProps<ServiceAccount> {
}
@observer
export class ServiceAccountsDetails extends React.Component<Props> {
@observable secrets: Secret[];
@observable imagePullSecrets: Secret[];
@disposeOnUnmount
loadSecrets = autorun(async () => {
this.secrets = null;
this.imagePullSecrets = null;
const { object: serviceAccount } = this.props;
if (!serviceAccount) {
return;
}
const namespace = serviceAccount.getNs();
const secrets = serviceAccount.getSecrets().map(({ name }) => {
return secretsStore.load({ name, namespace });
});
this.secrets = await Promise.all(secrets);
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async({ name }) => {
return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name));
});
this.imagePullSecrets = await Promise.all(imagePullSecrets);
});
constructor(props: Props) {
super(props);
makeObservable(this);
}
renderSecrets() {
const { secrets } = this;
if (!secrets) {
return <Spinner center/>;
}
return secrets.map(secret =>
<ServiceAccountsSecret key={secret.getId()} secret={secret}/>
);
}
renderImagePullSecrets() {
const { imagePullSecrets } = this;
if (!imagePullSecrets) {
return <Spinner center/>;
}
return this.renderSecretLinks(imagePullSecrets);
}
renderSecretLinks(secrets: Secret[]) {
return secrets.map((secret) => {
if (secret.getId() === null) {
return (
<div key={secret.getName()}>
{secret.getName()}
<Icon
small material="warning"
tooltip="Secret is not found"
/>
</div>
);
}
return (
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
{secret.getName()}
</Link>
);
});
}
generateDummySecretObject(name: string) {
return new Secret({
apiVersion: "v1",
kind: "Secret",
metadata: {
name,
uid: null,
selfLink: null,
resourceVersion: null
}
});
}
render() {
const { object: serviceAccount } = this.props;
if (!serviceAccount) {
return null;
}
const tokens = secretsStore.items.filter(secret =>
secret.getNs() == serviceAccount.getNs() &&
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
);
const imagePullSecrets = serviceAccount.getImagePullSecrets();
return (
<div className="ServiceAccountsDetails">
<KubeObjectMeta object={serviceAccount}/>
{tokens.length > 0 &&
<DrawerItem name="Tokens" className="links">
{this.renderSecretLinks(tokens)}
</DrawerItem>
}
{imagePullSecrets.length > 0 &&
<DrawerItem name="ImagePullSecrets" className="links">
{this.renderImagePullSecrets()}
</DrawerItem>
}
<DrawerTitle title="Mountable secrets"/>
<div className="secrets">
{this.renderSecrets()}
</div>
</div>
);
}
}
kubeObjectDetailRegistry.add({
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
Details: (props) => <ServiceAccountsDetails {...props} />
}
});
kubeObjectDetailRegistry.add({
kind: "ServiceAccount",
apiVersions: ["v1"],
priority: 5,
components: {
Details: (props) => <KubeEventDetails {...props} />
}
});
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./view";
export * from "./details";
export * from "./create-dialog";
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.ServiceAccountsSecret {
margin-bottom: $margin * 3;
&:nth-child(even) {
margin-left: -$margin * 3;
margin-right: -$margin * 3;
padding: $padding $padding * 3;
}
.secret-row {
display: flex;
border-bottom: 1px solid $borderFaintColor;
padding: $padding 0;
&:first-child {
padding-top: 0
}
}
.name {
flex-basis: 23%;
color: $drawerItemNameColor;
}
.value {
flex-basis: 76%;
color: $drawerItemValueColor;
word-break: break-all;
&:empty:after {
content: '—'
}
}
.asterisks {
font-size: large;
margin-right: $margin / 2;
line-height: 90%;
}
}
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./secret.scss";
import moment from "moment";
import React from "react";
import type { Secret } from "../../../api/endpoints/secret.api";
import { prevDefault } from "../../../utils";
import { Icon } from "../../icon";
interface Props {
secret: Secret;
}
interface State {
showToken: boolean;
}
export class ServiceAccountsSecret extends React.Component<Props, State> {
public state: State = {
showToken: false,
};
renderSecretValue() {
const { secret } = this.props;
const { showToken } = this.state;
return (
<>
{!showToken && (
<>
<span className="asterisks">{Array(16).fill("•").join("")}</span>
<Icon
small material="lock_open"
tooltip="Show value"
onClick={prevDefault(() => this.setState({ showToken: true }))}
/>
</>
)}
{showToken && (
<span className="raw-value">{secret.getToken()}</span>
)}
</>
);
}
render() {
const { metadata: { name, creationTimestamp }, type } = this.props.secret;
return (
<div className="ServiceAccountsSecret box grow-fixed">
<div className="secret-row">
<span className="name">Name: </span>
<span className="value">{name}</span>
</div>
<div className="secret-row">
<span className="name">Value: </span>
<span className="value flex align-center">{this.renderSecretValue()}</span>
</div>
<div className="secret-row">
<span className="name">Created at: </span>
<span className="value" title={creationTimestamp}>
{moment(creationTimestamp).format("LLL")}
</span>
</div>
<div className="secret-row">
<span className="name">Type: </span>
<span className="value">{type}</span>
</div>
</div>
);
}
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { apiManager } from "../../../api/api-manager";
import { ServiceAccount, serviceAccountsApi } from "../../../api/endpoints";
import { KubeObjectStore } from "../../../kube-object.store";
import { autoBind } from "../../../utils";
export class ServiceAccountsStore extends KubeObjectStore<ServiceAccount> {
api = serviceAccountsApi;
constructor() {
super();
autoBind(this);
}
protected async createItem(params: { name: string; namespace?: string }) {
await super.createItem(params);
return this.api.get(params); // hackfix: load freshly created account, cause it doesn't have "secrets" field yet
}
}
export const serviceAccountsStore = new ServiceAccountsStore();
apiManager.registerStore(serviceAccountsStore);
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.ServiceAccounts {
.TableCell {
&.warning {
@include table-cell-warning;
}
}
}
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "./view.scss";
import { observer } from "mobx-react";
import React from "react";
import type { RouteComponentProps } from "react-router";
import type { ServiceAccountsRouteParams } from "../user-management.route";
import { kubeObjectMenuRegistry } from "../../../../extensions/registries/kube-object-menu-registry";
import type { ServiceAccount } from "../../../api/endpoints/service-accounts.api";
import { Icon } from "../../icon";
import { KubeObjectListLayout } from "../../kube-object";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import type { KubeObjectMenuProps } from "../../kube-object/kube-object-menu";
import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog";
import { MenuItem } from "../../menu";
import { CreateServiceAccountDialog } from "./create-dialog";
import { serviceAccountsStore } from "./store";
enum columnId {
name = "name",
namespace = "namespace",
age = "age",
}
interface Props extends RouteComponentProps<ServiceAccountsRouteParams> {
}
@observer
export class ServiceAccounts extends React.Component<Props> {
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="access_service_accounts"
className="ServiceAccounts" store={serviceAccountsStore}
sortingCallbacks={{
[columnId.name]: (account: ServiceAccount) => account.getName(),
[columnId.namespace]: (account: ServiceAccount) => account.getNs(),
[columnId.age]: (account: ServiceAccount) => account.getTimeDiffFromNow(),
}}
searchFilters={[
(account: ServiceAccount) => account.getSearchFields(),
]}
renderHeaderTitle="Service Accounts"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={(account: ServiceAccount) => [
account.getName(),
<KubeObjectStatusIcon key="icon" object={account} />,
account.getNs(),
account.getAge(),
]}
renderItemMenu={(item: ServiceAccount) => {
return <ServiceAccountMenu object={item}/>;
}}
addRemoveButtons={{
onAdd: () => CreateServiceAccountDialog.open(),
addTooltip: "Create new Service Account",
}}
/>
<CreateServiceAccountDialog/>
</>
);
}
}
function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
const { object, toolbar } = props;
return (
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
<Icon material="insert_drive_file" title="Kubeconfig File" interactive={toolbar}/>
<span className="title">Kubeconfig</span>
</MenuItem>
);
}
kubeObjectMenuRegistry.add({
kind: "ServiceAccount",
apiVersions: ["v1"],
components: {
MenuItem: ServiceAccountMenu
}
});
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import type { ServiceAccount } from "../../api/endpoints";
import type { KubeObject } from "../../api/kube-object";
import { Icon } from "../icon";
import type { SelectOption } from "../select";
import { TooltipPosition } from "../tooltip";
export type ServiceAccountOption = SelectOption<string> & { account: ServiceAccount };
export function getRoleRefSelectOption<T extends KubeObject>(item: T): SelectOption<T> {
return {
value: item,
label: (
<>
<Icon
small
material={item.kind === "Role" ? "person" : "people"}
tooltip={{
preferredPositions: TooltipPosition.LEFT,
children: item.kind
}}
/>
{" "}
{item.getName()}
</>
),
};
}
@@ -19,45 +19,62 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { RouteProps } from "react-router";
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
import type { RouteProps } from "react-router";
// Routes
export const serviceAccountsRoute: RouteProps = {
path: "/service-accounts"
};
export const podSecurityPoliciesRoute: RouteProps = {
path: "/pod-security-policies"
};
export const rolesRoute: RouteProps = {
path: "/roles"
};
export const clusterRolesRoute: RouteProps = {
path: "/cluster-roles"
};
export const roleBindingsRoute: RouteProps = {
path: "/role-bindings"
};
export const podSecurityPoliciesRoute: RouteProps = {
path: "/pod-security-policies"
export const clusterRoleBindingsRoute: RouteProps = {
path: "/cluster-role-bindings"
};
export const usersManagementRoute: RouteProps = {
path: [
serviceAccountsRoute,
podSecurityPoliciesRoute,
roleBindingsRoute,
clusterRoleBindingsRoute,
rolesRoute,
podSecurityPoliciesRoute
clusterRolesRoute,
].map(route => route.path.toString())
};
// Route params
export interface IServiceAccountsRouteParams {
export interface ServiceAccountsRouteParams {
}
export interface IRoleBindingsRouteParams {
export interface RoleBindingsRouteParams {
}
export interface IRolesRouteParams {
export interface ClusterRoleBindingsRouteParams {
}
export interface RolesRouteParams {
}
export interface ClusterRolesRouteParams {
}
// URL-builders
export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params);
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path);
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path);
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path);
export const serviceAccountsURL = buildURL<ServiceAccountsRouteParams>(serviceAccountsRoute.path);
export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path);
export const roleBindingsURL = buildURL<RoleBindingsRouteParams>(roleBindingsRoute.path);
export const clusterRoleBindingsURL = buildURL<ClusterRoleBindingsRouteParams>(clusterRoleBindingsRoute.path);
export const rolesURL = buildURL<RoleBindingsRouteParams>(rolesRoute.path);
export const clusterRolesURL = buildURL<ClusterRoleBindingsRouteParams>(clusterRolesRoute.path);
@@ -20,15 +20,32 @@
*/
import "./user-management.scss";
import React from "react";
import { observer } from "mobx-react";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { Roles } from "../+user-management-roles";
import { RoleBindings } from "../+user-management-roles-bindings";
import { ServiceAccounts } from "../+user-management-service-accounts";
import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
import React from "react";
import { PodSecurityPolicies } from "../+pod-security-policies";
import { isAllowedResource } from "../../../common/rbac";
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { ClusterRoles } from "./+cluster-roles";
import { ClusterRoleBindings } from "./+cluster-role-bindings";
import { Roles } from "./+roles";
import { RoleBindings } from "./+role-bindings";
import { ServiceAccounts } from "./+service-accounts";
import {
clusterRoleBindingsRoute,
clusterRoleBindingsURL,
clusterRolesRoute,
clusterRolesURL,
podSecurityPoliciesRoute,
podSecurityPoliciesURL,
roleBindingsRoute,
roleBindingsURL,
rolesRoute,
rolesURL,
serviceAccountsRoute,
serviceAccountsURL,
} from "./user-management.route";
@observer
export class UserManagement extends React.Component {
@@ -44,18 +61,16 @@ export class UserManagement extends React.Component {
});
}
if (isAllowedResource("rolebindings") || isAllowedResource("clusterrolebindings")) {
// TODO: seperate out these two pages
if (isAllowedResource("clusterroles")) {
tabRoutes.push({
title: "Role Bindings",
component: RoleBindings,
url: roleBindingsURL(),
routePath: roleBindingsRoute.path.toString(),
title: "Cluster Roles",
component: ClusterRoles,
url: clusterRolesURL(),
routePath: clusterRolesRoute.path.toString(),
});
}
if (isAllowedResource("roles") || isAllowedResource("clusterroles")) {
// TODO: seperate out these two pages
if (isAllowedResource("roles")) {
tabRoutes.push({
title: "Roles",
component: Roles,
@@ -64,6 +79,24 @@ export class UserManagement extends React.Component {
});
}
if (isAllowedResource("clusterrolebindings")) {
tabRoutes.push({
title: "Cluster Role Bindings",
component: ClusterRoleBindings,
url: clusterRoleBindingsURL(),
routePath: clusterRoleBindingsRoute.path.toString(),
});
}
if (isAllowedResource("rolebindings")) {
tabRoutes.push({
title: "Role Bindings",
component: RoleBindings,
url: roleBindingsURL(),
routePath: roleBindingsRoute.path.toString(),
});
}
if (isAllowedResource("podsecuritypolicies")) {
tabRoutes.push({
title: "Pod Security Policies",