mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(k8sconfigure): migrate configure to react [EE-5524] (#10218)
This commit is contained in:
parent
0f1e77a6d5
commit
515b02813b
59 changed files with 1819 additions and 833 deletions
|
@ -420,9 +420,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
|||
url: '/configure',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/configure/configure.html',
|
||||
controller: 'KubernetesConfigureController',
|
||||
controllerAs: 'ctrl',
|
||||
component: 'kubernetesConfigureView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import angular from 'angular';
|
|||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { IngressClassDatatable } from '@/react/kubernetes/cluster/ingressClass/IngressClassDatatable';
|
||||
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||
import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector';
|
||||
import { KubeApplicationAccessPolicySelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationAccessPolicySelector';
|
||||
|
@ -30,6 +30,7 @@ export const ngModule = angular
|
|||
'onChangeControllers',
|
||||
'description',
|
||||
'ingressControllers',
|
||||
'initialIngressControllers',
|
||||
'allowNoneIngressClass',
|
||||
'isLoading',
|
||||
'noIngressControllerLabel',
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ServicesView } from '@/react/kubernetes/services/ServicesView';
|
|||
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
|
||||
import { ConfigmapsAndSecretsView } from '@/react/kubernetes/configs/ListView/ConfigmapsAndSecretsView';
|
||||
import { ApplicationDetailsView } from '@/react/kubernetes/applications/DetailsView/ApplicationDetailsView';
|
||||
import { ConfigureView } from '@/react/kubernetes/cluster/ConfigureView';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.kubernetes.react.views', [])
|
||||
|
@ -43,6 +44,10 @@ export const viewsModule = angular
|
|||
[]
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'kubernetesConfigureView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ConfigureView))), [])
|
||||
)
|
||||
.component(
|
||||
'kubernetesDashboardView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
|
||||
|
|
|
@ -1,347 +0,0 @@
|
|||
<page-header
|
||||
ng-if="ctrl.state.viewReady"
|
||||
title="'Kubernetes features configuration'"
|
||||
breadcrumbs="[
|
||||
{ label:'Environments', link:'portainer.endpoints' },
|
||||
{
|
||||
label:ctrl.endpoint.Name,
|
||||
link: 'portainer.endpoints.endpoint',
|
||||
linkParams:{id: ctrl.endpoint.Id}
|
||||
},
|
||||
'Kubernetes configuration'
|
||||
]"
|
||||
reload="true"
|
||||
>
|
||||
</page-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesClusterSetupForm">
|
||||
<div class="col-sm-12 form-section-title"> Networking - Services </div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small">
|
||||
<p> Enabling the load balancer feature will allow users to expose application they deploy over an external IP address assigned by cloud provider. </p>
|
||||
<div class="!inline-flex gap-1 !align-top">
|
||||
<div class="icon icon-sm"><pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon></div>
|
||||
<div>Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-4">
|
||||
<label class="control-label col-sm-5 col-lg-4 px-0 text-left"> Allow users to use external load balancer </label>
|
||||
<label class="switch col-sm-8 mb-0">
|
||||
<input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><span class="slider round" data-cy="kubeSetup-loadBalancerToggle"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Networking - Ingresses </div>
|
||||
|
||||
<ingress-class-datatable
|
||||
on-change-controllers="(ctrl.onChangeControllers)"
|
||||
allow-none-ingress-class="ctrl.formValues.AllowNoneIngressClass"
|
||||
ingress-controllers="ctrl.originalIngressControllers"
|
||||
is-loading="ctrl.isIngressControllersLoading"
|
||||
description="'Enabling ingress controllers in your cluster allows them to be available in the Portainer UI for users to publish applications over HTTP/HTTPS. A controller must have a class name for it to be included here.'"
|
||||
no-ingress-controller-label="'No supported ingress controllers found.'"
|
||||
view="'cluster'"
|
||||
></ingress-class-datatable>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="ctrl.formValues.AllowNoneIngressClass"
|
||||
name="'allowNoIngressClass'"
|
||||
label="'Allow ingress class to be set to "none"'"
|
||||
tooltip="'This allows users setting up ingresses to select "none" as the ingress class.'"
|
||||
on-change="(ctrl.onToggleAllowNoneIngressClass)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="ctrl.formValues.IngressAvailabilityPerNamespace"
|
||||
name="'ingressAvailabilityPerNamespace'"
|
||||
label="'Configure ingress controller availability per namespace'"
|
||||
tooltip="'This allows an administrator to configure, in each namespace, which ingress controllers will be available for users to select when setting up ingresses for applications.'"
|
||||
on-change="(ctrl.onToggleIngressAvailabilityPerNamespace)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="ctrl.formValues.RestrictStandardUserIngressW"
|
||||
name="'restrictStandardUserIngressW'"
|
||||
label="'Only allow admins to deploy ingresses'"
|
||||
feature-id="ctrl.limitedFeatureIngressDeploy"
|
||||
tooltip="'Enforces only allowing admins to deploy ingresses (and disallows standard users from doing so).'"
|
||||
on-change="(ctrl.onToggleRestrictStandardUserIngressW)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8 text-muted'"
|
||||
data-cy="kubeSetup-restrictStandardUserIngressWToggle"
|
||||
disabled="!ctrl.isRBACEnabled"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 !inline-flex gap-1 !align-top">
|
||||
<div class="icon icon-sm"><pr-icon icon="'alert-circle'" mode="'primary'"></pr-icon></div>
|
||||
<div class="text-muted small"
|
||||
>You may set up ingress defaults (hostnames and annotations) via Create/Edit ingress. Users may then select them via the hostname dropdown in Create/Edit
|
||||
application.</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- auto update window -->
|
||||
<div class="col-sm-12 form-section-title"> Change Window Settings </div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="ctrl.state.autoUpdateSettings.Enabled"
|
||||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable Change Window'"
|
||||
feature-id="ctrl.limitedFeatureAutoWindow"
|
||||
tooltip="'GitOps updates to stacks or applications outside the defined change window will not occur.'"
|
||||
on-change="(ctrl.onToggleAutoUpdate)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8 text-muted'"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<time-window-picker ng-show="ctrl.state.autoUpdateSettings.Enabled" time-window="ctrl.state.autoUpdateSettings" time-zone="ctrl.state.timeZone"></time-window-picker>
|
||||
|
||||
<!-- #region SECURITY -->
|
||||
<div class="col-sm-12 form-section-title"> Security </div>
|
||||
|
||||
<div
|
||||
ng-if="!ctrl.isRBACEnabled"
|
||||
class="small mb-6 mt-1 flex w-full gap-1 rounded-lg border border-solid border-warning-5 bg-warning-2 p-4 text-warning-8 th-highcontrast:bg-yellow-11 th-highcontrast:text-white th-dark:bg-yellow-11 th-dark:text-white"
|
||||
>
|
||||
<div class="mt-0.5">
|
||||
<pr-icon icon="'alert-triangle'" feather="true" class-name="'text-warning-7 th-dark:text-white th-highcontrast:text-white'"></pr-icon>
|
||||
</div>
|
||||
<div>
|
||||
<p> Your cluster does not have Kubernetes role-based access control (RBAC) enabled. </p>
|
||||
<p> This means you can't use Portainer RBAC functionality to regulate access to environment resources based on user roles. </p>
|
||||
<p class="mb-0">
|
||||
To enable RBAC, start the <a
|
||||
class="th-highcontrast:text-blue-4 th-dark:text-blue-7"
|
||||
href="https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver"
|
||||
target="_blank"
|
||||
>API server</a
|
||||
> with the <code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">--authorization-mode</code> flag set to a
|
||||
comma-separated list that includes <code class="bg-gray-4 th-highcontrast:bg-black th-dark:bg-black">RBAC</code>, for example:
|
||||
<code class="bg-gray-4 box-decoration-clone th-highcontrast:bg-black th-dark:bg-black">kube-apiserver --authorization-mode=Example1,RBAC,Example2</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
By default, all the users have access to the default namespace. Enable this option to set accesses on the default namespace.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="ctrl.formValues.RestrictDefaultNamespace"
|
||||
name="'restrictDefaultNs'"
|
||||
label="'Restrict access to the default namespace'"
|
||||
on-change="(ctrl.onToggleRestrictNs)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8 text-muted'"
|
||||
data-cy="kubeSetup-restrictDefaultNsToggle"
|
||||
disabled="!ctrl.isRBACEnabled"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Resources and Metrics </div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small">
|
||||
<p>
|
||||
By ENABLING resource over-commit, you are able to assign more resources to namespaces than is physically available in the cluster. This may lead to unexpected
|
||||
deployment failures if there is insufficient resource to service demand.
|
||||
</p>
|
||||
<div class="mt-1 inline-flex gap-1 !align-top">
|
||||
<div class="icon icon-sm"><pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon></div>
|
||||
<div
|
||||
>By DISABLING resource over-commit (highly recommended), you are only able to assign resources to namespaces that are less (in aggregate) than the cluster total
|
||||
minus any system resource reservation.</div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 mt-2">
|
||||
<por-switch-field
|
||||
data-cy="'kubeSetup-resourceOverCommitToggle'"
|
||||
label="'Allow resource over-commit'"
|
||||
name="'resource-over-commit-switch'"
|
||||
feature-id="ctrl.limitedFeature"
|
||||
checked="ctrl.formValues.EnableResourceOverCommit"
|
||||
on-change="(ctrl.onChangeEnableResourceOverCommit)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small">
|
||||
<p> Enabling this feature will allow users to use specific features like autoscaling and to see container and node resource usage. </p>
|
||||
<div class="mt-1 !inline-flex gap-1 !align-top">
|
||||
<div class="icon icon-small"><pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon></div>
|
||||
<div
|
||||
>Ensure that
|
||||
<a href="https://kubernetes.io/docs/tasks/debug-application-cluster/resource-metrics-pipeline/#metrics-server" target="_blank">metrics server</a> or
|
||||
<a href="https://github.com/kubernetes-sigs/prometheus-adapter" target="_blank">prometheus</a> is running inside your cluster.</div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label col-sm-5 col-lg-4 px-0 text-left"> Enable features using the metrics API </label>
|
||||
<label class="switch col-sm-8">
|
||||
<input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" ng-change="ctrl.enableMetricsServer()" />
|
||||
<span class="slider round" data-cy="kubeSetup-metricsToggle"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="ctrl.state.metrics.pending && ctrl.state.metrics.userClick" class="col-sm-12 small text-muted" style="margin-top: 5px">
|
||||
Checking metrics API... <pr-icon icon="'loader'" class-name="'ml-0.5'"></pr-icon>
|
||||
</div>
|
||||
<div
|
||||
ng-if="!ctrl.state.metrics.pending && ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
|
||||
class="col-sm-12 small text-muted vertical-center"
|
||||
style="margin-top: 5px"
|
||||
>
|
||||
<pr-icon icon="'check'" mode="'success'"></pr-icon> Successfully reached metrics API
|
||||
</div>
|
||||
<div
|
||||
ng-if="!ctrl.state.metrics.pending && !ctrl.state.metrics.isServerRunning && ctrl.state.metrics.userClick"
|
||||
class="col-sm-12 small text-muted vertical-center mt-2"
|
||||
>
|
||||
<pr-icon icon="'x'" mode="'danger'"></pr-icon> Unable to reach metrics API, make sure metrics server is properly deployed inside that cluster.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Available storage options </div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
||||
<div class="col-sm-12 small text-muted vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
Unable to detect any storage class available to persist data. Users won't be able to persist application data inside this cluster.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<p>
|
||||
Select which storage options will be available for use when deploying applications. Have a look at your storage driver documentation to figure out which access
|
||||
policy to configure and if the volume expansion capability is supported.
|
||||
</p>
|
||||
<p>
|
||||
You can find more information about access modes
|
||||
<a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes" target="_blank">in the official Kubernetes documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<div style="margin-top: 10px" class="col-sm-12">
|
||||
<table class="table" style="table-layout: fixed">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td>Storage</td>
|
||||
<td>Shared access policy</td>
|
||||
<td>Volume expansion</td>
|
||||
</tr>
|
||||
<tr ng-repeat="class in ctrl.StorageClasses">
|
||||
<td>
|
||||
<div class="flex h-full flex-row items-center">
|
||||
<label class="switch mb-0 mr-2">
|
||||
<input type="checkbox" ng-model="class.selected" /><span class="slider round" data-cy="kubeSetup-storageToggle{{ class.Name }}"></span>
|
||||
</label>
|
||||
<span>{{ class.Name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<storage-access-mode-selector
|
||||
options="ctrl.availableAccessModes"
|
||||
value="class.AccessModes"
|
||||
on-change="(ctrl.onChangeStorageClassAccessMode)"
|
||||
storage-class-name="class.Name"
|
||||
></storage-access-mode-selector>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex h-full flex-row items-center">
|
||||
<label class="switch mb-0 mr-2"
|
||||
><input type="checkbox" ng-model="class.AllowVolumeExpansion" /><span
|
||||
class="slider round"
|
||||
data-cy="kubeSetup-storageExpansionToggle{{ class.Name }}"
|
||||
></span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span ng-if="!ctrl.hasValidStorageConfiguration()" class="text-muted small vertical-center mt-2">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||
Shared access policy configuration required
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-click="ctrl.configure()"
|
||||
ng-disabled="ctrl.state.actionInProgress || !kubernetesClusterSetupForm.$valid || !ctrl.hasValidStorageConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
analytics-on
|
||||
analytics-if="ctrl.restrictDefaultToggledOn()"
|
||||
analytics-category="kubernetes"
|
||||
analytics-event="kubernetes-configure"
|
||||
analytics-properties="{ metadata: { restrictAccessToDefaultNamespace: ctrl.formValues.RestrictDefaultNamespace } }"
|
||||
data-cy="kubeSetup-saveConfigurationButton"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,397 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { getIsRBACEnabled } from '@/react/kubernetes/cluster/getIsRBACEnabled';
|
||||
import { ModalType } from '@@/modals/Modal/types';
|
||||
import { getMetricsForAllNodes } from '@/react/kubernetes/services/service.ts';
|
||||
|
||||
class KubernetesConfigureController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $scope, Notifications, KubernetesStorageService, EndpointService, EndpointProvider, KubernetesResourcePoolService, KubernetesIngressService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesStorageService = KubernetesStorageService;
|
||||
this.EndpointService = EndpointService;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
|
||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.configureAsync = this.configureAsync.bind(this);
|
||||
this.areControllersChanged = this.areControllersChanged.bind(this);
|
||||
this.areFormValuesChanged = this.areFormValuesChanged.bind(this);
|
||||
this.areStorageClassesChanged = this.areStorageClassesChanged.bind(this);
|
||||
this.onBeforeOnload = this.onBeforeOnload.bind(this);
|
||||
this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT;
|
||||
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||
this.limitedFeatureIngressDeploy = FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY;
|
||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||
this.onChangeControllers = this.onChangeControllers.bind(this);
|
||||
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
||||
this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this);
|
||||
this.onToggleAllowNoneIngressClass = this.onToggleAllowNoneIngressClass.bind(this);
|
||||
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
||||
this.onToggleRestrictNs = this.onToggleRestrictNs.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region STORAGE CLASSES UI MANAGEMENT */
|
||||
storageClassAvailable() {
|
||||
return this.StorageClasses && this.StorageClasses.length > 0;
|
||||
}
|
||||
|
||||
hasValidStorageConfiguration() {
|
||||
let valid = true;
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
if (item.selected && item.AccessModes.length === 0) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region INGRESS CLASSES UI MANAGEMENT */
|
||||
onChangeControllers(controllerClassMap) {
|
||||
this.ingressControllers = controllerClassMap;
|
||||
}
|
||||
|
||||
hasTraefikIngress() {
|
||||
return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK });
|
||||
}
|
||||
|
||||
toggleAdvancedIngSettings($event) {
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.state.isIngToggleSectionExpanded = !this.state.isIngToggleSectionExpanded;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleAllowNoneIngressClass() {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.AllowNoneIngressClass = !this.formValues.AllowNoneIngressClass;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleIngressAvailabilityPerNamespace() {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.IngressAvailabilityPerNamespace = !this.formValues.IngressAvailabilityPerNamespace;
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region RESOURCES AND METRICS */
|
||||
|
||||
onChangeEnableResourceOverCommit(enabled) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.EnableResourceOverCommit = enabled;
|
||||
if (enabled) {
|
||||
this.formValues.ResourceOverCommitPercentage = 20;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region CONFIGURE */
|
||||
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
||||
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
||||
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
endpoint.Kubernetes.Configuration.EnableResourceOverCommit = this.formValues.EnableResourceOverCommit;
|
||||
endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage = this.formValues.ResourceOverCommitPercentage;
|
||||
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
||||
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = this.formValues.IngressAvailabilityPerNamespace;
|
||||
endpoint.Kubernetes.Configuration.AllowNoneIngressClass = this.formValues.AllowNoneIngressClass;
|
||||
endpoint.ChangeWindow = this.state.autoUpdateSettings;
|
||||
}
|
||||
|
||||
transformFormValues() {
|
||||
const storageClasses = _.map(this.StorageClasses, (item) => {
|
||||
if (item.selected) {
|
||||
const res = new KubernetesStorageClass();
|
||||
res.Name = item.Name;
|
||||
res.AccessModes = _.map(item.AccessModes, 'Name');
|
||||
res.Provisioner = item.Provisioner;
|
||||
res.AllowVolumeExpansion = item.AllowVolumeExpansion;
|
||||
return res;
|
||||
}
|
||||
});
|
||||
_.pull(storageClasses, undefined);
|
||||
|
||||
const ingressClasses = _.without(
|
||||
_.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic)),
|
||||
undefined
|
||||
);
|
||||
_.pull(ingressClasses, undefined);
|
||||
|
||||
return [storageClasses, ingressClasses];
|
||||
}
|
||||
|
||||
async removeIngressesAcrossNamespaces() {
|
||||
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||
if (!ingressesToDel.length) {
|
||||
return;
|
||||
}
|
||||
const promises = [];
|
||||
const oldEndpoint = this.EndpointProvider.currentEndpoint();
|
||||
this.EndpointProvider.setCurrentEndpoint(this.endpoint);
|
||||
|
||||
try {
|
||||
const allResourcePools = await this.KubernetesResourcePoolService.get();
|
||||
const resourcePools = _.filter(
|
||||
allResourcePools,
|
||||
(resourcePool) =>
|
||||
!KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) &&
|
||||
!KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name) &&
|
||||
resourcePool.Namespace.Status === 'Active'
|
||||
);
|
||||
|
||||
ingressesToDel.forEach((ingress) => {
|
||||
resourcePools.forEach((resourcePool) => {
|
||||
promises.push(this.KubernetesIngressService.delete(resourcePool.Namespace.Name, ingress.Name));
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
this.EndpointProvider.setCurrentEndpoint(oldEndpoint);
|
||||
}
|
||||
|
||||
const responses = await Promise.allSettled(promises);
|
||||
responses.forEach((respons) => {
|
||||
if (respons.status == 'rejected' && respons.reason.err.status != 404) {
|
||||
throw respons.reason;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableMetricsServer() {
|
||||
return this.$async(async () => {
|
||||
if (this.formValues.UseServerMetrics) {
|
||||
this.state.metrics.userClick = true;
|
||||
this.state.metrics.pending = true;
|
||||
try {
|
||||
await getMetricsForAllNodes(this.endpoint.Id);
|
||||
this.state.metrics.isServerRunning = true;
|
||||
this.state.metrics.pending = false;
|
||||
this.state.metrics.userClick = true;
|
||||
this.formValues.UseServerMetrics = true;
|
||||
} catch (_) {
|
||||
this.state.metrics.isServerRunning = false;
|
||||
this.state.metrics.pending = false;
|
||||
this.formValues.UseServerMetrics = false;
|
||||
}
|
||||
} else {
|
||||
this.state.metrics.userClick = false;
|
||||
this.formValues.UseServerMetrics = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async configureAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const [storageClasses, ingressClasses] = this.transformFormValues();
|
||||
|
||||
await this.removeIngressesAcrossNamespaces();
|
||||
|
||||
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||
// updateIngressControllerClassMap must be done after updateEndpoint, as a hacky workaround. A better solution: saving ingresscontrollers somewhere else, is being discussed
|
||||
await updateIngressControllerClassMap(this.state.endpointId, this.ingressControllers || []);
|
||||
this.state.isSaving = true;
|
||||
const storagePromises = _.map(storageClasses, (storageClass) => {
|
||||
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||
if (oldStorageClass) {
|
||||
return this.KubernetesStorageService.patch(this.state.endpointId, oldStorageClass, storageClass);
|
||||
}
|
||||
});
|
||||
await Promise.all(storagePromises);
|
||||
this.$state.reload();
|
||||
this.Notifications.success('Success', 'Configuration successfully applied');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to apply configuration');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
configure() {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
restrictDefaultToggledOn() {
|
||||
return this.formValues.RestrictDefaultNamespace && !this.oldFormValues.RestrictDefaultNamespace;
|
||||
}
|
||||
|
||||
onToggleAutoUpdate(value) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.state.autoUpdateSettings.Enabled = value;
|
||||
});
|
||||
}
|
||||
|
||||
onChangeStorageClassAccessMode(storageClassName, accessModes) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
const storageClass = this.StorageClasses.find((item) => item.Name === storageClassName);
|
||||
|
||||
if (!storageClass) {
|
||||
throw new Error('Storage class not found');
|
||||
}
|
||||
|
||||
storageClass.AccessModes = accessModes;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleRestrictNs() {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.RestrictDefaultNamespace = !this.formValues.RestrictDefaultNamespace;
|
||||
});
|
||||
}
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
displayConfigureClassPanel: {},
|
||||
viewReady: false,
|
||||
isIngToggleSectionExpanded: false,
|
||||
endpointId: this.$state.params.endpointId,
|
||||
duplicates: {
|
||||
ingressClasses: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
metrics: {
|
||||
pending: false,
|
||||
isServerRunning: false,
|
||||
userClick: false,
|
||||
},
|
||||
timeZone: '',
|
||||
isSaving: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
EnableResourceOverCommit: true,
|
||||
ResourceOverCommitPercentage: 20,
|
||||
IngressClasses: [],
|
||||
RestrictDefaultNamespace: false,
|
||||
enableAutoUpdateTimeWindow: false,
|
||||
IngressAvailabilityPerNamespace: false,
|
||||
};
|
||||
|
||||
// default to true if error is thrown
|
||||
this.isRBACEnabled = true;
|
||||
|
||||
this.isIngressControllersLoading = true;
|
||||
try {
|
||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
|
||||
[this.StorageClasses, this.endpoint, this.isRBACEnabled] = await Promise.all([
|
||||
this.KubernetesStorageService.get(this.state.endpointId),
|
||||
this.EndpointService.endpoint(this.state.endpointId),
|
||||
getIsRBACEnabled(this.state.endpointId),
|
||||
]);
|
||||
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.state.endpointId });
|
||||
this.originalIngressControllers = structuredClone(this.ingressControllers) || [];
|
||||
|
||||
this.state.autoUpdateSettings = this.endpoint.ChangeWindow;
|
||||
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
||||
if (storage) {
|
||||
item.selected = true;
|
||||
item.AccessModes = storage.AccessModes.map((name) => this.availableAccessModes.find((accessMode) => accessMode.Name === name));
|
||||
} else if (this.availableAccessModes.length) {
|
||||
// set a default access mode if the storage class is not enabled and there are available access modes
|
||||
item.AccessModes = [this.availableAccessModes[0]];
|
||||
}
|
||||
});
|
||||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
this.formValues.EnableResourceOverCommit = this.endpoint.Kubernetes.Configuration.EnableResourceOverCommit;
|
||||
this.formValues.ResourceOverCommitPercentage = this.endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage;
|
||||
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||
ic.IsNew = false;
|
||||
ic.NeedsDeletion = false;
|
||||
return ic;
|
||||
});
|
||||
this.formValues.IngressAvailabilityPerNamespace = this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace;
|
||||
this.formValues.AllowNoneIngressClass = this.endpoint.Kubernetes.Configuration.AllowNoneIngressClass;
|
||||
|
||||
this.oldStorageClasses = angular.copy(this.StorageClasses);
|
||||
this.oldFormValues = angular.copy(this.formValues);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment configuration');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
this.isIngressControllersLoading = false;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeOnload);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
$onDestroy() {
|
||||
window.removeEventListener('beforeunload', this.onBeforeOnload);
|
||||
}
|
||||
|
||||
areControllersChanged() {
|
||||
return !_.isEqual(this.ingressControllers, this.originalIngressControllers);
|
||||
}
|
||||
|
||||
areFormValuesChanged() {
|
||||
return !_.isEqual(this.formValues, this.oldFormValues);
|
||||
}
|
||||
|
||||
areStorageClassesChanged() {
|
||||
// angular is pesky and modifies this.StorageClasses (adds $$hashkey to each item)
|
||||
// angular.toJson removes this to make the comparison work
|
||||
const storageClassesWithoutHashKey = angular.toJson(this.StorageClasses);
|
||||
const oldStorageClassesWithoutHashKey = angular.toJson(this.oldStorageClasses);
|
||||
return !_.isEqual(storageClassesWithoutHashKey, oldStorageClassesWithoutHashKey);
|
||||
}
|
||||
|
||||
onBeforeOnload(event) {
|
||||
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged() || this.areStorageClassesChanged())) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
}
|
||||
|
||||
uiCanExit() {
|
||||
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged() || this.areStorageClassesChanged()) && !this.isIngressControllersLoading) {
|
||||
return confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'You currently have unsaved changes in the cluster setup view. Are you sure you want to leave?',
|
||||
modalType: ModalType.Warn,
|
||||
confirmButton: buildConfirmButton('Yes', 'danger'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesConfigureController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesConfigureController', KubernetesConfigureController);
|
|
@ -190,6 +190,7 @@
|
|||
ng-if="$ctrl.state.ingressAvailabilityPerNamespace"
|
||||
on-change-controllers="($ctrl.onChangeIngressControllerAvailability)"
|
||||
ingress-controllers="$ctrl.ingressControllers"
|
||||
initial-ingress-controllers="$ctrl.initialIngressControllers"
|
||||
description="'Enable the ingress controllers that users can select when publishing applications in this namespace.'"
|
||||
no-ingress-controller-label="'No ingress controllers found in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster.'"
|
||||
view="'namespace'"
|
||||
|
|
|
@ -7,7 +7,7 @@ import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
|||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/useIngressControllerClassMap';
|
||||
|
||||
class KubernetesCreateResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
@ -201,6 +201,7 @@ class KubernetesCreateResourcePoolController {
|
|||
this.ingressControllers = [];
|
||||
if (this.state.ingressAvailabilityPerNamespace) {
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, allowedOnly: true });
|
||||
this.initialIngressControllers = structuredClone(this.ingressControllers);
|
||||
}
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
|
|
|
@ -174,6 +174,7 @@
|
|||
ng-if="ctrl.state.ingressAvailabilityPerNamespace"
|
||||
on-change-controllers="(ctrl.onChangeIngressControllerAvailability)"
|
||||
ingress-controllers="ctrl.ingressControllers"
|
||||
initial-ingress-controllers="$ctrl.initialIngressControllers"
|
||||
description="'Enable the ingress controllers that users can select when publishing applications in this namespace.'"
|
||||
no-ingress-controller-label="'No ingress controllers found in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster.'"
|
||||
view="'namespace'"
|
||||
|
|
|
@ -13,7 +13,7 @@ import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
|||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { updateIngressControllerClassMap, getIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
import { updateIngressControllerClassMap, getIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/useIngressControllerClassMap';
|
||||
import { confirmUpdate } from '@@/modals/confirm';
|
||||
import { confirmUpdateNamespace } from '@/react/kubernetes/namespaces/ItemView/ConfirmUpdateNamespace';
|
||||
import { getMetricsForAllPods } from '@/react/kubernetes/services/service.ts';
|
||||
|
@ -373,6 +373,7 @@ class KubernetesResourcePoolController {
|
|||
this.ingressControllers = [];
|
||||
if (this.state.ingressAvailabilityPerNamespace) {
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, namespace: name });
|
||||
this.initialIngressControllers = structuredClone(this.ingressControllers);
|
||||
}
|
||||
|
||||
this.pool = _.find(pools, { Namespace: { Name: name } });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue