From ebf686c00a1ff15a8aa0b7400b13beb7931915ae Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Sun, 6 Oct 2024 20:08:01 +0200 Subject: [PATCH 1/6] Add Kubernetes access section to the role editor --- api/proto/teleport/legacy/types/types.proto | 5 +- api/types/constants.go | 8 +- api/types/types.pb.go | 5 +- .../Roles/RoleEditor/StandardEditor.test.tsx | 159 ++++++++++- .../src/Roles/RoleEditor/StandardEditor.tsx | 188 ++++++++++++- .../Roles/RoleEditor/standardmodel.test.ts | 168 +++++++++++- .../src/Roles/RoleEditor/standardmodel.ts | 257 ++++++++++++++++-- .../teleport/src/services/resources/types.ts | 10 + 8 files changed, 746 insertions(+), 54 deletions(-) diff --git a/api/proto/teleport/legacy/types/types.proto b/api/proto/teleport/legacy/types/types.proto index 466795336b066..18021b60b34e3 100644 --- a/api/proto/teleport/legacy/types/types.proto +++ b/api/proto/teleport/legacy/types/types.proto @@ -3304,8 +3304,9 @@ message DatabasePermission { // KubernetesResource is the Kubernetes resource identifier. message KubernetesResource { - // Kind specifies the Kubernetes Resource type. - // At the moment only "pod" is supported. + // Kind specifies the Kubernetes Resource type. See + // `KubernetesResourcesKinds` in `api/types/constants.go` for the list of + // supported values. string Kind = 1 [(gogoproto.jsontag) = "kind,omitempty"]; // Namespace is the resource namespace. // It supports wildcards. diff --git a/api/types/constants.go b/api/types/constants.go index 85dbe7c140388..1066f131d8823 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1247,7 +1247,11 @@ var RequestableResourceKinds = []string{ KindSAMLIdPServiceProvider, } -// KubernetesResourcesKinds lists the supported Kubernetes resource kinds. +// KubernetesResourcesKinds lists the supported Kubernetes resource kinds. This +// is for the latest version of Role resources; roles whose version is set to +// v6 or prior only support [KindKubePod]. +// This list needs to be kept in sync with `kubernetesResourceKindOptions` in +// `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. var KubernetesResourcesKinds = []string{ KindKubePod, KindKubeSecret, @@ -1296,6 +1300,8 @@ const ( ) // KubernetesVerbs lists the supported Kubernetes verbs. +// This list needs to be kept in sync with `kubernetesResourceVerbOptions` in +// `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. var KubernetesVerbs = []string{ Wildcard, KubeVerbGet, diff --git a/api/types/types.pb.go b/api/types/types.pb.go index 14f5bfd930c01..d74a033288a18 100644 --- a/api/types/types.pb.go +++ b/api/types/types.pb.go @@ -8302,8 +8302,9 @@ var xxx_messageInfo_DatabasePermission proto.InternalMessageInfo // KubernetesResource is the Kubernetes resource identifier. type KubernetesResource struct { - // Kind specifies the Kubernetes Resource type. - // At the moment only "pod" is supported. + // Kind specifies the Kubernetes Resource type. See + // `KubernetesResourcesKinds` in `api/types/constants.go` for the list of + // supported values. Kind string `protobuf:"bytes,1,opt,name=Kind,proto3" json:"kind,omitempty"` // Namespace is the resource namespace. // It supports wildcards. diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx index 5f22d2bfd6635..8ee9abd04ddbb 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx @@ -27,12 +27,16 @@ import TeleportContextProvider from 'teleport/TeleportContextProvider'; import { createTeleportContext } from 'teleport/mocks/contexts'; import { + AccessSpec, + KubernetesAccessSpec, newRole, roleToRoleEditorModel, ServerAccessSpec, StandardEditorModel, } from './standardmodel'; import { + KubernetesAccessSpecSection, + SectionProps, ServerAccessSpecSection, StandardEditor, StandardEditorProps, @@ -126,19 +130,19 @@ const getSectionByName = (name: string) => // eslint-disable-next-line testing-library/no-node-access screen.getByRole('heading', { level: 3, name }).closest('details'); -const TestServerAccessSpecsSection = ({ +const StatefulSection = ({ + defaultValue, + component: Component, onChange, }: { - onChange(spec: ServerAccessSpec): void; + defaultValue: S; + component: React.ComponentType>; + onChange(spec: S): void; }) => { - const [model, setModel] = useState({ - kind: 'node', - labels: [], - logins: [], - }); + const [model, setModel] = useState(defaultValue); return ( - { @@ -153,7 +157,17 @@ const TestServerAccessSpecsSection = ({ test('editing server access specs', async () => { const user = userEvent.setup(); const onChange = jest.fn(); - render(); + render( + + component={ServerAccessSpecSection} + defaultValue={{ + kind: 'node', + labels: [], + logins: [], + }} + onChange={onChange} + /> + ); await user.click(screen.getByRole('button', { name: 'Add a Label' })); await user.type(screen.getByPlaceholderText('label key'), 'some-key'); await user.type(screen.getByPlaceholderText('label value'), 'some-value'); @@ -173,3 +187,130 @@ test('editing server access specs', async () => { ], } as ServerAccessSpec); }); + +describe('KubernetesAccessSpecSection', () => { + const setup = () => { + const onChange = jest.fn(); + render( + + component={KubernetesAccessSpecSection} + defaultValue={{ + kind: 'kube_cluster', + groups: [], + labels: [], + resources: [], + }} + onChange={onChange} + /> + ); + return { user: userEvent.setup(), onChange }; + }; + + test('editing the spec', async () => { + const { user, onChange } = setup(); + + await selectEvent.create(screen.getByLabelText('Groups'), 'group1', { + createOptionText: 'Group: group1', + }); + await selectEvent.create(screen.getByLabelText('Groups'), 'group2', { + createOptionText: 'Group: group2', + }); + + await user.click(screen.getByRole('button', { name: 'Add a Label' })); + await user.type(screen.getByPlaceholderText('label key'), 'some-key'); + await user.type(screen.getByPlaceholderText('label value'), 'some-value'); + + await user.click(screen.getByRole('button', { name: 'Add a Resource' })); + expect( + reactSelectValueContainer(screen.getByLabelText('Kind')) + ).toHaveTextContent('Any kind'); + expect(screen.getByLabelText('Name')).toHaveValue('*'); + expect(screen.getByLabelText('Namespace')).toHaveValue('*'); + await selectEvent.select(screen.getByLabelText('Kind'), 'Job'); + await user.clear(screen.getByLabelText('Name')); + await user.type(screen.getByLabelText('Name'), 'job-name'); + await user.clear(screen.getByLabelText('Namespace')); + await user.type(screen.getByLabelText('Namespace'), 'job-namespace'); + await selectEvent.select(screen.getByLabelText('Verbs'), [ + 'create', + 'delete', + ]); + + expect(onChange).toHaveBeenLastCalledWith({ + kind: 'kube_cluster', + groups: [ + expect.objectContaining({ value: 'group1' }), + expect.objectContaining({ value: 'group2' }), + ], + labels: [{ name: 'some-key', value: 'some-value' }], + resources: [ + { + kind: expect.objectContaining({ value: 'job' }), + name: 'job-name', + namespace: 'job-namespace', + verbs: [ + expect.objectContaining({ value: 'create' }), + expect.objectContaining({ value: 'delete' }), + ], + }, + ], + } as KubernetesAccessSpec); + }); + + test('adding and removing resources', async () => { + const { user, onChange } = setup(); + + await user.click(screen.getByRole('button', { name: 'Add a Resource' })); + await user.clear(screen.getByLabelText('Name')); + await user.type(screen.getByLabelText('Name'), 'res1'); + await user.click( + screen.getByRole('button', { name: 'Add Another Resource' }) + ); + await user.clear(screen.getAllByLabelText('Name')[1]); + await user.type(screen.getAllByLabelText('Name')[1], 'res2'); + await user.click( + screen.getByRole('button', { name: 'Add Another Resource' }) + ); + await user.clear(screen.getAllByLabelText('Name')[2]); + await user.type(screen.getAllByLabelText('Name')[2], 'res3'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + resources: [ + expect.objectContaining({ name: 'res1' }), + expect.objectContaining({ name: 'res2' }), + expect.objectContaining({ name: 'res3' }), + ], + }) + ); + + await user.click( + screen.getAllByRole('button', { name: 'Remove resource' })[1] + ); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + resources: [ + expect.objectContaining({ name: 'res1' }), + expect.objectContaining({ name: 'res3' }), + ], + }) + ); + await user.click( + screen.getAllByRole('button', { name: 'Remove resource' })[0] + ); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + resources: [expect.objectContaining({ name: 'res3' })], + }) + ); + await user.click( + screen.getAllByRole('button', { name: 'Remove resource' })[0] + ); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ resources: [] }) + ); + }); +}); + +const reactSelectValueContainer = (input: HTMLInputElement) => + // eslint-disable-next-line testing-library/no-node-access + input.closest('.react-select__value-container'); diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx index 553a8bfd474d0..d73c9044ae536 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx @@ -17,7 +17,16 @@ */ import React, { useState } from 'react'; -import { Box, ButtonIcon, Flex, H3, Text } from 'design'; +import { + Box, + ButtonIcon, + ButtonSecondary, + Flex, + H3, + H4, + Mark, + Text, +} from 'design'; import FieldInput from 'shared/components/FieldInput'; import Validation, { Validator } from 'shared/components/Validation'; import { requiredField } from 'shared/components/Validation/rules'; @@ -27,7 +36,10 @@ import styled, { useTheme } from 'styled-components'; import { MenuButton, MenuItem } from 'shared/components/MenuAction'; -import { FieldSelectCreatable } from 'shared/components/FieldSelect'; +import { + FieldSelect, + FieldSelectCreatable, +} from 'shared/components/FieldSelect'; import { Role, RoleWithYaml } from 'teleport/services/resources'; @@ -42,6 +54,12 @@ import { AccessSpecKind, AccessSpec, ServerAccessSpec, + newAccessSpec, + KubernetesAccessSpec, + newKubernetesResourceModel, + kubernetesResourceKindOptions, + kubernetesVerbOptions, + KubernetesResourceModel, } from './standardmodel'; import { EditorSaveCancelButton } from './Shared'; import { RequiresResetToStandard } from './RequiresResetToStandard'; @@ -112,7 +130,7 @@ export const StandardEditor = ({ ...standardEditorModel.roleModel, accessSpecs: [ ...standardEditorModel.roleModel.accessSpecs, - { kind, labels: [], logins: [] }, + newAccessSpec(kind), ], }); } @@ -210,7 +228,7 @@ export const StandardEditor = ({ ); }; -type SectionProps = { +export type SectionProps = { value: T; isProcessing: boolean; onChange?(value: T): void; @@ -289,7 +307,7 @@ const Section = ({ open={expanded} border={1} borderColor={theme.colors.interactive.tonal.neutral[0]} - borderRadius={2} + borderRadius={3} > ) { + return ( + <> + `Group: ${label}`} + components={{ + DropdownIndicator: null, + }} + value={value.groups} + onChange={groups => onChange?.({ ...value, groups })} + /> + + + Labels + + onChange?.({ ...value, labels })} + /> + + + {value.resources.map((resource, iRes) => ( + + onChange?.({ + ...value, + resources: value.resources.map((res, i) => + i === iRes ? newRes : res + ), + }) + } + onRemove={() => + onChange?.({ + ...value, + resources: value.resources.toSpliced(iRes, 1), + }) + } + /> + ))} + + + + onChange?.({ + ...value, + resources: [...value.resources, newKubernetesResourceModel()], + }) + } + > + + {value.resources.length > 0 + ? 'Add Another Resource' + : 'Add a Resource'} + + + + + ); +} + +function KubernetesResourceView({ + value, + isProcessing, + onChange, + onRemove, +}: { + value: KubernetesResourceModel; + isProcessing: boolean; + onChange(m: KubernetesResourceModel): void; + onRemove(): void; +}) { + const { kind, name, namespace, verbs } = value; + const theme = useTheme(); + return ( + + + +

Resource

+
+ + + +
+ onChange?.({ ...value, kind: k })} + /> + + Name of the resource. Special value *{' '} + means any name. + + } + disabled={isProcessing} + value={name} + onChange={e => onChange?.({ ...value, name: e.target.value })} + /> + + Namespace that contains the resource. Special value{' '} + * means any namespace. + + } + disabled={isProcessing} + value={namespace} + onChange={e => onChange?.({ ...value, namespace: e.target.value })} + /> + onChange?.({ ...value, verbs: v })} + mb={0} + /> +
+ ); } export const EditorWrapper = styled(Box)<{ mute?: boolean }>` opacity: ${p => (p.mute ? 0.4 : 1)}; pointer-events: ${p => (p.mute ? 'none' : '')}; `; + +// TODO(bl-nero): This should ideally use tonal neutral 1 from the opposite +// theme as background. +const MarkInverse = styled(Mark)` + background: ${p => p.theme.colors.text.primaryInverse}; + color: ${p => p.theme.colors.text.main}; +`; diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts index 311018beb8a9c..cf98eb9f22aa5 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts +++ b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts @@ -18,7 +18,13 @@ import { Role } from 'teleport/services/resources'; +import { Label as UILabel } from 'teleport/components/LabelsInput/LabelsInput'; + +import { Labels } from 'teleport/services/resources'; + import { + labelsModelToLabels, + labelsToModel, RoleEditorModel, roleEditorModelToRole, roleToRoleEditorModel, @@ -224,7 +230,7 @@ describe('roleToRoleEditorModel', () => { } as RoleEditorModel); }); - it('converts server access spec', () => { + it('creates a server access spec', () => { const minRole = minimalRole(); expect( roleToRoleEditorModel({ @@ -232,7 +238,7 @@ describe('roleToRoleEditorModel', () => { spec: { ...minRole.spec, allow: { - node_labels: { foo: 'bar', doubleFoo: ['bar1', 'bar2'] }, + node_labels: { foo: 'bar' }, logins: ['root', 'cthulhu', 'sandman'], }, }, @@ -242,11 +248,7 @@ describe('roleToRoleEditorModel', () => { accessSpecs: [ { kind: 'node', - labels: [ - { name: 'foo', value: 'bar' }, - { name: 'doubleFoo', value: 'bar1' }, - { name: 'doubleFoo', value: 'bar2' }, - ], + labels: [{ name: 'foo', value: 'bar' }], logins: [ { label: 'root', value: 'root' }, { label: 'cthulhu', value: 'cthulhu' }, @@ -256,6 +258,72 @@ describe('roleToRoleEditorModel', () => { ], } as RoleEditorModel); }); + + it('creates a Kubernetes access spec', () => { + const minRole = minimalRole(); + expect( + roleToRoleEditorModel({ + ...minRole, + spec: { + ...minRole.spec, + allow: { + kubernetes_groups: ['group1', 'group2'], + kubernetes_labels: { bar: 'foo' }, + kubernetes_resources: [ + { + kind: 'pod', + name: 'some-pod', + namespace: '*', + verbs: ['get', 'update'], + }, + { + // No namespace required for cluster-wide resources. + kind: 'kube_node', + name: 'some-node', + }, + ], + }, + }, + }) + ).toEqual({ + ...minimalRoleModel(), + accessSpecs: [ + { + kind: 'kube_cluster', + groups: [ + { label: 'group1', value: 'group1' }, + { label: 'group2', value: 'group2' }, + ], + labels: [{ name: 'bar', value: 'foo' }], + resources: [ + { + kind: { value: 'pod', label: 'Pod' }, + name: 'some-pod', + namespace: '*', + verbs: [ + { label: 'get', value: 'get' }, + { label: 'update', value: 'update' }, + ], + }, + { + kind: { value: 'kube_node', label: 'Node' }, + name: 'some-node', + namespace: '', + verbs: [], + }, + ], + }, + ], + } as RoleEditorModel); + }); +}); + +test('labelsToModel', () => { + expect(labelsToModel({ foo: 'bar', doubleFoo: ['bar1', 'bar2'] })).toEqual([ + { name: 'foo', value: 'bar' }, + { name: 'doubleFoo', value: 'bar1' }, + { name: 'doubleFoo', value: 'bar2' }, + ]); }); describe('roleEditorModelToRole', () => { @@ -287,12 +355,7 @@ describe('roleEditorModelToRole', () => { accessSpecs: [ { kind: 'node', - labels: [ - { name: 'foo', value: 'bar' }, - { name: 'tripleFoo', value: 'bar1' }, - { name: 'tripleFoo', value: 'bar2' }, - { name: 'tripleFoo', value: 'bar3' }, - ], + labels: [{ name: 'foo', value: 'bar' }], logins: [ { label: 'root', value: 'root' }, { label: 'cthulhu', value: 'cthulhu' }, @@ -306,10 +369,87 @@ describe('roleEditorModelToRole', () => { spec: { ...minRole.spec, allow: { - node_labels: { foo: 'bar', tripleFoo: ['bar1', 'bar2', 'bar3'] }, + node_labels: { foo: 'bar' }, logins: ['root', 'cthulhu', 'sandman'], }, }, } as Role); }); + + it('converts a Kubernetes access spec', () => { + const minRole = minimalRole(); + expect( + roleEditorModelToRole({ + ...minimalRoleModel(), + accessSpecs: [ + { + kind: 'kube_cluster', + groups: [ + { label: 'group1', value: 'group1' }, + { label: 'group2', value: 'group2' }, + ], + labels: [{ name: 'bar', value: 'foo' }], + resources: [ + { + kind: { value: 'pod', label: 'Pod' }, + name: 'some-pod', + namespace: '*', + verbs: [ + { label: 'get', value: 'get' }, + { label: 'update', value: 'update' }, + ], + }, + { + kind: { value: 'kube_node', label: 'Node' }, + name: 'some-node', + namespace: '', + verbs: [], + }, + ], + }, + ], + }) + ).toEqual({ + ...minRole, + spec: { + ...minRole.spec, + allow: { + kubernetes_groups: ['group1', 'group2'], + kubernetes_labels: { bar: 'foo' }, + kubernetes_resources: [ + { + kind: 'pod', + name: 'some-pod', + namespace: '*', + verbs: ['get', 'update'], + }, + { + kind: 'kube_node', + name: 'some-node', + namespace: '', + verbs: [], + }, + ], + }, + }, + } as Role); + }); +}); + +test('labelsModelToLabels', () => { + const model: UILabel[] = [ + { name: 'foo', value: 'bar' }, + { name: 'doubleFoo', value: 'bar1' }, + { name: 'doubleFoo', value: 'bar2' }, + // Moving from 2 to 3 values is a separate code branch, hence one more + // case. + { name: 'tripleFoo', value: 'bar1' }, + { name: 'tripleFoo', value: 'bar2' }, + { name: 'tripleFoo', value: 'bar3' }, + ]; + expect(labelsModelToLabels(model)).toEqual({ + foo: 'bar', + doubleFoo: ['bar1', 'bar2'], + tripleFoo: ['bar1', 'bar2', 'bar3'], + } as Labels); }); diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts index 1d9066a1b93ee..07561b42d9e1b 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts +++ b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts @@ -20,9 +20,14 @@ import { equalsDeep } from 'shared/utils/highbar'; import { Option } from 'shared/components/Select'; -import { Labels, Role, RoleConditions } from 'teleport/services/resources'; +import { + KubernetesResource, + Labels, + Role, + RoleConditions, +} from 'teleport/services/resources'; -import { Label } from 'teleport/components/LabelsInput/LabelsInput'; +import { Label as UILabel } from 'teleport/components/LabelsInput/LabelsInput'; import { defaultOptions } from './withDefaults'; @@ -77,11 +82,130 @@ type AccessSpecBase = { export type AccessSpecKind = 'node' | 'kube_cluster'; /** Model for the Kubernetes access specification section. */ -export type KubernetesAccessSpec = AccessSpecBase<'kube_cluster'>; +export type KubernetesAccessSpec = AccessSpecBase<'kube_cluster'> & { + groups: readonly Option[]; + labels: UILabel[]; + resources: KubernetesResourceModel[]; +}; + +export type KubernetesResourceModel = { + kind: KubernetesResourceKindOption; + name: string; + namespace: string; + verbs: readonly KubernetesVerbOption[]; +}; + +type KubernetesResourceKindOption = Option; +type KubernetesResourceKind = + | '*' + | 'pod' + | 'secret' + | 'configmap' + | 'namespace' + | 'service' + | 'serviceaccount' + | 'kube_node' + | 'persistentvolume' + | 'persistentvolumeclaim' + | 'deployment' + | 'replicaset' + | 'statefulset' + | 'daemonset' + | 'clusterrole' + | 'kube_role' + | 'clusterrolebinding' + | 'rolebinding' + | 'cronjob' + | 'job' + | 'certificatesigningrequest' + | 'ingress'; + +/** + * All possible resource kind drop-down options. This array needs to be kept in + * sync with `KubernetesResourcesKinds` in `api/types/constants.go. + */ +export const kubernetesResourceKindOptions: KubernetesResourceKindOption[] = [ + // The "any kind" option goes first. + { value: '*', label: 'Any kind' }, + + // The rest is sorted by label. + ...( + [ + { value: 'pod', label: 'Pod' }, + { value: 'secret', label: 'Secret' }, + { value: 'configmap', label: 'ConfigMap' }, + { value: 'namespace', label: 'Namespace' }, + { value: 'service', label: 'Service' }, + { value: 'serviceaccount', label: 'ServiceAccount' }, + { value: 'kube_node', label: 'Node' }, + { value: 'persistentvolume', label: 'PersistentVolume' }, + { value: 'persistentvolumeclaim', label: 'PersistentVolumeClaim' }, + { value: 'deployment', label: 'Deployment' }, + { value: 'replicaset', label: 'ReplicaSet' }, + { value: 'statefulset', label: 'Statefulset' }, + { value: 'daemonset', label: 'DaemonSet' }, + { value: 'clusterrole', label: 'ClusterRole' }, + { value: 'kube_role', label: 'Role' }, + { value: 'clusterrolebinding', label: 'ClusterRoleBinding' }, + { value: 'rolebinding', label: 'RoleBinding' }, + { value: 'cronjob', label: 'Cronjob' }, + { value: 'job', label: 'Job' }, + { + value: 'certificatesigningrequest', + label: 'CertificateSigningRequest', + }, + { value: 'ingress', label: 'Ingress' }, + ] as const + ).toSorted((a, b) => a.label.localeCompare(b.label)), +]; + +type KubernetesVerbOption = Option; +type KubernetesVerb = + | '*' + | 'get' + | 'create' + | 'update' + | 'patch' + | 'delete' + | 'list' + | 'watch' + | 'deletecollection' + | 'exec' + | 'portforward'; + +/** + * All possible Kubernetes verb drop-down options. This array needs to be kept + * in sync with `KubernetesVerbs` in `api/types/constants.go. + */ +export const kubernetesVerbOptions: KubernetesVerbOption[] = [ + // The "any kind" option goes first. + { value: '*', label: 'All verbs' }, + + // The rest is sorted. + ...( + [ + 'get', + 'create', + 'update', + 'patch', + 'delete', + 'list', + 'watch', + 'deletecollection', + + // TODO(bl-nero): These are actually not k8s verbs, but they are allowed + // in our config. We may want to explain them in the UI somehow. + 'exec', + 'portforward', + ] as const + ) + .toSorted((a, b) => a.localeCompare(b)) + .map(verb => ({ value: verb, label: verb })), +]; /** Model for the server access specification section. */ export type ServerAccessSpec = AccessSpecBase<'node'> & { - labels: Label[]; + labels: UILabel[]; logins: readonly Option[]; }; @@ -105,6 +229,26 @@ export function newRole(): Role { }; } +export function newAccessSpec(kind: AccessSpecKind): AccessSpec { + switch (kind) { + case 'node': + return { kind: 'node', labels: [], logins: [] }; + case 'kube_cluster': + return { kind: 'kube_cluster', groups: [], labels: [], resources: [] }; + default: + kind satisfies never; + } +} + +export function newKubernetesResourceModel(): KubernetesResourceModel { + return { + kind: kubernetesResourceKindOptions.find(k => k.value === '*'), + name: '*', + namespace: '*', + verbs: [], + }; +} + /** * Converts a role to its in-editor UI model representation. The resulting * model may be marked as requiring reset if the role contains unsupported @@ -145,12 +289,24 @@ export function roleToRoleEditorModel( }; } +/** + * Converts a `RoleConditions` instance (an "allow" or "deny" section, to be + * specific) to a list of access specification models. + */ function roleConditionsToAccessSpecs(conditions: RoleConditions): { accessSpecs: AccessSpec[]; requiresReset: boolean; } { - const { node_labels, logins, ...rest } = conditions; + const { + node_labels, + logins, + kubernetes_groups, + kubernetes_labels, + kubernetes_resources, + ...rest + } = conditions; const accessSpecs: AccessSpec[] = []; + const nodeLabelsModel = labelsToModel(node_labels); const nodeLoginsModel = (logins ?? []).map(login => ({ label: login, @@ -163,13 +319,36 @@ function roleConditionsToAccessSpecs(conditions: RoleConditions): { logins: nodeLoginsModel, }); } + + const kubeGroupsModel = (kubernetes_groups ?? []).map(group => ({ + label: group, + value: group, + })); + const kubeLabelsModel = labelsToModel(kubernetes_labels); + const kubeResourcesModel = kubernetesResourcesToModel(kubernetes_resources); + if ( + kubeGroupsModel.length > 0 || + kubeLabelsModel.length > 0 || + kubeResourcesModel.length > 0 + ) { + accessSpecs.push({ + kind: 'kube_cluster', + groups: kubeGroupsModel, + labels: kubeLabelsModel, + resources: kubeResourcesModel, + }); + } return { accessSpecs, requiresReset: !isEmpty(rest), }; } -function labelsToModel(labels: Labels | undefined) { +/** + * Converts a set of labels, as represented in the role resource, to a list of + * `LabelInput` value models. + */ +export function labelsToModel(labels: Labels | undefined): UILabel[] { if (!labels) return []; return Object.entries(labels).flatMap(([name, value]) => { if (typeof value === 'string') { @@ -183,6 +362,21 @@ function labelsToModel(labels: Labels | undefined) { }); } +function kubernetesResourcesToModel( + resources: KubernetesResource[] | undefined +): KubernetesResourceModel[] { + return (resources ?? []).map( + ({ kind, name, namespace = '', verbs = [] }) => ({ + kind: kubernetesResourceKindOptions.find(o => o.value === kind), + name, + namespace, + verbs: verbs.map(verb => + kubernetesVerbOptions.find(o => o.value === verb) + ), + }) + ); +} + function isEmpty(obj: object) { return Object.keys(obj).length === 0; } @@ -211,25 +405,52 @@ export function roleEditorModelToRole(roleModel: RoleEditorModel): Role { }; for (const spec of roleModel.accessSpecs) { - if (spec.kind === 'node') { - const labels = {}; - for (const { name, value } of spec.labels) { - if (!Object.hasOwn(labels, name)) { - labels[name] = value; - } else if (typeof labels[name] === 'string') { - labels[name] = [labels[name], value]; - } else { - labels[name].push(value); - } + const { kind } = spec; + switch (kind) { + case 'node': { + role.spec.allow.node_labels = labelsModelToLabels(spec.labels); + role.spec.allow.logins = spec.logins.map(opt => opt.value); + break; + } + case 'kube_cluster': { + role.spec.allow.kubernetes_groups = spec.groups.map(opt => opt.value); + role.spec.allow.kubernetes_labels = labelsModelToLabels(spec.labels); + role.spec.allow.kubernetes_resources = spec.resources.map( + ({ kind, name, namespace, verbs }) => ({ + kind: kind.value, + name, + namespace, + verbs: verbs.map(opt => opt.value), + }) + ); + break; } - role.spec.allow.node_labels = labels; - role.spec.allow.logins = spec.logins.map(opt => opt.value); + default: + kind satisfies never; } } return role; } +/** + * Converts a list of `LabelInput` value models to a set of labels, as + * represented in the role resource. + */ +export function labelsModelToLabels(uiLabels: UILabel[]): Labels { + const labels = {}; + for (const { name, value } of uiLabels) { + if (!Object.hasOwn(labels, name)) { + labels[name] = value; + } else if (typeof labels[name] === 'string') { + labels[name] = [labels[name], value]; + } else { + labels[name].push(value); + } + } + return labels; +} + /** Detects if fields were modified by comparing against the original role. */ export function hasModifiedFields( updated: RoleEditorModel, diff --git a/web/packages/teleport/src/services/resources/types.ts b/web/packages/teleport/src/services/resources/types.ts index 50a49ecdf757e..3c366d24aac01 100644 --- a/web/packages/teleport/src/services/resources/types.ts +++ b/web/packages/teleport/src/services/resources/types.ts @@ -62,10 +62,20 @@ export type Role = { export type RoleConditions = { node_labels?: Labels; logins?: string[]; + kubernetes_groups?: string[]; + kubernetes_labels?: Labels; + kubernetes_resources?: KubernetesResource[]; }; export type Labels = Record; +export type KubernetesResource = { + kind?: string; + name?: string; + namespace?: string; + verbs?: string[]; +}; + /** * Teleport role options in full format, as returned from Teleport API. Note * that its fields follow the snake case convention to match the wire format. From eabbf327e2278d22a1b8d62341a06ec613c34406 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Fri, 18 Oct 2024 19:17:42 +0200 Subject: [PATCH 2/6] Add a multi-value input component --- .../FieldMultiInput/FieldMultiInput.story.tsx | 17 +++ .../FieldMultiInput/FieldMultiInput.test.tsx | 51 ++++++++ .../FieldMultiInput/FieldMultiInput.tsx | 121 ++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx create mode 100644 web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx create mode 100644 web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx new file mode 100644 index 0000000000000..9c8b7dcc255de --- /dev/null +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx @@ -0,0 +1,17 @@ +import React, { useState } from 'react'; +import { FieldMultiInput } from './FieldMultiInput'; +import Box from 'design/Box'; + +export default { + title: 'Shared', +}; + +export function Story() { + const [items, setItems] = useState([]); + return ( + + + + ); +} +Story.storyName = 'FieldMultiInput'; diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx new file mode 100644 index 0000000000000..449e3da4424ce --- /dev/null +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx @@ -0,0 +1,51 @@ +import userEvent from '@testing-library/user-event'; +import React, { useState } from 'react'; +import { FieldMultiInput, FieldMultiInputProps } from './FieldMultiInput'; +import { render, screen } from 'design/utils/testing'; + +const TestFieldMultiInput = ({ + onChange, + ...rest +}: Partial) => { + const [items, setItems] = useState([]); + const handleChange = (it: string[]) => { + setItems(it); + onChange?.(it); + }; + return ; +}; + +test('adding, editing, and removing items', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render(); + + await user.type(screen.getByRole('textbox'), 'apples'); + expect(onChange).toHaveBeenLastCalledWith(['apples']); + + await user.click(screen.getByRole('button', { name: 'Add More' })); + expect(onChange).toHaveBeenLastCalledWith(['apples', '']); + + await user.type(screen.getAllByRole('textbox')[1], 'oranges'); + expect(onChange).toHaveBeenLastCalledWith(['apples', 'oranges']); + + await user.click(screen.getAllByRole('button', { name: 'Remove Item' })[0]); + expect(onChange).toHaveBeenLastCalledWith(['oranges']); + + await user.click(screen.getAllByRole('button', { name: 'Remove Item' })[0]); + expect(onChange).toHaveBeenLastCalledWith([]); +}); + +test('keyboard handling', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render(); + + await user.click(screen.getByRole('textbox')); + await user.keyboard('apples{Enter}oranges'); + expect(onChange).toHaveBeenLastCalledWith(['apples', 'oranges']); + + await user.click(screen.getAllByRole('textbox')[0]); + await user.keyboard('{Enter}bananas'); + expect(onChange).toHaveBeenLastCalledWith(['apples', 'bananas', 'oranges']); +}); diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx new file mode 100644 index 0000000000000..5309ea72f0ab2 --- /dev/null +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx @@ -0,0 +1,121 @@ +import Box from 'design/Box'; +import { ButtonSecondary } from 'design/Button'; +import ButtonIcon from 'design/ButtonIcon'; +import Flex from 'design/Flex'; +import * as Icon from 'design/Icon'; +import Input from 'design/Input'; +import { useEffect, useRef, useState } from 'react'; +import styled, { useTheme } from 'styled-components'; + +export type FieldMultiInputProps = { + label?: string; + value: string[]; + disabled?: boolean; + onChange?(val: string[]): void; +}; + +/** + * Allows editing a list of strings, one value per row. Use instead of + * `FieldSelectCreatable` when: + * + * - There are no predefined values to be picked from. + * - Values are expected to be relatively long and would be unreadable after + * being truncated. + */ +export function FieldMultiInput({ + label, + value, + disabled, + onChange, +}: FieldMultiInputProps) { + if (value.length === 0) { + value = ['']; + } + + const theme = useTheme(); + // Index of the input to be focused after the next rendering. + const toFocus = useRef(); + + const setFocus = element => { + element?.focus(); + toFocus.current = undefined; + }; + + function insertItem(index: number) { + onChange?.(value.toSpliced(index, 0, '')); + } + + function removeItem(index: number) { + onChange?.(value.toSpliced(index, 1)); + } + + function handleKeyDown(index: number, e: React.KeyboardEvent) { + if (e.key === 'Enter') { + insertItem(index + 1); + toFocus.current = index + 1; + } + } + + return ( + +
+ {label && {label}} + {value.map((val, i) => ( + // Note on keys: using index as a key is an anti-pattern in general, + // but here, we can safely assume that even though the list is + // editable, we don't rely on any unmanaged HTML element state other + // than focus, which we deal with separately anyway. The alternatives + // would be either to require an array with keys generated + // synthetically and injected from outside (which would make the API + // difficult to use) or to keep the array with generated IDs as local + // state (which would require us to write a prop/state reconciliation + // procedure whose complexity would probably outweigh the benefits). + + + + onChange?.( + value.map((v, j) => (j === i ? e.target.value : v)) + ) + } + onKeyDown={e => handleKeyDown(i, e)} + /> + + removeItem(i)} + disabled={disabled} + > + + + + ))} + insertItem(value.length)} + > + + Add More + +
+
+ ); +} + +const Fieldset = styled.fieldset` + border: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: ${props => props.theme.space[2]}px; +`; + +const Legend = styled.legend` + margin: 0 0 ${props => props.theme.space[1]}px 0; + padding: 0; + ${props => props.theme.typography.body3} +`; From 351faa7e1de324f059576101aebf671af48c1833 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Wed, 23 Oct 2024 19:26:45 +0200 Subject: [PATCH 3/6] Review --- api/types/constants.go | 14 ++++++++++---- .../src/Roles/RoleEditor/StandardEditor.test.tsx | 1 + .../src/Roles/RoleEditor/StandardEditor.tsx | 8 ++++---- .../src/Roles/RoleEditor/standardmodel.test.ts | 4 ++++ .../teleport/src/Roles/RoleEditor/standardmodel.ts | 4 ++++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/api/types/constants.go b/api/types/constants.go index 1066f131d8823..f972d8cf9b4d9 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1247,11 +1247,14 @@ var RequestableResourceKinds = []string{ KindSAMLIdPServiceProvider, } +// The list below needs to be kept in sync with `kubernetesResourceKindOptions` +// in `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. (Keeping +// this comment separate to prevent it from being included in the official +// package docs.) + // KubernetesResourcesKinds lists the supported Kubernetes resource kinds. This // is for the latest version of Role resources; roles whose version is set to // v6 or prior only support [KindKubePod]. -// This list needs to be kept in sync with `kubernetesResourceKindOptions` in -// `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. var KubernetesResourcesKinds = []string{ KindKubePod, KindKubeSecret, @@ -1299,9 +1302,12 @@ const ( KubeVerbPortForward = "portforward" ) +// The list below needs to be kept in sync with `kubernetesResourceVerbOptions` +// in `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. (Keeping +// this comment separate to prevent it from being included in the official +// package docs.) + // KubernetesVerbs lists the supported Kubernetes verbs. -// This list needs to be kept in sync with `kubernetesResourceVerbOptions` in -// `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`. var KubernetesVerbs = []string{ Wildcard, KubeVerbGet, diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx index 8ee9abd04ddbb..b0009118a55a6 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx @@ -245,6 +245,7 @@ describe('KubernetesAccessSpecSection', () => { labels: [{ name: 'some-key', value: 'some-value' }], resources: [ { + id: expect.any(String), kind: expect.objectContaining({ value: 'job' }), name: 'job-name', namespace: 'job-namespace', diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx index d73c9044ae536..7a19f03ca43b3 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx @@ -466,23 +466,23 @@ export function KubernetesAccessSpecSection({ /> - {value.resources.map((resource, iRes) => ( + {value.resources.map((resource, index) => ( onChange?.({ ...value, resources: value.resources.map((res, i) => - i === iRes ? newRes : res + i === index ? newRes : res ), }) } onRemove={() => onChange?.({ ...value, - resources: value.resources.toSpliced(iRes, 1), + resources: value.resources.toSpliced(index, 1), }) } /> diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts index cf98eb9f22aa5..f95d9fe7c183d 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts +++ b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.test.ts @@ -297,6 +297,7 @@ describe('roleToRoleEditorModel', () => { labels: [{ name: 'bar', value: 'foo' }], resources: [ { + id: expect.any(String), kind: { value: 'pod', label: 'Pod' }, name: 'some-pod', namespace: '*', @@ -306,6 +307,7 @@ describe('roleToRoleEditorModel', () => { ], }, { + id: expect.any(String), kind: { value: 'kube_node', label: 'Node' }, name: 'some-node', namespace: '', @@ -391,6 +393,7 @@ describe('roleEditorModelToRole', () => { labels: [{ name: 'bar', value: 'foo' }], resources: [ { + id: 'dummy-id-1', kind: { value: 'pod', label: 'Pod' }, name: 'some-pod', namespace: '*', @@ -400,6 +403,7 @@ describe('roleEditorModelToRole', () => { ], }, { + id: 'dummy-id-2', kind: { value: 'kube_node', label: 'Node' }, name: 'some-node', namespace: '', diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts index 07561b42d9e1b..9c13913315d8f 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts +++ b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts @@ -89,6 +89,8 @@ export type KubernetesAccessSpec = AccessSpecBase<'kube_cluster'> & { }; export type KubernetesResourceModel = { + /** Autogenerated ID to be used with the `key` property. */ + id: string; kind: KubernetesResourceKindOption; name: string; namespace: string; @@ -242,6 +244,7 @@ export function newAccessSpec(kind: AccessSpecKind): AccessSpec { export function newKubernetesResourceModel(): KubernetesResourceModel { return { + id: crypto.randomUUID(), kind: kubernetesResourceKindOptions.find(k => k.value === '*'), name: '*', namespace: '*', @@ -367,6 +370,7 @@ function kubernetesResourcesToModel( ): KubernetesResourceModel[] { return (resources ?? []).map( ({ kind, name, namespace = '', verbs = [] }) => ({ + id: crypto.randomUUID(), kind: kubernetesResourceKindOptions.find(o => o.value === kind), name, namespace, From 36f989c358da38f0ddb465aee16ec350475cc75f Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Thu, 24 Oct 2024 15:46:14 +0200 Subject: [PATCH 4/6] Update the k8s operator docs This also removes the mention of valid values of the Kind field, as I don't want the external documentation to point to Teleport source files. --- api/proto/teleport/legacy/types/types.proto | 4 +--- api/types/types.pb.go | 4 +--- .../operator-crds/resources.teleport.dev_roles.yaml | 4 ---- .../operator-crds/resources.teleport.dev_rolesv6.yaml | 2 -- .../operator-crds/resources.teleport.dev_rolesv7.yaml | 2 -- .../config/crd/bases/resources.teleport.dev_roles.yaml | 4 ---- .../config/crd/bases/resources.teleport.dev_rolesv6.yaml | 2 -- .../config/crd/bases/resources.teleport.dev_rolesv7.yaml | 2 -- 8 files changed, 2 insertions(+), 22 deletions(-) diff --git a/api/proto/teleport/legacy/types/types.proto b/api/proto/teleport/legacy/types/types.proto index cdc8668ae7f71..a51edd0cfa11c 100644 --- a/api/proto/teleport/legacy/types/types.proto +++ b/api/proto/teleport/legacy/types/types.proto @@ -3319,9 +3319,7 @@ message DatabasePermission { // KubernetesResource is the Kubernetes resource identifier. message KubernetesResource { - // Kind specifies the Kubernetes Resource type. See - // `KubernetesResourcesKinds` in `api/types/constants.go` for the list of - // supported values. + // Kind specifies the Kubernetes Resource type. string Kind = 1 [(gogoproto.jsontag) = "kind,omitempty"]; // Namespace is the resource namespace. // It supports wildcards. diff --git a/api/types/types.pb.go b/api/types/types.pb.go index 6e11c61f11f9f..ce1a982a8b029 100644 --- a/api/types/types.pb.go +++ b/api/types/types.pb.go @@ -8383,9 +8383,7 @@ var xxx_messageInfo_DatabasePermission proto.InternalMessageInfo // KubernetesResource is the Kubernetes resource identifier. type KubernetesResource struct { - // Kind specifies the Kubernetes Resource type. See - // `KubernetesResourcesKinds` in `api/types/constants.go` for the list of - // supported values. + // Kind specifies the Kubernetes Resource type. Kind string `protobuf:"bytes,1,opt,name=Kind,proto3" json:"kind,omitempty"` // Namespace is the resource namespace. // It supports wildcards. diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_roles.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_roles.yaml index ef5b2d5ce6676..a51a1d9bc1f58 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_roles.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_roles.yaml @@ -260,7 +260,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -797,7 +796,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -1613,7 +1611,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -2150,7 +2147,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv6.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv6.yaml index 69cdeef31b1fa..bb7c899188b2b 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv6.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv6.yaml @@ -263,7 +263,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -800,7 +799,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. diff --git a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv7.yaml b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv7.yaml index 0d7aceff039f0..3c28e797f76dd 100644 --- a/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv7.yaml +++ b/examples/chart/teleport-cluster/charts/teleport-operator/operator-crds/resources.teleport.dev_rolesv7.yaml @@ -263,7 +263,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -800,7 +799,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. diff --git a/integrations/operator/config/crd/bases/resources.teleport.dev_roles.yaml b/integrations/operator/config/crd/bases/resources.teleport.dev_roles.yaml index ef5b2d5ce6676..a51a1d9bc1f58 100644 --- a/integrations/operator/config/crd/bases/resources.teleport.dev_roles.yaml +++ b/integrations/operator/config/crd/bases/resources.teleport.dev_roles.yaml @@ -260,7 +260,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -797,7 +796,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -1613,7 +1611,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -2150,7 +2147,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. diff --git a/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv6.yaml b/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv6.yaml index 69cdeef31b1fa..bb7c899188b2b 100644 --- a/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv6.yaml +++ b/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv6.yaml @@ -263,7 +263,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -800,7 +799,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. diff --git a/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv7.yaml b/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv7.yaml index 0d7aceff039f0..3c28e797f76dd 100644 --- a/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv7.yaml +++ b/integrations/operator/config/crd/bases/resources.teleport.dev_rolesv7.yaml @@ -263,7 +263,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. @@ -800,7 +799,6 @@ spec: properties: kind: description: Kind specifies the Kubernetes Resource type. - At the moment only "pod" is supported. type: string name: description: Name is the resource name. It supports wildcards. From ba129e7982cf4c467489113165d1d12f2f8af5dc Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Thu, 24 Oct 2024 16:18:01 +0200 Subject: [PATCH 5/6] Update operator CRDs and Terraform resources --- .../operator-resources/resources.teleport.dev_roles.mdx | 8 ++++---- .../operator-resources/resources.teleport.dev_rolesv6.mdx | 4 ++-- .../operator-resources/resources.teleport.dev_rolesv7.mdx | 4 ++-- .../reference/terraform-provider/data-sources/role.mdx | 4 ++-- .../pages/reference/terraform-provider/resources/role.mdx | 4 ++-- integrations/terraform/tfschema/types_terraform.go | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/pages/reference/operator-resources/resources.teleport.dev_roles.mdx b/docs/pages/reference/operator-resources/resources.teleport.dev_roles.mdx index 1a1bd131d98ee..bcfa55b92e380 100644 --- a/docs/pages/reference/operator-resources/resources.teleport.dev_roles.mdx +++ b/docs/pages/reference/operator-resources/resources.teleport.dev_roles.mdx @@ -109,7 +109,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| @@ -267,7 +267,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| @@ -508,7 +508,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| @@ -666,7 +666,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| diff --git a/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv6.mdx b/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv6.mdx index 5a52c58697753..faf5066ed3d65 100644 --- a/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv6.mdx +++ b/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv6.mdx @@ -109,7 +109,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| @@ -267,7 +267,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| diff --git a/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv7.mdx b/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv7.mdx index 328bfa0c52c24..2c4e6e5415ea1 100644 --- a/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv7.mdx +++ b/docs/pages/reference/operator-resources/resources.teleport.dev_rolesv7.mdx @@ -109,7 +109,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| @@ -267,7 +267,7 @@ resource, which you can apply after installing the Teleport Kubernetes operator. |Field|Type|Description| |---|---|---| -|kind|string|Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported.| +|kind|string|Kind specifies the Kubernetes Resource type.| |name|string|Name is the resource name. It supports wildcards.| |namespace|string|Namespace is the resource namespace. It supports wildcards.| |verbs|[]string|Verbs are the allowed Kubernetes verbs for the following resource.| diff --git a/docs/pages/reference/terraform-provider/data-sources/role.mdx b/docs/pages/reference/terraform-provider/data-sources/role.mdx index b5ace20ebbbc2..ff453e50bcc19 100644 --- a/docs/pages/reference/terraform-provider/data-sources/role.mdx +++ b/docs/pages/reference/terraform-provider/data-sources/role.mdx @@ -127,7 +127,7 @@ Optional: Optional: -- `kind` (String) Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported. +- `kind` (String) Kind specifies the Kubernetes Resource type. - `name` (String) Name is the resource name. It supports wildcards. - `namespace` (String) Namespace is the resource namespace. It supports wildcards. - `verbs` (List of String) Verbs are the allowed Kubernetes verbs for the following resource. @@ -299,7 +299,7 @@ Optional: Optional: -- `kind` (String) Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported. +- `kind` (String) Kind specifies the Kubernetes Resource type. - `name` (String) Name is the resource name. It supports wildcards. - `namespace` (String) Namespace is the resource namespace. It supports wildcards. - `verbs` (List of String) Verbs are the allowed Kubernetes verbs for the following resource. diff --git a/docs/pages/reference/terraform-provider/resources/role.mdx b/docs/pages/reference/terraform-provider/resources/role.mdx index 1a48109632ced..d2d344b089650 100644 --- a/docs/pages/reference/terraform-provider/resources/role.mdx +++ b/docs/pages/reference/terraform-provider/resources/role.mdx @@ -181,7 +181,7 @@ Optional: Optional: -- `kind` (String) Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported. +- `kind` (String) Kind specifies the Kubernetes Resource type. - `name` (String) Name is the resource name. It supports wildcards. - `namespace` (String) Namespace is the resource namespace. It supports wildcards. - `verbs` (List of String) Verbs are the allowed Kubernetes verbs for the following resource. @@ -353,7 +353,7 @@ Optional: Optional: -- `kind` (String) Kind specifies the Kubernetes Resource type. At the moment only "pod" is supported. +- `kind` (String) Kind specifies the Kubernetes Resource type. - `name` (String) Name is the resource name. It supports wildcards. - `namespace` (String) Namespace is the resource namespace. It supports wildcards. - `verbs` (List of String) Verbs are the allowed Kubernetes verbs for the following resource. diff --git a/integrations/terraform/tfschema/types_terraform.go b/integrations/terraform/tfschema/types_terraform.go index 7e841973780dc..13628d9ab6ee9 100644 --- a/integrations/terraform/tfschema/types_terraform.go +++ b/integrations/terraform/tfschema/types_terraform.go @@ -1722,7 +1722,7 @@ func GenSchemaRoleV6(ctx context.Context) (github_com_hashicorp_terraform_plugin "kubernetes_resources": { Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ "kind": { - Description: "Kind specifies the Kubernetes Resource type. At the moment only \"pod\" is supported.", + Description: "Kind specifies the Kubernetes Resource type.", Optional: true, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, }, @@ -2175,7 +2175,7 @@ func GenSchemaRoleV6(ctx context.Context) (github_com_hashicorp_terraform_plugin "kubernetes_resources": { Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.ListNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ "kind": { - Description: "Kind specifies the Kubernetes Resource type. At the moment only \"pod\" is supported.", + Description: "Kind specifies the Kubernetes Resource type.", Optional: true, Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, }, From aa88cbc427f655ca6be88970bdcff2fce63701e1 Mon Sep 17 00:00:00 2001 From: Bartosz Leper Date: Thu, 24 Oct 2024 17:22:15 +0200 Subject: [PATCH 6/6] Lint, licenses --- .../FieldMultiInput/FieldMultiInput.story.tsx | 22 ++++++++++++++++++- .../FieldMultiInput/FieldMultiInput.test.tsx | 22 ++++++++++++++++++- .../FieldMultiInput/FieldMultiInput.tsx | 20 ++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx index 9c8b7dcc255de..5362236a8b24d 100644 --- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx @@ -1,7 +1,27 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import React, { useState } from 'react'; -import { FieldMultiInput } from './FieldMultiInput'; + import Box from 'design/Box'; +import { FieldMultiInput } from './FieldMultiInput'; + export default { title: 'Shared', }; diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx index 449e3da4424ce..ce023a071053a 100644 --- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx @@ -1,8 +1,28 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import userEvent from '@testing-library/user-event'; import React, { useState } from 'react'; -import { FieldMultiInput, FieldMultiInputProps } from './FieldMultiInput'; + import { render, screen } from 'design/utils/testing'; +import { FieldMultiInput, FieldMultiInputProps } from './FieldMultiInput'; + const TestFieldMultiInput = ({ onChange, ...rest diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx index 5309ea72f0ab2..eaa98ef0a6511 100644 --- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx +++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx @@ -1,10 +1,28 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import Box from 'design/Box'; import { ButtonSecondary } from 'design/Button'; import ButtonIcon from 'design/ButtonIcon'; import Flex from 'design/Flex'; import * as Icon from 'design/Icon'; import Input from 'design/Input'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; import styled, { useTheme } from 'styled-components'; export type FieldMultiInputProps = {