1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

feat(kuberenetes): add annotations to kube objects EE-4089 (#8499)

* add annotations BE teaser
* fix settings icon click on home screen for kube env
* add debouce to namespace validation
* ingress button tooltip fixed
* fix tooltip text
This commit is contained in:
Prabhat Khera 2023-03-01 13:11:12 +13:00 committed by GitHub
parent 5f66020e42
commit defce0cf6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 579 additions and 483 deletions

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useMemo, ReactNode } from 'react';
import { useState, useEffect, useMemo, ReactNode, useCallback } from 'react';
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
import { v4 as uuidv4 } from 'uuid';
import { debounce } from 'lodash';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useConfigurations } from '@/react/kubernetes/configs/queries';
@ -286,9 +287,155 @@ export function CreateIngressView() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ingressRule, servicePorts]);
const validate = useCallback(
(
ingressRule: Rule,
ingressNames: string[],
serviceOptions: Option<string>[],
existingIngressClass?: IngressController
) => {
const errors: Record<string, ReactNode> = {};
const rule = { ...ingressRule };
// User cannot edit the namespace and the ingress name
if (!isEdit) {
if (!rule.Namespace) {
errors.namespace = 'Namespace is required';
}
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
if (!rule.IngressName) {
errors.ingressName = 'Ingress name is required';
} else if (!nameRegex.test(rule.IngressName)) {
errors.ingressName =
"This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').";
} else if (ingressNames.includes(rule.IngressName)) {
errors.ingressName = 'Ingress name already exists';
}
if (!rule.IngressClassName) {
errors.className = 'Ingress class is required';
}
}
if (isEdit && !ingressRule.IngressClassName) {
errors.className =
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.';
}
if (
isEdit &&
(!existingIngressClass ||
(existingIngressClass && !existingIngressClass.Availability)) &&
ingressRule.IngressClassName
) {
if (!rule.IngressType) {
errors.className =
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.';
} else {
errors.className =
'Currently set to an ingress class that you do not have access to - you must select a valid class.';
}
}
const duplicatedAnnotations: string[] = [];
rule.Annotations?.forEach((a, i) => {
if (!a.Key) {
errors[`annotations.key[${i}]`] = 'Annotation key is required';
} else if (duplicatedAnnotations.includes(a.Key)) {
errors[`annotations.key[${i}]`] = 'Annotation cannot be duplicated';
}
if (!a.Value) {
errors[`annotations.value[${i}]`] = 'Annotation value is required';
}
duplicatedAnnotations.push(a.Key);
});
const duplicatedHosts: string[] = [];
// Check if the paths are duplicates
rule.Hosts?.forEach((host, hi) => {
if (!host.NoHost) {
if (!host.Host) {
errors[`hosts[${hi}].host`] = 'Host is required';
} else if (duplicatedHosts.includes(host.Host)) {
errors[`hosts[${hi}].host`] = 'Host cannot be duplicated';
}
duplicatedHosts.push(host.Host);
}
// Validate service
host.Paths?.forEach((path, pi) => {
if (!path.ServiceName) {
errors[`hosts[${hi}].paths[${pi}].servicename`] =
'Service name is required';
}
if (
isEdit &&
path.ServiceName &&
!serviceOptions.find((s) => s.value === path.ServiceName)
) {
errors[`hosts[${hi}].paths[${pi}].servicename`] = (
<span>
Currently set to {path.ServiceName}, which does not exist. You
can create a service with this name for a particular deployment
via{' '}
<Link
to="kubernetes.applications"
params={{ id: environmentId }}
className="text-primary"
target="_blank"
>
Applications
</Link>
, and on returning here it will be picked up.
</span>
);
}
if (!path.ServicePort) {
errors[`hosts[${hi}].paths[${pi}].serviceport`] =
'Service port is required';
}
});
// Validate paths
const paths = host.Paths.map((path) => path.Route);
paths.forEach((item, idx) => {
if (!item) {
errors[`hosts[${hi}].paths[${idx}].path`] = 'Path cannot be empty';
} else if (paths.indexOf(item) !== idx) {
errors[`hosts[${hi}].paths[${idx}].path`] =
'Paths cannot be duplicated';
} else {
// Validate host and path combination globally
const isExists = checkIfPathExistsWithHost(
ingresses,
host.Host,
item,
params.name
);
if (isExists) {
errors[`hosts[${hi}].paths[${idx}].path`] =
'Path is already in use with the same host';
}
}
});
});
setErrors(errors);
if (Object.keys(errors).length > 0) {
return false;
}
return true;
},
[ingresses, environmentId, isEdit, params.name]
);
const debouncedValidate = useMemo(() => debounce(validate, 300), [validate]);
useEffect(() => {
if (namespace.length > 0) {
validate(
debouncedValidate(
ingressRule,
ingressNames || [],
servicesOptions || [],
@ -302,6 +449,7 @@ export function CreateIngressView() {
ingressNames,
servicesOptions,
existingIngressClass,
debouncedValidate,
]);
return (
@ -361,146 +509,6 @@ export function CreateIngressView() {
</>
);
function validate(
ingressRule: Rule,
ingressNames: string[],
serviceOptions: Option<string>[],
existingIngressClass?: IngressController
) {
const errors: Record<string, ReactNode> = {};
const rule = { ...ingressRule };
// User cannot edit the namespace and the ingress name
if (!isEdit) {
if (!rule.Namespace) {
errors.namespace = 'Namespace is required';
}
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
if (!rule.IngressName) {
errors.ingressName = 'Ingress name is required';
} else if (!nameRegex.test(rule.IngressName)) {
errors.ingressName =
"This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').";
} else if (ingressNames.includes(rule.IngressName)) {
errors.ingressName = 'Ingress name already exists';
}
if (!rule.IngressClassName) {
errors.className = 'Ingress class is required';
}
}
if (isEdit && !ingressRule.IngressClassName) {
errors.className =
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.';
}
if (
isEdit &&
(!existingIngressClass ||
(existingIngressClass && !existingIngressClass.Availability)) &&
ingressRule.IngressClassName
) {
if (!rule.IngressType) {
errors.className =
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.';
} else {
errors.className =
'Currently set to an ingress class that you do not have access to - you must select a valid class.';
}
}
const duplicatedAnnotations: string[] = [];
rule.Annotations?.forEach((a, i) => {
if (!a.Key) {
errors[`annotations.key[${i}]`] = 'Annotation key is required';
} else if (duplicatedAnnotations.includes(a.Key)) {
errors[`annotations.key[${i}]`] = 'Annotation cannot be duplicated';
}
if (!a.Value) {
errors[`annotations.value[${i}]`] = 'Annotation value is required';
}
duplicatedAnnotations.push(a.Key);
});
const duplicatedHosts: string[] = [];
// Check if the paths are duplicates
rule.Hosts?.forEach((host, hi) => {
if (!host.NoHost) {
if (!host.Host) {
errors[`hosts[${hi}].host`] = 'Host is required';
} else if (duplicatedHosts.includes(host.Host)) {
errors[`hosts[${hi}].host`] = 'Host cannot be duplicated';
}
duplicatedHosts.push(host.Host);
}
// Validate service
host.Paths?.forEach((path, pi) => {
if (!path.ServiceName) {
errors[`hosts[${hi}].paths[${pi}].servicename`] =
'Service name is required';
}
if (
isEdit &&
path.ServiceName &&
!serviceOptions.find((s) => s.value === path.ServiceName)
) {
errors[`hosts[${hi}].paths[${pi}].servicename`] = (
<span>
Currently set to {path.ServiceName}, which does not exist. You can
create a service with this name for a particular deployment via{' '}
<Link
to="kubernetes.applications"
params={{ id: environmentId }}
className="text-primary"
target="_blank"
>
Applications
</Link>
, and on returning here it will be picked up.
</span>
);
}
if (!path.ServicePort) {
errors[`hosts[${hi}].paths[${pi}].serviceport`] =
'Service port is required';
}
});
// Validate paths
const paths = host.Paths.map((path) => path.Route);
paths.forEach((item, idx) => {
if (!item) {
errors[`hosts[${hi}].paths[${idx}].path`] = 'Path cannot be empty';
} else if (paths.indexOf(item) !== idx) {
errors[`hosts[${hi}].paths[${idx}].path`] =
'Paths cannot be duplicated';
} else {
// Validate host and path combination globally
const isExists = checkIfPathExistsWithHost(
ingresses,
host.Host,
item,
params.name
);
if (isExists) {
errors[`hosts[${hi}].paths[${idx}].path`] =
'Path is already in use with the same host';
}
}
});
});
setErrors(errors);
if (Object.keys(errors).length > 0) {
return false;
}
return true;
}
function handleNamespaceChange(ns: string) {
setNamespace(ns);
if (!isEdit) {

View file

@ -10,6 +10,7 @@ import { FormError } from '@@/form-components/FormError';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { Tooltip } from '@@/Tip/Tooltip';
import { Button } from '@@/buttons';
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
import { Annotations } from './Annotations';
import { Rule, ServicePorts } from './types';
@ -199,27 +200,33 @@ export function IngressForm({
</div>
<div className="col-sm-12 text-muted !mb-0 px-0">
<div className="mb-2">Annotations</div>
<p className="vertical-center text-muted small">
<Icon icon={Info} mode="primary" />
<span>
You can specify{' '}
<a
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
target="_black"
>
annotations
</a>{' '}
for the object. See further Kubernetes documentation on{' '}
<a
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
target="_black"
>
well-known annotations
</a>
.
</span>
</p>
<div className="control-label !mb-3 text-left font-medium">
Annotations
<Tooltip
message={
<div className="vertical-center">
<span>
You can specify{' '}
<a
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
target="_black"
>
annotations
</a>{' '}
for the object. See further Kubernetes documentation on{' '}
<a
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
target="_black"
>
well-known annotations
</a>
.
</span>
</div>
}
setHtmlMessage
/>
</div>
</div>
{rule?.Annotations && (
@ -233,38 +240,46 @@ export function IngressForm({
)}
<div className="col-sm-12 anntation-actions p-0">
<Button
className="btn btn-sm btn-light mb-2 !ml-0"
onClick={() => addNewAnnotation()}
icon={Plus}
title="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type."
>
{' '}
add annotation
</Button>
<TooltipWithChildren message="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type.">
<span>
<Button
className="btn btn-sm btn-light mb-2 !ml-0"
onClick={() => addNewAnnotation()}
icon={Plus}
>
{' '}
Add annotation
</Button>
</span>
</TooltipWithChildren>
{rule.IngressType === 'nginx' && (
<>
<Button
className="btn btn-sm btn-light mb-2 ml-2"
onClick={() => addNewAnnotation('rewrite')}
icon={Plus}
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
data-cy="add-rewrite-annotation"
>
{' '}
Add rewrite annotation
</Button>
<TooltipWithChildren message="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to.">
<span>
<Button
className="btn btn-sm btn-light mb-2 ml-2"
onClick={() => addNewAnnotation('rewrite')}
icon={Plus}
data-cy="add-rewrite-annotation"
>
Add rewrite annotation
</Button>
</span>
</TooltipWithChildren>
<Button
className="btn btn-sm btn-light mb-2 ml-2"
onClick={() => addNewAnnotation('regex')}
icon={Plus}
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
data-cy="add-regex-annotation"
>
Add regular expression annotation
</Button>
<TooltipWithChildren message="Enable use of regular expressions in ingress paths (set in the ingress details of an application). Use this along with rewrite-target to specify the regex capturing group to be replaced, e.g. path regex of ^/foo/(,*) and rewrite-target of /bar/$1 rewrites example.com/foo/account to example.com/bar/account.">
<span>
<Button
className="btn btn-sm btn-light mb-2 ml-2"
onClick={() => addNewAnnotation('regex')}
icon={Plus}
data-cy="add-regex-annotation"
>
Add regular expression annotation
</Button>
</span>
</TooltipWithChildren>
</>
)}