mirror of https://github.com/portainer/portainer
fix(namespace): create ns qa feedback [EE-2226] (#10474)
parent
bcb3f918d1
commit
07ec2ffe5e
|
@ -11,6 +11,7 @@ type K8sNamespaceDetails struct {
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Annotations map[string]string `json:"Annotations"`
|
Annotations map[string]string `json:"Annotations"`
|
||||||
ResourceQuota *K8sResourceQuota `json:"ResourceQuota"`
|
ResourceQuota *K8sResourceQuota `json:"ResourceQuota"`
|
||||||
|
Owner string `json:"Owner"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type K8sResourceQuota struct {
|
type K8sResourceQuota struct {
|
||||||
|
|
|
@ -63,15 +63,21 @@ func (kcl *KubeClient) GetNamespace(name string) (portainer.K8sNamespaceInfo, er
|
||||||
|
|
||||||
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
|
// CreateNamespace creates a new ingress in a given namespace in a k8s endpoint.
|
||||||
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
|
func (kcl *KubeClient) CreateNamespace(info models.K8sNamespaceDetails) error {
|
||||||
|
portainerLabels := map[string]string{
|
||||||
|
"io.portainer.kubernetes.resourcepool.name": info.Name,
|
||||||
|
"io.portainer.kubernetes.resourcepool.owner": info.Owner,
|
||||||
|
}
|
||||||
|
|
||||||
var ns v1.Namespace
|
var ns v1.Namespace
|
||||||
ns.Name = info.Name
|
ns.Name = info.Name
|
||||||
ns.Annotations = info.Annotations
|
ns.Annotations = info.Annotations
|
||||||
|
ns.Labels = portainerLabels
|
||||||
|
|
||||||
resourceQuota := &v1.ResourceQuota{
|
resourceQuota := &v1.ResourceQuota{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "portainer-rq-" + info.Name,
|
Name: "portainer-rq-" + info.Name,
|
||||||
Namespace: info.Name,
|
Namespace: info.Name,
|
||||||
|
Labels: portainerLabels,
|
||||||
},
|
},
|
||||||
Spec: v1.ResourceQuotaSpec{
|
Spec: v1.ResourceQuotaSpec{
|
||||||
Hard: v1.ResourceList{},
|
Hard: v1.ResourceList{},
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
class="searchInput"
|
class="searchInput"
|
||||||
ng-model="$ctrl.state.textFilter"
|
ng-model="$ctrl.state.textFilter"
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
placeholder="Search for a namespace..."
|
placeholder="Search..."
|
||||||
auto-focus
|
auto-focus
|
||||||
ng-model-options="{ debounce: 300 }"
|
ng-model-options="{ debounce: 300 }"
|
||||||
data-cy="k8sNamespace-namespaceSearchInput"
|
data-cy="k8sNamespace-namespaceSearchInput"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||||
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
||||||
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '@@/Widget';
|
import { Widget, WidgetBody } from '@@/Widget';
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ export function CreateNamespaceForm() {
|
||||||
const { data: registries } = useEnvironmentRegistries(environmentId, {
|
const { data: registries } = useEnvironmentRegistries(environmentId, {
|
||||||
hideDefault: true,
|
hideDefault: true,
|
||||||
});
|
});
|
||||||
|
const { user } = useCurrentUser();
|
||||||
// for namespace create, show ingress classes that are allowed in the current environment.
|
// for namespace create, show ingress classes that are allowed in the current environment.
|
||||||
// the ingressClasses show the none option, so we don't need to add it here.
|
// the ingressClasses show the none option, so we don't need to add it here.
|
||||||
const { data: ingressClasses } = useIngressControllerClassMapQuery({
|
const { data: ingressClasses } = useIngressControllerClassMapQuery({
|
||||||
|
@ -65,7 +67,7 @@ export function CreateNamespaceForm() {
|
||||||
<Formik
|
<Formik
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={(values) => handleSubmit(values, user.Username)}
|
||||||
validateOnMount
|
validateOnMount
|
||||||
validationSchema={getNamespaceValidationSchema(
|
validationSchema={getNamespaceValidationSchema(
|
||||||
memoryLimit,
|
memoryLimit,
|
||||||
|
@ -78,9 +80,9 @@ export function CreateNamespaceForm() {
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSubmit(values: CreateNamespaceFormValues) {
|
function handleSubmit(values: CreateNamespaceFormValues, userName: string) {
|
||||||
const createNamespacePayload: CreateNamespacePayload =
|
const createNamespacePayload: CreateNamespacePayload =
|
||||||
transformFormValuesToNamespacePayload(values);
|
transformFormValuesToNamespacePayload(values, userName);
|
||||||
const updateRegistriesPayload: UpdateRegistryPayload[] =
|
const updateRegistriesPayload: UpdateRegistryPayload[] =
|
||||||
values.registries.flatMap((registryFormValues) => {
|
values.registries.flatMap((registryFormValues) => {
|
||||||
// find the matching registry from the cluster registries
|
// find the matching registry from the cluster registries
|
||||||
|
|
|
@ -15,6 +15,7 @@ export type CreateNamespaceFormValues = {
|
||||||
|
|
||||||
export type CreateNamespacePayload = {
|
export type CreateNamespacePayload = {
|
||||||
Name: string;
|
Name: string;
|
||||||
|
Owner: string;
|
||||||
ResourceQuota: ResourceQuotaPayload;
|
ResourceQuota: ResourceQuotaPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { CreateNamespaceFormValues, CreateNamespacePayload } from './types';
|
import { CreateNamespaceFormValues, CreateNamespacePayload } from './types';
|
||||||
|
|
||||||
export function transformFormValuesToNamespacePayload(
|
export function transformFormValuesToNamespacePayload(
|
||||||
createNamespaceFormValues: CreateNamespaceFormValues
|
createNamespaceFormValues: CreateNamespaceFormValues,
|
||||||
|
owner: string
|
||||||
): CreateNamespacePayload {
|
): CreateNamespacePayload {
|
||||||
const memoryInBytes =
|
const memoryInBytes =
|
||||||
Number(createNamespaceFormValues.resourceQuota.memory) * 10 ** 6;
|
Number(createNamespaceFormValues.resourceQuota.memory) * 10 ** 6;
|
||||||
return {
|
return {
|
||||||
Name: createNamespaceFormValues.name,
|
Name: createNamespaceFormValues.name,
|
||||||
|
Owner: owner,
|
||||||
ResourceQuota: {
|
ResourceQuota: {
|
||||||
enabled: createNamespaceFormValues.resourceQuota.enabled,
|
enabled: createNamespaceFormValues.resourceQuota.enabled,
|
||||||
cpu: createNamespaceFormValues.resourceQuota.cpu,
|
cpu: createNamespaceFormValues.resourceQuota.cpu,
|
||||||
|
|
|
@ -97,9 +97,7 @@ export function NamespaceInnerForm({
|
||||||
}
|
}
|
||||||
errors={errors.registries}
|
errors={errors.registries}
|
||||||
/>
|
/>
|
||||||
{storageClasses.length > 0 && (
|
{storageClasses.length > 0 && <StorageQuotaFormSection />}
|
||||||
<StorageQuotaFormSection storageClasses={storageClasses} />
|
|
||||||
)}
|
|
||||||
<NamespaceSummary
|
<NamespaceSummary
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
values={values}
|
values={values}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { InlineLoader } from '@@/InlineLoader';
|
import { InlineLoader } from '@@/InlineLoader';
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
import { RegistriesSelector } from './RegistriesSelector';
|
import { RegistriesSelector } from './RegistriesSelector';
|
||||||
|
|
||||||
|
@ -24,10 +25,13 @@ export function RegistriesFormSection({ values, onChange, errors }: Props) {
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<FormSection title="Registries">
|
<FormSection title="Registries">
|
||||||
|
<TextTip color="blue" className="mb-2">
|
||||||
|
Define which registries can be used by users who have access to this
|
||||||
|
namespace.
|
||||||
|
</TextTip>
|
||||||
<FormControl
|
<FormControl
|
||||||
inputId="registries"
|
inputId="registries"
|
||||||
label="Select registries"
|
label="Select registries"
|
||||||
required
|
|
||||||
errors={errors}
|
errors={errors}
|
||||||
>
|
>
|
||||||
{registriesQuery.isLoading && (
|
{registriesQuery.isLoading && (
|
||||||
|
|
|
@ -35,22 +35,12 @@ export function ResourceQuotaFormSection({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection title="Resource Quota">
|
<FormSection title="Resource Quota">
|
||||||
{values.enabled ? (
|
<TextTip color="blue">
|
||||||
<TextTip color="blue">
|
A resource quota sets boundaries on the compute resources a namespace
|
||||||
A namespace is a logical abstraction of a Kubernetes cluster, to
|
can use. It's good practice to set a quota for a namespace to
|
||||||
provide for more flexible management of resources. Best practice is to
|
manage resources effectively. Alternatively, you can disable assigning a
|
||||||
set a quota assignment as this ensures greatest security/stability;
|
quota for unrestricted access (not recommended).
|
||||||
alternatively, you can disable assigning a quota for unrestricted
|
</TextTip>
|
||||||
access (not recommended).
|
|
||||||
</TextTip>
|
|
||||||
) : (
|
|
||||||
<TextTip color="blue">
|
|
||||||
A namespace is a logical abstraction of a Kubernetes cluster, to
|
|
||||||
provide for more flexible management of resources. Resource
|
|
||||||
over-commit is disabled, please assign a capped limit of resources to
|
|
||||||
this namespace.
|
|
||||||
</TextTip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SwitchField
|
<SwitchField
|
||||||
data-cy="k8sNamespaceCreate-resourceAssignmentToggle"
|
data-cy="k8sNamespaceCreate-resourceAssignmentToggle"
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
import { StorageClass } from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
import { StorageQuotaItem } from './StorageQuotaItem';
|
import { StorageQuotaItem } from './StorageQuotaItem';
|
||||||
|
|
||||||
interface Props {
|
export function StorageQuotaFormSection() {
|
||||||
storageClasses: StorageClass[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StorageQuotaFormSection({ storageClasses }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<FormSection title="Storage">
|
<FormSection title="Storage">
|
||||||
<TextTip color="blue">
|
<TextTip color="blue">
|
||||||
|
@ -19,9 +13,7 @@ export function StorageQuotaFormSection({ storageClasses }: Props) {
|
||||||
this namespace.
|
this namespace.
|
||||||
</TextTip>
|
</TextTip>
|
||||||
|
|
||||||
{storageClasses.map((storageClass) => (
|
<StorageQuotaItem />
|
||||||
<StorageQuotaItem key={storageClass.Name} storageClass={storageClass} />
|
|
||||||
))}
|
|
||||||
</FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
import { Database } from 'lucide-react';
|
import { Database } from 'lucide-react';
|
||||||
|
|
||||||
import { StorageClass } from '@/react/portainer/environments/types';
|
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
|
||||||
type Props = {
|
export function StorageQuotaItem() {
|
||||||
storageClass: StorageClass;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function StorageQuotaItem({ storageClass }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<div key={storageClass.Name}>
|
<div>
|
||||||
<FormSectionTitle>
|
<FormSectionTitle>
|
||||||
<div className="vertical-center text-muted inline-flex gap-1 align-top">
|
<div className="vertical-center text-muted inline-flex gap-1 align-top">
|
||||||
<Icon icon={Database} className="!mt-0.5 flex-none" />
|
<Icon icon={Database} className="!mt-0.5 flex-none" />
|
||||||
<span>{storageClass.Name}</span>
|
<span>standard</span>
|
||||||
</div>
|
</div>
|
||||||
</FormSectionTitle>
|
</FormSectionTitle>
|
||||||
<hr className="mt-2 mb-0 w-full" />
|
<hr className="mt-2 mb-0 w-full" />
|
||||||
|
|
Loading…
Reference in New Issue