9af9b70f3e
commite4605d990dAuthor: yi-portainer <yi.chen@portainer.io> Date: Tue Feb 2 17:42:57 2021 +1300 * update portainer version commit768697157cAuthor: LP B <xAt0mZ@users.noreply.github.com> Date: Tue Feb 2 05:00:19 2021 +0100 sec(app): remove unused and vulnerable dependencies (#4801) commitd3086da139Author: cong meng <mcpacino@gmail.com> Date: Tue Feb 2 15:10:06 2021 +1300 fix(k8s) trigger port validation while changing protocol (ce#394) (#4804) Co-authored-by: Simon Meng <simon.meng@portainer.io> commit95894e8047Author: cong meng <mcpacino@gmail.com> Date: Tue Feb 2 15:03:11 2021 +1300 fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805) Co-authored-by: Simon Meng <simon.meng@portainer.io> commit81de55feddAuthor: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Tue Feb 2 11:12:40 2021 +1300 * fix missing kubectl download (#4802) commit84827b8782Author: Steven Kang <skan070@gmail.com> Date: Sun Jan 31 17:32:30 2021 +1300 feat(build): introducing buildx for Windows (#4792) * feat(build): introducing buildx for Windows * feat(build): re-ordered USER * feat(build): Fixed Typo * feat(build): fixed typo commita71e71f481Author: Dmitry Salakhov <to@dimasalakhov.com> Date: Mon Jan 25 19:16:53 2021 +0000 feat(compose): add docker-compose wrapper (#4713) * feat(compose): add docker-compose wrapper ce-187 * fix(compose): pick compose implementation upon startup * Add static compose build for linux * Fix wget * Fix platofrm specific docker-compose download * Keep amd64 architecture as download parameter * Add tmp folder for docker-compose * fix: line endings * add proxy server * logs * Proxy * Add lite transport for compose * Fix local deployment * refactor: pass proxyManager by ref * fix: string conversion * refactor: compose wrapper remove unused code * fix: tests * Add edge * Fix merge issue * refactor: remove unused code * Move server to proxy implementation * Cleanup wrapper and manager * feat: pass max supported compose syntax version with each endpoint * fix: pick compose syntax version * fix: store wrapper version in portainer * Get and show composeSyntaxMaxVersion at stack creation screen * Get and show composeSyntaxMaxVersion at stack editor screen * refactor: proxy server * Fix used tmp * Bump docker-compose to 1.28.0 * remove message for docker compose limitation * fix: markup typo * Rollback docker compose to 1.27.4 * * attempt to fix the windows build issue * * attempt to debug grunt issue * * use console log in grunt file * fix: try to fix windows build by removing indirect deps from go.mod * Remove tmp folder * Remove builder stage * feat(build/windows): add git for Docker Compose * feat(build/windows): add git for Docker Compose * feat(build/windows): add git for Docker Compose * feat(build/windows): add git for Docker Compose * feat(build/windows): add git for Docker Compose * feat(build/windows): add git for Docker Compose - fixed verbose output * refactor: renames * fix(stack): get endpoint by EndpointProvider * fix(stack): use margin to add space between line instead of using br tag Co-authored-by: Stéphane Busso <stephane.busso@gmail.com> Co-authored-by: Simon Meng <simon.meng@portainer.io> Co-authored-by: yi-portainer <yi.chen@portainer.io> Co-authored-by: Steven Kang <skan070@gmail.com> commit83f4c5ec0bAuthor: LP B <xAt0mZ@users.noreply.github.com> Date: Mon Jan 25 02:43:54 2021 +0100 fix(k8s/app): remove advanced deployment panel from app details view (#4730) commit41308d570dAuthor: Maxime Bajeux <max.bajeux@gmail.com> Date: Mon Jan 25 02:14:35 2021 +0100 feat(configurations): Review UI/UX configurations (#4691) * feat(configurations): Review UI/UX configurations * feat(configurations): fix binary secret value * fix(frontend): populate data between simple and advanced modes (#4503) * fix(configuration): parseYaml before create configuration * fix(configurations): change c to C in ConfigurationOwner * fix(application): change configuration index to configuration key in the view * fix(configuration): resolve problem in application create with configuration not overriden. * fix(configuration): fix bad import in helper Co-authored-by: Simon Meng <simon.meng@portainer.io> commit46ff8a01bcAuthor: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Fri Jan 22 03:08:08 2021 +0200 fix(kubernetes/pods): save note (#4675) * feat(kubernetes/pods): introduce patch api * feat(k8s/pods): pod converter * feat(kubernetes/pods): introduce patch api * feat(k8s/pod): add annotations only if needed * fix(k8s/pod): replace class with factory function commit2b257d2785Author: yi-portainer <yi.chen@portainer.io> Date: Thu Jan 21 00:02:22 2021 +1300 Squashed commit of the following 2.0.1 release fixes: commitf90d6b55d6Author: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Wed Jan 13 00:56:19 2021 +0200 feat(service): clear source volume when change type (#4627) * feat(service): clear source volume when change type * feat(service): init volume source to the correct value commit1b82b450d7Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Thu Jan 7 14:47:32 2021 +1300 * bump the APIVersion to 2.0.1 (#4688) commitb78d804881Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Wed Dec 30 23:03:43 2020 +1300 Revert "chore(build): bump Kompose version (#4475)" (#4676) This reverts commit380f106571. Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> commit51b72c12f9Author: Anthony Lapenna <anthony.lapenna@portainer.io> Date: Wed Dec 23 14:45:32 2020 +1300 fix(docker/stack-details): do not display editor tab for external stack (#4650) commit58c04bdbe3Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Tue Dec 22 13:47:11 2020 +1300 + silently continue when downloading artifacts in windows (#4637) commita6320d5222Author: cong meng <mcpacino@gmail.com> Date: Tue Dec 22 13:38:54 2020 +1300 fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618) * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) * fix(frontend) rephrase comments (#4629) Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> Co-authored-by: Simon Meng <simon.meng@portainer.io> Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> commitda41dbb79aAuthor: cong meng <mcpacino@gmail.com> Date: Wed Jan 20 15:19:35 2021 +1300 fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725) Co-authored-by: Simon Meng <simon.meng@portainer.io> commit68d42617f2Author: Maxime Bajeux <max.bajeux@gmail.com> Date: Wed Jan 20 01:02:18 2021 +0100 feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster (#4525) * feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster * fix(applications): if there is at least one node the application can schedule on, then do not show the warning commit8323e22309Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com> Date: Wed Jan 20 12:06:25 2021 +1300 Update issue templates Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question) commit20d4341170Author: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Tue Jan 19 00:10:08 2021 +0200 fix(state): check validity of state (#4609) commit832cafc933Author: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Mon Jan 18 02:59:57 2021 +0200 fix(registries): update password only when not empty (#4669) commitf3c537ac2cAuthor: cong meng <mcpacino@gmail.com> Date: Mon Jan 18 13:02:16 2021 +1300 chore(build): bump Kompose version (#4473) (#4724) Co-authored-by: Simon Meng <simon.meng@portainer.io> commit958baf6283Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com> Date: Mon Jan 18 09:30:17 2021 +1300 Update README.md commit08e392378eAuthor: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Sun Jan 17 09:28:09 2021 +0200 chore(app): fail on angular components missing nginject (#4224) commita2d9734b8bAuthor: Alice Groux <alice.grx@gmail.com> Date: Sun Jan 17 04:50:22 2021 +0100 fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#4511) * fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable * fix(k8s/datatables): reduce size of expand/collapse column commit15aed9fc6fAuthor: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Sun Jan 17 06:23:32 2021 +0530 feat(area/kubernetes): show shared access policy in volume details (#4707) commit121d33538dAuthor: Alice Groux <alice.grx@gmail.com> Date: Fri Jan 15 02:51:36 2021 +0100 fix(k8s/application): validate load balancer ports inputs (#4426) * fix(k8s/application): validate load balancer ports inputs * fix(k8s/application): allow user to only change the protocol on the first port mapping commit7a03351df8Author: Olli Janatuinen <olljanat@users.noreply.github.com> Date: Thu Jan 14 23:05:33 2021 +0200 dep(api): Support Docker Stack 3.8 (#4333) - Linux: Update Docker binary to version 19.03.13 - Windows: Update Docker binary to version 19.03.12 commit0c2987893dAuthor: Alice Groux <alice.grx@gmail.com> Date: Thu Jan 14 03:04:44 2021 +0100 feat(app/images): in advanced mode, remove tooltip and add an information message (#4528) commitd1eddaa188Author: Alice Groux <alice.grx@gmail.com> Date: Thu Jan 14 00:24:56 2021 +0100 feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514) commitd336ada3c2Author: Anthony Lapenna <anthony.lapenna@portainer.io> Date: Wed Jan 13 16:13:27 2021 +1300 feat(k8s/application): review application creation warning style (#4613) commit839198fbffAuthor: Avadhut Tanugade <30384908+mrwhoknows55@users.noreply.github.com> Date: Wed Jan 13 04:49:18 2021 +0530 commit486ffa5bbdAuthor: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Tue Jan 12 23:40:09 2021 +0200 chore(webpack): add source maps (#4471) * chore(webpack): add source maps * feat(build): fetch source maps for 3rd party libs commit4cd468ce21Author: Maxime Bajeux <max.bajeux@gmail.com> Date: Tue Jan 12 02:35:59 2021 +0100 Can't create kubernetes resources with a username longer than 63 characters (#4672) * fix(kubernetes): truncate username when we create resource * fix(k8s): remove forbidden characters in owner label commitcbd7fdc62eAuthor: Chaim Lev-Ari <chiptus@users.noreply.github.com> Date: Tue Jan 12 01:38:49 2021 +0200 feat(docker/stacks): introduce date info for stacks (#4660) * feat(docker/stacks): add creation and update dates * feat(docker/stacks): put ownership column as the last column * feat(docker/stacks): fix the no stacks message * refactor(docker/stacks): make external stacks helpers more readable * feat(docker/stacks): add updated and created by * feat(docker/stacks): toggle updated column * refactor(datatable): create column visibility component Co-authored-by: alice groux <alice.grx@gmail.com> commitb9fe8009ddAuthor: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Mon Jan 11 08:05:19 2021 +0530 feat(image-details): Show labels in images datatable (#4287) * feat(images): show labels in images datatable * move labels to image details view commit6a504e7134Author: Stéphane Busso <sbusso@users.noreply.github.com> Date: Mon Jan 11 14:44:15 2021 +1300 fix(settings): Use default setting if UserSessionTimeout not set (#4521) * fix(settings): Use default settings if UserSessionTimeout not set * Update UserSessionTimeout settings in database if set to empty string commit51ba0876a5Author: Alice Groux <alice.grx@gmail.com> Date: Mon Jan 11 00:51:46 2021 +0100 feat(k8s/configuration): rename add ingress controller button and changed information text (#4540) commit769e6a4c6cAuthor: Alice Groux <alice.grx@gmail.com> Date: Sun Jan 10 23:30:31 2021 +0100 feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541) commit105d1ae519Author: cong meng <mcpacino@gmail.com> Date: Fri Jan 8 15:30:43 2021 +1300 feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) (#4565) * feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) * feat(frontend): change the "Use internal authentication" style to be primary (#3065) * feat(frontend): resize the login with "provider" button to use a 120% font size (#3065) * feat(frontend): remove unused css for h1 tag (#3065) Co-authored-by: Simon Meng <simon.meng@portainer.io> commitcf508065ecAuthor: cong meng <mcpacino@gmail.com> Date: Fri Jan 8 12:51:27 2021 +1300 fix(frontend): application edit page initializes the overridenKeyType of new added configuration key to NONE so that the user can select how to load it (#4548) (#4593) Co-authored-by: Simon Meng <simon.meng@portainer.io> commiteab828279eAuthor: itsconquest <william.conquest@portainer.io> Date: Fri Jan 8 12:46:57 2021 +1300 chore(project): exclude refactors (#4689) commitd5763a970bAuthor: cong meng <mcpacino@gmail.com> Date: Fri Jan 8 12:45:06 2021 +1300 fix(frontend): Resource pool 'created' attribute is showing the time you view it at & not actual creation time (#4568) (#4599) Co-authored-by: Simon Meng <simon.meng@portainer.io> commitc9f68a4d8fAuthor: cong meng <mcpacino@gmail.com> Date: Fri Jan 8 11:55:42 2021 +1300 fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574) Co-authored-by: Simon Meng <simon.meng@portainer.io> commit7848bcf2f4Author: Alice Groux <alice.grx@gmail.com> Date: Thu Jan 7 22:29:17 2021 +0100 feat(k8s/resources-list-view): add advanced deployment panel to resources list view (#4516) * feat(k8s/resources-list-view): add advanced deployment panel to applications view, configurations view and volumes view * feat(k8s/resources-list-view): move advanced deployment into a template and use it everywhere commitb924347c5bAuthor: Stéphane Busso <stephane.busso@gmail.com> Date: Thu Jan 7 14:03:46 2021 +1300 Bump portainer version commit9fbda9fb99Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com> Date: Thu Jan 7 13:38:01 2021 +1300 Merge in release fixes to develop (#4687) * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618) * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) * fix(frontend) rephrase comments (#4629) Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> Co-authored-by: Simon Meng <simon.meng@portainer.io> Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> * + silently continue when downloading artifacts in windows (#4637) * fix(docker/stack-details): do not display editor tab for external stack (#4650) * Revert "chore(build): bump Kompose version (#4475)" (#4676) This reverts commit380f106571. Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> Co-authored-by: cong meng <mcpacino@gmail.com> Co-authored-by: Simon Meng <simon.meng@portainer.io> Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com> Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io> commit82f8062784Author: Anthony Lapenna <lapenna.anthony@gmail.com> Date: Wed Jan 6 11:31:05 2021 +1300 chore(github): update issue template commit49982eb98aAuthor: knittl <knittl89+github@gmail.com> Date: Tue Jan 5 20:49:50 2021 +0100 commit4be3ac470fMerge:7975ef79a50ab51bAuthor: Stéphane Busso <sbusso@users.noreply.github.com> Date: Thu Dec 24 23:45:53 2020 +1300 Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version Revert "chore(build): bump Kompose version" commita50ab51befAuthor: Stéphane Busso <sbusso@users.noreply.github.com> Date: Thu Dec 24 12:12:28 2020 +1300 Revert "chore(build): bump Kompose version (#4475)" This reverts commit380f106571.
970 lines
36 KiB
JavaScript
970 lines
36 KiB
JavaScript
import angular from 'angular';
|
|
import * as _ from 'lodash-es';
|
|
import filesizeParser from 'filesize-parser';
|
|
import * as JsonPatch from 'fast-json-patch';
|
|
|
|
import {
|
|
KubernetesApplicationDataAccessPolicies,
|
|
KubernetesApplicationDeploymentTypes,
|
|
KubernetesApplicationPublishingTypes,
|
|
KubernetesApplicationQuotaDefaults,
|
|
KubernetesApplicationTypes,
|
|
KubernetesApplicationPlacementTypes,
|
|
} from 'Kubernetes/models/application/models';
|
|
import {
|
|
KubernetesApplicationConfigurationFormValue,
|
|
KubernetesApplicationConfigurationFormValueOverridenKey,
|
|
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
|
|
KubernetesApplicationEnvironmentVariableFormValue,
|
|
KubernetesApplicationFormValues,
|
|
KubernetesApplicationPersistedFolderFormValue,
|
|
KubernetesApplicationPublishedPortFormValue,
|
|
KubernetesApplicationPlacementFormValue,
|
|
KubernetesFormValueDuplicate,
|
|
} from 'Kubernetes/models/application/formValues';
|
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
|
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
|
|
|
class KubernetesCreateApplicationController {
|
|
/* #region CONSTRUCTOR */
|
|
|
|
/* @ngInject */
|
|
constructor(
|
|
$async,
|
|
$state,
|
|
Notifications,
|
|
EndpointProvider,
|
|
Authentication,
|
|
ModalService,
|
|
KubernetesResourcePoolService,
|
|
KubernetesApplicationService,
|
|
KubernetesStackService,
|
|
KubernetesConfigurationService,
|
|
KubernetesNodeService,
|
|
KubernetesIngressService,
|
|
KubernetesPersistentVolumeClaimService,
|
|
KubernetesNamespaceHelper,
|
|
KubernetesVolumeService
|
|
) {
|
|
this.$async = $async;
|
|
this.$state = $state;
|
|
this.Notifications = Notifications;
|
|
this.EndpointProvider = EndpointProvider;
|
|
this.Authentication = Authentication;
|
|
this.ModalService = ModalService;
|
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
|
this.KubernetesStackService = KubernetesStackService;
|
|
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
|
this.KubernetesNodeService = KubernetesNodeService;
|
|
this.KubernetesVolumeService = KubernetesVolumeService;
|
|
this.KubernetesIngressService = KubernetesIngressService;
|
|
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
|
|
|
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
|
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
|
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
|
this.ApplicationPlacementTypes = KubernetesApplicationPlacementTypes;
|
|
this.ApplicationTypes = KubernetesApplicationTypes;
|
|
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
|
this.ServiceTypes = KubernetesServiceTypes;
|
|
|
|
this.onInit = this.onInit.bind(this);
|
|
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
|
|
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
|
|
this.updateSlidersAsync = this.updateSlidersAsync.bind(this);
|
|
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
|
|
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
|
|
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
|
|
this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this);
|
|
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
|
}
|
|
/* #endregion */
|
|
|
|
onChangeName() {
|
|
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
|
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
|
}
|
|
|
|
/* #region AUTO SCLAER UI MANAGEMENT */
|
|
unselectAutoScaler() {
|
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
|
|
this.formValues.AutoScaler.IsUsed = false;
|
|
}
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region CONFIGURATION UI MANAGEMENT */
|
|
addConfiguration() {
|
|
let config = new KubernetesApplicationConfigurationFormValue();
|
|
config.SelectedConfiguration = this.configurations[0];
|
|
this.formValues.Configurations.push(config);
|
|
}
|
|
|
|
removeConfiguration(index) {
|
|
this.formValues.Configurations.splice(index, 1);
|
|
this.onChangeConfigurationPath();
|
|
}
|
|
|
|
overrideConfiguration(index) {
|
|
const config = this.formValues.Configurations[index];
|
|
config.Overriden = true;
|
|
config.OverridenKeys = _.map(_.keys(config.SelectedConfiguration.Data), (key) => {
|
|
const res = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
|
res.Key = key;
|
|
return res;
|
|
});
|
|
}
|
|
|
|
resetConfiguration(index) {
|
|
const config = this.formValues.Configurations[index];
|
|
config.Overriden = false;
|
|
config.OverridenKeys = [];
|
|
this.onChangeConfigurationPath();
|
|
}
|
|
|
|
clearConfigurations() {
|
|
this.formValues.Configurations = [];
|
|
}
|
|
|
|
onChangeConfigurationPath() {
|
|
this.state.duplicates.configurationPaths.refs = [];
|
|
|
|
const paths = _.reduce(
|
|
this.formValues.Configurations,
|
|
(result, config) => {
|
|
const uniqOverridenKeysPath = _.uniq(_.map(config.OverridenKeys, 'Path'));
|
|
return _.concat(result, uniqOverridenKeysPath);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const duplicatePaths = KubernetesFormValidationHelper.getDuplicates(paths);
|
|
|
|
_.forEach(this.formValues.Configurations, (config, index) => {
|
|
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
|
|
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
|
|
if (findPath) {
|
|
this.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] = findPath;
|
|
}
|
|
});
|
|
});
|
|
|
|
this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region ENVIRONMENT UI MANAGEMENT */
|
|
addEnvironmentVariable() {
|
|
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
|
|
}
|
|
|
|
restoreEnvironmentVariable(index) {
|
|
this.formValues.EnvironmentVariables[index].NeedsDeletion = false;
|
|
}
|
|
|
|
removeEnvironmentVariable(index) {
|
|
if (this.state.isEdit && !this.formValues.EnvironmentVariables[index].IsNew) {
|
|
this.formValues.EnvironmentVariables[index].NeedsDeletion = true;
|
|
} else {
|
|
this.formValues.EnvironmentVariables.splice(index, 1);
|
|
}
|
|
this.onChangeEnvironmentName();
|
|
}
|
|
|
|
onChangeEnvironmentName() {
|
|
this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
|
|
this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region PERSISTENT FOLDERS UI MANAGEMENT */
|
|
addPersistedFolder() {
|
|
let storageClass = {};
|
|
if (this.storageClasses.length > 0) {
|
|
storageClass = this.storageClasses[0];
|
|
}
|
|
|
|
this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass));
|
|
this.resetDeploymentType();
|
|
}
|
|
|
|
restorePersistedFolder(index) {
|
|
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
|
}
|
|
|
|
resetPersistedFolders() {
|
|
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
persistedFolder.ExistingVolume = null;
|
|
persistedFolder.UseNewVolume = true;
|
|
});
|
|
}
|
|
|
|
removePersistedFolder(index) {
|
|
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
|
|
this.formValues.PersistedFolders[index].NeedsDeletion = true;
|
|
} else {
|
|
this.formValues.PersistedFolders.splice(index, 1);
|
|
}
|
|
this.onChangePersistedFolderPath();
|
|
this.onChangeExistingVolumeSelection();
|
|
}
|
|
|
|
onChangePersistedFolderPath() {
|
|
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
|
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
if (persistedFolder.NeedsDeletion) {
|
|
return undefined;
|
|
}
|
|
return persistedFolder.ContainerPath;
|
|
})
|
|
);
|
|
this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
|
}
|
|
|
|
onChangeExistingVolumeSelection() {
|
|
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
|
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
|
|
})
|
|
);
|
|
this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
|
|
}
|
|
|
|
useNewVolume(index) {
|
|
this.formValues.PersistedFolders[index].UseNewVolume = true;
|
|
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
|
this.state.PersistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
|
|
}
|
|
|
|
useExistingVolume(index) {
|
|
this.formValues.PersistedFolders[index].UseNewVolume = false;
|
|
this.state.PersistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
|
|
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
|
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
|
this.resetDeploymentType();
|
|
}
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region PLACEMENT UI MANAGEMENT */
|
|
addPlacement() {
|
|
const placement = new KubernetesApplicationPlacementFormValue();
|
|
const label = this.nodesLabels[0];
|
|
placement.Label = label;
|
|
placement.Value = label.Values[0];
|
|
this.formValues.Placements.push(placement);
|
|
this.onChangePlacement();
|
|
}
|
|
|
|
restorePlacement(index) {
|
|
this.formValues.Placements[index].NeedsDeletion = false;
|
|
this.onChangePlacement();
|
|
}
|
|
|
|
removePlacement(index) {
|
|
if (this.state.isEdit && !this.formValues.Placements[index].IsNew) {
|
|
this.formValues.Placements[index].NeedsDeletion = true;
|
|
} else {
|
|
this.formValues.Placements.splice(index, 1);
|
|
}
|
|
this.onChangePlacement();
|
|
}
|
|
|
|
// call all validation functions when a placement is added/removed/restored
|
|
onChangePlacement() {
|
|
this.onChangePlacementLabelValidate();
|
|
}
|
|
|
|
onChangePlacementLabel(index) {
|
|
this.formValues.Placements[index].Value = this.formValues.Placements[index].Label.Values[0];
|
|
this.onChangePlacementLabelValidate();
|
|
}
|
|
|
|
onChangePlacementLabelValidate() {
|
|
const state = this.state.duplicates.placements;
|
|
const source = _.map(this.formValues.Placements, (p) => (p.NeedsDeletion ? undefined : p.Label.Key));
|
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
|
state.refs = duplicates;
|
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
|
}
|
|
|
|
/* #endregion */
|
|
|
|
/* #region PUBLISHED PORTS UI MANAGEMENT */
|
|
addPublishedPort() {
|
|
const p = new KubernetesApplicationPublishedPortFormValue();
|
|
const ingresses = this.filteredIngresses;
|
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
|
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
|
if (this.formValues.PublishedPorts.length) {
|
|
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
|
}
|
|
this.formValues.PublishedPorts.push(p);
|
|
}
|
|
|
|
resetPublishedPorts() {
|
|
const ingresses = this.filteredIngresses;
|
|
_.forEach(this.formValues.PublishedPorts, (p) => {
|
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
|
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
|
});
|
|
}
|
|
|
|
restorePublishedPort(index) {
|
|
this.formValues.PublishedPorts[index].NeedsDeletion = false;
|
|
this.onChangePublishedPorts();
|
|
}
|
|
|
|
removePublishedPort(index) {
|
|
if (this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew) {
|
|
this.formValues.PublishedPorts[index].NeedsDeletion = true;
|
|
} else {
|
|
this.formValues.PublishedPorts.splice(index, 1);
|
|
}
|
|
this.onChangePublishedPorts();
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region PUBLISHED PORTS ON CHANGE VALIDATION */
|
|
onChangePublishedPorts() {
|
|
this.onChangePortMappingContainerPort();
|
|
this.onChangePortMappingNodePort();
|
|
this.onChangePortMappingIngressRoute();
|
|
this.onChangePortMappingLoadBalancer();
|
|
this.onChangePortProtocol();
|
|
}
|
|
|
|
onChangePortMappingContainerPort() {
|
|
const state = this.state.duplicates.publishedPorts.containerPorts;
|
|
if (this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INGRESS) {
|
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol));
|
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
|
state.refs = duplicates;
|
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
|
} else {
|
|
state.refs = {};
|
|
state.hasDuplicates = false;
|
|
}
|
|
}
|
|
|
|
onChangePortMappingNodePort() {
|
|
const state = this.state.duplicates.publishedPorts.nodePorts;
|
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
|
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort));
|
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
|
state.refs = duplicates;
|
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
|
} else {
|
|
state.refs = {};
|
|
state.hasDuplicates = false;
|
|
}
|
|
}
|
|
|
|
onChangePortMappingIngress(index) {
|
|
const publishedPort = this.formValues.PublishedPorts[index];
|
|
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
|
publishedPort.IngressHost = ingress.Host;
|
|
this.onChangePublishedPorts();
|
|
}
|
|
|
|
onChangePortMappingIngressRoute() {
|
|
const state = this.state.duplicates.publishedPorts.ingressRoutes;
|
|
|
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
|
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
|
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
|
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path));
|
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
|
|
_.forEach(newRoutes, (route, idx) => {
|
|
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
|
|
duplicates[idx] = route;
|
|
}
|
|
});
|
|
state.refs = duplicates;
|
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
|
} else {
|
|
state.refs = {};
|
|
state.hasDuplicates = false;
|
|
}
|
|
}
|
|
|
|
onChangePortMappingLoadBalancer() {
|
|
const state = this.state.duplicates.publishedPorts.loadBalancerPorts;
|
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort));
|
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
|
state.refs = duplicates;
|
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
|
} else {
|
|
state.refs = {};
|
|
state.hasDuplicates = false;
|
|
}
|
|
}
|
|
|
|
onChangePortProtocol(index) {
|
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
|
const newPorts = _.filter(this.formValues.PublishedPorts, { IsNew: true });
|
|
_.forEach(newPorts, (port) => {
|
|
port.Protocol = index ? this.formValues.PublishedPorts[index].Protocol : newPorts[0].Protocol;
|
|
});
|
|
this.onChangePortMappingLoadBalancer();
|
|
}
|
|
this.onChangePortMappingContainerPort();
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region STATE VALIDATION FUNCTIONS */
|
|
isValid() {
|
|
return (
|
|
!this.state.alreadyExists &&
|
|
!this.state.duplicates.environmentVariables.hasDuplicates &&
|
|
!this.state.duplicates.persistedFolders.hasDuplicates &&
|
|
!this.state.duplicates.configurationPaths.hasDuplicates &&
|
|
!this.state.duplicates.existingVolumes.hasDuplicates &&
|
|
!this.state.duplicates.publishedPorts.containerPorts.hasDuplicates &&
|
|
!this.state.duplicates.publishedPorts.nodePorts.hasDuplicates &&
|
|
!this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates &&
|
|
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates
|
|
);
|
|
}
|
|
|
|
storageClassAvailable() {
|
|
return this.storageClasses && this.storageClasses.length > 0;
|
|
}
|
|
|
|
hasMultipleStorageClassesAvailable() {
|
|
return this.storageClasses && this.storageClasses.length > 1;
|
|
}
|
|
|
|
resetDeploymentType() {
|
|
this.formValues.DeploymentType = this.ApplicationDeploymentTypes.REPLICATED;
|
|
}
|
|
|
|
// The data access policy panel is not shown when:
|
|
// * There is not persisted folder specified
|
|
showDataAccessPolicySection() {
|
|
return this.formValues.PersistedFolders.length !== 0;
|
|
}
|
|
|
|
// A global deployment is not available when either:
|
|
// * For each persisted folder specified, if one of the storage object only supports the RWO access mode
|
|
// * The data access policy is set to ISOLATED
|
|
supportGlobalDeployment() {
|
|
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
|
const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO']));
|
|
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
|
|
|
if ((hasFolders && hasRWOOnly) || isIsolated) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A StatefulSet is defined by DataAccessPolicy === ISOLATED
|
|
isEditAndStatefulSet() {
|
|
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
|
}
|
|
|
|
// A scalable deployment is available when either:
|
|
// * No persisted folders are specified
|
|
// * The access policy is set to shared and for each persisted folders specified, all the associated
|
|
// storage objects support at least RWX access mode (no RWO only)
|
|
// * The access policy is set to isolated
|
|
supportScalableReplicaDeployment() {
|
|
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
|
const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO']));
|
|
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
|
|
|
if (!hasFolders || isIsolated || (hasFolders && !hasRWOOnly)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO)
|
|
getNonScalableStorage() {
|
|
let storageOptions = [];
|
|
|
|
for (let i = 0; i < this.formValues.PersistedFolders.length; i++) {
|
|
const folder = this.formValues.PersistedFolders[i];
|
|
|
|
if (_.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
|
|
storageOptions.push(folder.StorageClass.Name);
|
|
}
|
|
}
|
|
|
|
return _.uniq(storageOptions).join(', ');
|
|
}
|
|
|
|
resourceQuotaCapacityExceeded() {
|
|
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
|
}
|
|
|
|
resourceReservationsOverflow() {
|
|
const instances = this.formValues.ReplicaCount;
|
|
const cpu = this.formValues.CpuLimit;
|
|
const maxCpu = this.state.sliders.cpu.max;
|
|
const memory = this.formValues.MemoryLimit;
|
|
const maxMemory = this.state.sliders.memory.max;
|
|
|
|
if (cpu * instances > maxCpu) {
|
|
return true;
|
|
}
|
|
|
|
if (memory * instances > maxMemory) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
autoScalerOverflow() {
|
|
const instances = this.formValues.AutoScaler.MaxReplicas;
|
|
const cpu = this.formValues.CpuLimit;
|
|
const maxCpu = this.state.sliders.cpu.max;
|
|
const memory = this.formValues.MemoryLimit;
|
|
const maxMemory = this.state.sliders.memory.max;
|
|
|
|
if (cpu * instances > maxCpu) {
|
|
return true;
|
|
}
|
|
|
|
if (memory * instances > maxMemory) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
publishViaLoadBalancerEnabled() {
|
|
return this.state.useLoadBalancer;
|
|
}
|
|
|
|
publishViaIngressEnabled() {
|
|
return this.filteredIngresses.length;
|
|
}
|
|
|
|
isEditAndNoChangesMade() {
|
|
if (!this.state.isEdit) return false;
|
|
const changes = JsonPatch.compare(this.savedFormValues, this.formValues);
|
|
this.editChanges = _.filter(changes, (change) => !_.includes(change.path, '$$hashKey') && change.path !== '/ApplicationType');
|
|
return !this.editChanges.length;
|
|
}
|
|
|
|
isEditAndExistingPersistedFolder(index) {
|
|
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
|
}
|
|
|
|
isEditAndNotNewPublishedPort(index) {
|
|
return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew;
|
|
}
|
|
|
|
isNotInternalAndHasNoPublishedPorts() {
|
|
const toDelPorts = _.filter(this.formValues.PublishedPorts, { NeedsDeletion: true });
|
|
const toKeepPorts = _.without(this.formValues.PublishedPorts, ...toDelPorts);
|
|
return this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INTERNAL && toKeepPorts.length === 0;
|
|
}
|
|
|
|
isEditAndNotNewPlacement(index) {
|
|
return this.state.isEdit && !this.formValues.Placements[index].IsNew;
|
|
}
|
|
|
|
isNewAndNotFirst(index) {
|
|
return !this.state.isEdit && index !== 0;
|
|
}
|
|
|
|
showPlacementPolicySection() {
|
|
const placements = _.filter(this.formValues.Placements, { NeedsDeletion: false });
|
|
return placements.length !== 0;
|
|
}
|
|
|
|
isNonScalable() {
|
|
const scalable = this.supportScalableReplicaDeployment();
|
|
const global = this.supportGlobalDeployment();
|
|
const replica = this.formValues.ReplicaCount > 1;
|
|
const replicated = this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED;
|
|
const res = (replicated && !scalable && replica) || (!replicated && !global);
|
|
return res;
|
|
}
|
|
|
|
isDeployUpdateButtonDisabled() {
|
|
const overflow = this.resourceReservationsOverflow();
|
|
const autoScalerOverflow = this.autoScalerOverflow();
|
|
const inProgress = this.state.actionInProgress;
|
|
const invalid = !this.isValid();
|
|
const hasNoChanges = this.isEditAndNoChangesMade();
|
|
const nonScalable = this.isNonScalable();
|
|
const notInternalNoPorts = this.isNotInternalAndHasNoPublishedPorts();
|
|
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || notInternalNoPorts;
|
|
}
|
|
|
|
disableLoadBalancerEdit() {
|
|
return (
|
|
this.state.isEdit &&
|
|
this.application.ServiceType === this.ServiceTypes.LOAD_BALANCER &&
|
|
!this.application.LoadBalancerIPAddress &&
|
|
this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER
|
|
);
|
|
}
|
|
|
|
isPublishingTypeEditDisabled() {
|
|
const ports = _.filter(this.formValues.PublishedPorts, { IsNew: false, NeedsDeletion: false });
|
|
return this.state.isEdit && this.formValues.PublishedPorts.length > 0 && ports.length > 0;
|
|
}
|
|
|
|
isEditLBWithPorts() {
|
|
return this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER && _.filter(this.formValues.PublishedPorts, { IsNew: false }).length;
|
|
}
|
|
|
|
isProtocolOptionDisabled(index, protocol) {
|
|
return (
|
|
this.disableLoadBalancerEdit() ||
|
|
(this.isEditAndNotNewPublishedPort(index) && this.formValues.PublishedPorts[index].Protocol !== protocol) ||
|
|
(this.isEditLBWithPorts() && this.formValues.PublishedPorts[index].Protocol !== protocol) ||
|
|
(this.isNewAndNotFirst(index) && this.formValues.PublishedPorts[index].Protocol !== protocol)
|
|
);
|
|
}
|
|
|
|
/* #endregion */
|
|
|
|
/* #region DATA AUTO REFRESH */
|
|
async updateSlidersAsync() {
|
|
try {
|
|
const quota = this.formValues.ResourcePool.Quota;
|
|
let minCpu,
|
|
maxCpu,
|
|
minMemory,
|
|
maxMemory = 0;
|
|
if (quota) {
|
|
this.state.resourcePoolHasQuota = true;
|
|
if (quota.CpuLimit) {
|
|
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
|
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
|
|
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
|
|
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
|
|
}
|
|
} else {
|
|
minCpu = 0;
|
|
maxCpu = this.state.nodes.cpu;
|
|
}
|
|
if (quota.MemoryLimit) {
|
|
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
|
|
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
|
|
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
|
|
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
|
|
}
|
|
} else {
|
|
minMemory = 0;
|
|
maxMemory = this.state.nodes.memory;
|
|
}
|
|
} else {
|
|
this.state.resourcePoolHasQuota = false;
|
|
minCpu = 0;
|
|
maxCpu = this.state.nodes.cpu;
|
|
minMemory = 0;
|
|
maxMemory = this.state.nodes.memory;
|
|
}
|
|
this.state.sliders.memory.min = minMemory;
|
|
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
|
this.state.sliders.cpu.min = minCpu;
|
|
this.state.sliders.cpu.max = _.round(maxCpu, 2);
|
|
if (!this.state.isEdit) {
|
|
this.formValues.CpuLimit = minCpu;
|
|
this.formValues.MemoryLimit = minMemory;
|
|
}
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to update resources selector');
|
|
}
|
|
}
|
|
|
|
updateSliders() {
|
|
return this.$async(this.updateSlidersAsync);
|
|
}
|
|
|
|
async refreshStacksAsync(namespace) {
|
|
try {
|
|
this.stacks = await this.KubernetesStackService.get(namespace);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
|
}
|
|
}
|
|
|
|
refreshStacks(namespace) {
|
|
return this.$async(this.refreshStacksAsync, namespace);
|
|
}
|
|
|
|
async refreshConfigurationsAsync(namespace) {
|
|
try {
|
|
this.configurations = await this.KubernetesConfigurationService.get(namespace);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
|
}
|
|
}
|
|
|
|
refreshConfigurations(namespace) {
|
|
return this.$async(this.refreshConfigurationsAsync, namespace);
|
|
}
|
|
|
|
async refreshApplicationsAsync(namespace) {
|
|
try {
|
|
this.applications = await this.KubernetesApplicationService.get(namespace);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
|
}
|
|
}
|
|
|
|
refreshApplications(namespace) {
|
|
return this.$async(this.refreshApplicationsAsync, namespace);
|
|
}
|
|
|
|
refreshVolumes(namespace) {
|
|
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
|
const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace;
|
|
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
|
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
|
|
return isSameNamespace && (isUnused || isRWX);
|
|
});
|
|
this.availableVolumes = filteredVolumes;
|
|
}
|
|
|
|
refreshIngresses(namespace) {
|
|
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
|
if (!this.publishViaIngressEnabled()) {
|
|
this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
|
}
|
|
this.formValues.OriginalIngresses = this.filteredIngresses;
|
|
}
|
|
|
|
async refreshNamespaceDataAsync(namespace) {
|
|
await Promise.all([
|
|
this.refreshStacks(namespace),
|
|
this.refreshConfigurations(namespace),
|
|
this.refreshApplications(namespace),
|
|
this.refreshIngresses(namespace),
|
|
this.refreshVolumes(namespace),
|
|
]);
|
|
this.onChangeName();
|
|
}
|
|
|
|
refreshNamespaceData(namespace) {
|
|
return this.$async(this.refreshNamespaceDataAsync, namespace);
|
|
}
|
|
|
|
resetFormValues() {
|
|
this.clearConfigurations();
|
|
this.resetPersistedFolders();
|
|
this.resetPublishedPorts();
|
|
}
|
|
|
|
onResourcePoolSelectionChange() {
|
|
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
|
this.updateSliders();
|
|
this.refreshNamespaceData(namespace);
|
|
this.resetFormValues();
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region ACTIONS */
|
|
async deployApplicationAsync() {
|
|
this.state.actionInProgress = true;
|
|
try {
|
|
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
|
|
_.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined);
|
|
await this.KubernetesApplicationService.create(this.formValues);
|
|
this.Notifications.success('Application successfully deployed', this.formValues.Name);
|
|
this.$state.go('kubernetes.applications');
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to create application');
|
|
} finally {
|
|
this.state.actionInProgress = false;
|
|
}
|
|
}
|
|
|
|
async updateApplicationAsync() {
|
|
try {
|
|
this.state.actionInProgress = true;
|
|
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues);
|
|
this.Notifications.success('Application successfully updated');
|
|
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to update application');
|
|
} finally {
|
|
this.state.actionInProgress = false;
|
|
}
|
|
}
|
|
|
|
deployApplication() {
|
|
if (this.state.isEdit) {
|
|
this.ModalService.confirmUpdate('Updating the application may cause a service interruption. Do you wish to continue?', (confirmed) => {
|
|
if (confirmed) {
|
|
return this.$async(this.updateApplicationAsync);
|
|
}
|
|
});
|
|
} else {
|
|
return this.$async(this.deployApplicationAsync);
|
|
}
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region APPLICATION - used on edit context only */
|
|
async getApplicationAsync() {
|
|
try {
|
|
const namespace = this.state.params.namespace;
|
|
[this.application, this.persistentVolumeClaims] = await Promise.all([
|
|
this.KubernetesApplicationService.get(namespace, this.state.params.name),
|
|
this.KubernetesPersistentVolumeClaimService.get(namespace),
|
|
]);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
|
}
|
|
}
|
|
|
|
getApplication() {
|
|
return this.$async(this.getApplicationAsync);
|
|
}
|
|
/* #endregion */
|
|
|
|
/* #region ON INIT */
|
|
async onInit() {
|
|
try {
|
|
this.state = {
|
|
actionInProgress: false,
|
|
useLoadBalancer: false,
|
|
useServerMetrics: false,
|
|
sliders: {
|
|
cpu: {
|
|
min: 0,
|
|
max: 0,
|
|
},
|
|
memory: {
|
|
min: 0,
|
|
max: 0,
|
|
},
|
|
},
|
|
nodes: {
|
|
memory: 0,
|
|
cpu: 0,
|
|
},
|
|
resourcePoolHasQuota: false,
|
|
viewReady: false,
|
|
availableSizeUnits: ['MB', 'GB', 'TB'],
|
|
alreadyExists: false,
|
|
duplicates: {
|
|
environmentVariables: new KubernetesFormValueDuplicate(),
|
|
persistedFolders: new KubernetesFormValueDuplicate(),
|
|
configurationPaths: new KubernetesFormValueDuplicate(),
|
|
existingVolumes: new KubernetesFormValueDuplicate(),
|
|
publishedPorts: {
|
|
containerPorts: new KubernetesFormValueDuplicate(),
|
|
nodePorts: new KubernetesFormValueDuplicate(),
|
|
ingressRoutes: new KubernetesFormValueDuplicate(),
|
|
loadBalancerPorts: new KubernetesFormValueDuplicate(),
|
|
},
|
|
placements: new KubernetesFormValueDuplicate(),
|
|
},
|
|
isEdit: false,
|
|
params: {
|
|
namespace: this.$transition$.params().namespace,
|
|
name: this.$transition$.params().name,
|
|
},
|
|
PersistedFoldersUseExistingVolumes: false,
|
|
};
|
|
|
|
this.isAdmin = this.Authentication.isAdmin();
|
|
|
|
this.editChanges = [];
|
|
|
|
if (this.$transition$.params().namespace && this.$transition$.params().name) {
|
|
this.state.isEdit = true;
|
|
}
|
|
|
|
const endpoint = this.EndpointProvider.currentEndpoint();
|
|
this.endpoint = endpoint;
|
|
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
|
|
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
|
this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
|
|
|
|
this.formValues = new KubernetesApplicationFormValues();
|
|
|
|
const [resourcePools, nodes, ingresses] = await Promise.all([
|
|
this.KubernetesResourcePoolService.get(),
|
|
this.KubernetesNodeService.get(),
|
|
this.KubernetesIngressService.get(),
|
|
]);
|
|
this.ingresses = ingresses;
|
|
|
|
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
|
this.formValues.ResourcePool = this.resourcePools[0];
|
|
|
|
// TODO: refactor @Max
|
|
// Don't pull all volumes and applications across all namespaces
|
|
// Use refreshNamespaceData flow (triggered on Init + on Namespace change)
|
|
// and query only accross the selected namespace
|
|
if (this.storageClassAvailable()) {
|
|
const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]);
|
|
this.volumes = volumes;
|
|
_.forEach(this.volumes, (volume) => {
|
|
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
|
});
|
|
}
|
|
|
|
_.forEach(nodes, (item) => {
|
|
this.state.nodes.memory += filesizeParser(item.Memory);
|
|
this.state.nodes.cpu += item.CPU;
|
|
});
|
|
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
|
|
|
|
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
|
await this.refreshNamespaceData(namespace);
|
|
|
|
if (this.state.isEdit) {
|
|
await this.getApplication();
|
|
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
|
|
this.application,
|
|
this.resourcePools,
|
|
this.configurations,
|
|
this.persistentVolumeClaims,
|
|
this.nodesLabels
|
|
);
|
|
this.formValues.OriginalIngresses = this.filteredIngresses;
|
|
this.savedFormValues = angular.copy(this.formValues);
|
|
delete this.formValues.ApplicationType;
|
|
|
|
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
|
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
const volume = _.find(this.availableVolumes, (vol) => vol.PersistentVolumeClaim.Name === persistedFolder.PersistentVolumeClaimName);
|
|
if (volume) {
|
|
persistedFolder.UseNewVolume = false;
|
|
persistedFolder.ExistingVolume = volume;
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
|
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
|
|
}
|
|
|
|
await this.updateSliders();
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
|
} finally {
|
|
this.state.viewReady = true;
|
|
}
|
|
}
|
|
|
|
$onInit() {
|
|
return this.$async(this.onInit);
|
|
}
|
|
/* #endregion */
|
|
}
|
|
|
|
export default KubernetesCreateApplicationController;
|
|
angular.module('portainer.kubernetes').controller('KubernetesCreateApplicationController', KubernetesCreateApplicationController);
|