From 7840e0bfe16f8b2ea4b94e895e94c9d6e19e3d66 Mon Sep 17 00:00:00 2001 From: Prabhat Khera <91852476+prabhat-org@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:08:06 +1300 Subject: [PATCH] feature(kubernetes): stack name made optional & add toggle to disable stack in kubernetes [EE-6170] (#10436) --- .../test_data/output_24_to_latest.json | 3 + api/http/handler/settings/settings_public.go | 3 + api/http/handler/settings/settings_update.go | 10 ++- .../handler/stacks/create_kubernetes_stack.go | 10 +-- .../handler/stacks/update_kubernetes_stack.go | 13 +++ api/portainer.go | 6 ++ .../applicationsDatatable.html | 5 +- .../applicationsDatatable.js | 1 + .../helm-templates.controller.js | 5 +- app/kubernetes/converters/daemonSet.js | 4 +- app/kubernetes/converters/deployment.js | 4 +- app/kubernetes/converters/statefulSet.js | 4 +- app/kubernetes/react/components/index.ts | 9 ++ app/kubernetes/services/applicationService.js | 10 ++- .../views/applications/applications.html | 1 + .../applications/applicationsController.js | 3 + .../create/createApplication.html | 54 +++++++++++- .../create/createApplicationController.js | 12 ++- app/kubernetes/views/deploy/deploy.html | 15 +++- .../views/deploy/deployController.js | 10 ++- app/portainer/services/api/stackService.js | 3 +- .../DeployView/StackName/StackName.tsx | 84 +++++++++++++++++++ .../DetailsView/ApplicationSummaryWidget.tsx | 25 ++++-- .../environments/environment.service/index.ts | 15 ++++ .../DeploymentOptionsSection.tsx | 19 +++++ .../KubeSettingsPanel/KubeSettingsPanel.tsx | 18 ++-- .../SettingsView/KubeSettingsPanel/types.ts | 1 + .../KubeSettingsPanel/validation.ts | 1 + app/react/portainer/settings/types.ts | 4 +- 29 files changed, 305 insertions(+), 47 deletions(-) create mode 100644 app/react/kubernetes/DeployView/StackName/StackName.tsx diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index 8ada67237..d10397ad5 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -602,6 +602,9 @@ "EnableTelemetry": true, "EnforceEdgeID": false, "FeatureFlagSettings": null, + "GlobalDeploymentOptions": { + "hideStacksFunctionality": false + }, "HelmRepositoryURL": "https://charts.bitnami.com/bitnami", "InternalAuthSettings": { "RequiredPasswordLength": 12 diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 6301f8c63..fd45748b2 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -17,6 +17,8 @@ type publicSettingsResponse struct { AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"` // The minimum required length for a password of any user when using internal auth mode RequiredPasswordLength int `json:"RequiredPasswordLength" example:"1"` + // Deployment options for encouraging deployment as code + GlobalDeploymentOptions portainer.GlobalDeploymentOptions `json:"GlobalDeploymentOptions"` // Show the Kompose build option (discontinued in 2.18) ShowKomposeBuildOption bool `json:"ShowKomposeBuildOption" example:"false"` // Whether edge compute features are enabled @@ -78,6 +80,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp AuthenticationMethod: appSettings.AuthenticationMethod, RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength, EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures, + GlobalDeploymentOptions: appSettings.GlobalDeploymentOptions, ShowKomposeBuildOption: appSettings.ShowKomposeBuildOption, EnableTelemetry: appSettings.EnableTelemetry, KubeconfigExpiry: appSettings.KubeconfigExpiry, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 51f1f8e09..574050c4d 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -32,8 +32,9 @@ type settingsUpdatePayload struct { SnapshotInterval *string `example:"5m"` // URL to the templates that will be displayed in the UI when navigating to App Templates TemplatesURL *string `example:"https://raw.githubusercontent.com/portainer/templates/master/templates.json"` - // The default check in interval for edge agent (in seconds) - EdgeAgentCheckinInterval *int `example:"5"` + // Deployment options for encouraging deployment as code + GlobalDeploymentOptions *portainer.GlobalDeploymentOptions // The default check in interval for edge agent (in seconds) + EdgeAgentCheckinInterval *int `example:"5"` // Show the Kompose build option (discontinued in 2.18) ShowKomposeBuildOption *bool `json:"ShowKomposeBuildOption" example:"false"` // Whether edge compute features are enabled @@ -159,6 +160,11 @@ func (handler *Handler) updateSettings(tx dataservices.DataStoreTx, payload sett settings.TemplatesURL = *payload.TemplatesURL } + // update the global deployment options, and the environment deployment options if they have changed + if payload.GlobalDeploymentOptions != nil { + settings.GlobalDeploymentOptions = *payload.GlobalDeploymentOptions + } + if payload.ShowKomposeBuildOption != nil { settings.ShowKomposeBuildOption = *payload.ShowKomposeBuildOption } diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 979bff0ce..201ae3e68 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -94,9 +94,7 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") } - if govalidator.IsNull(payload.StackName) { - return errors.New("Invalid stack name") - } + return nil } @@ -113,9 +111,6 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error { if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil { return err } - if govalidator.IsNull(payload.StackName) { - return errors.New("Invalid stack name") - } return nil } @@ -123,9 +118,6 @@ func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) { return errors.New("Invalid manifest URL") } - if govalidator.IsNull(payload.StackName) { - return errors.New("Invalid stack name") - } return nil } diff --git a/api/http/handler/stacks/update_kubernetes_stack.go b/api/http/handler/stacks/update_kubernetes_stack.go index f167ebc1e..5920144e0 100644 --- a/api/http/handler/stacks/update_kubernetes_stack.go +++ b/api/http/handler/stacks/update_kubernetes_stack.go @@ -24,6 +24,8 @@ import ( type kubernetesFileStackUpdatePayload struct { StackFileContent string + // Name of the stack + StackName string } type kubernetesGitStackUpdatePayload struct { @@ -39,6 +41,9 @@ func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") } + if govalidator.IsNull(payload.StackName) { + return errors.New("Invalid stack name") + } return nil } @@ -114,6 +119,14 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer. return httperror.InternalServerError("Failed to persist deployment file in a temp directory", err) } + if payload.StackName != stack.Name { + stack.Name = payload.StackName + err := handler.DataStore.Stack().Update(stack.ID, stack) + if err != nil { + return httperror.InternalServerError("Failed to update stack name", err) + } + } + // Refresh ECR registry secret if needed // RefreshEcrSecret method checks if the namespace has any ECR registry // otherwise return nil diff --git a/api/portainer.go b/api/portainer.go index 873bde1c3..f0bf53aae 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -933,6 +933,10 @@ type ( RetryInterval int } + GlobalDeploymentOptions struct { + HideStacksFunctionality bool `json:"hideStacksFunctionality" example:"false"` + } + // Settings represents the application settings Settings struct { // URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string @@ -951,6 +955,8 @@ type ( SnapshotInterval string `json:"SnapshotInterval" example:"5m"` // URL to the templates that will be displayed in the UI when navigating to App Templates TemplatesURL string `json:"TemplatesURL" example:"https://raw.githubusercontent.com/portainer/templates/master/templates.json"` + // Deployment options for encouraging git ops workflows + GlobalDeploymentOptions GlobalDeploymentOptions `json:"GlobalDeploymentOptions"` // The default check in interval for edge agent (in seconds) EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"` // Show the Kompose build option (discontinued in 2.18) diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index 13bec8dc9..6bbe34867 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -172,7 +172,7 @@ ng-click="$ctrl.changeOrderBy('Name')" > - + system external - {{ item.StackName || '-' }} + {{ item.StackName || '-' }} {{ item.ResourcePool }} @@ -330,6 +330,7 @@ refresh-callback="$ctrl.refreshCallback" on-publishing-mode-click="($ctrl.onPublishingModeClick)" is-primary="false" + hide-stacks-functionality="$ctrl.hideStacksFunctionality" > diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js index 89a308752..28f63b8c8 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js @@ -21,5 +21,6 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl isAppsLoading: '<', isSystemResources: '<', setSystemResources: '<', + hideStacksFunctionality: '<', }, }); diff --git a/app/kubernetes/components/helm/helm-templates/helm-templates.controller.js b/app/kubernetes/components/helm/helm-templates/helm-templates.controller.js index 674652c3a..fa46521d8 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-templates.controller.js +++ b/app/kubernetes/components/helm/helm-templates/helm-templates.controller.js @@ -185,8 +185,9 @@ export default class HelmTemplatesController { }; const helmRepos = await this.getHelmRepoURLs(); - await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]); - + if (helmRepos) { + await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]); + } if (this.state.charts.length > 0 && this.$state.params.chartName) { const chart = this.state.charts.find((chart) => chart.name === this.$state.params.chartName); if (chart) { diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js index 7724e5158..93a934231 100644 --- a/app/kubernetes/converters/daemonSet.js +++ b/app/kubernetes/converters/daemonSet.js @@ -42,7 +42,9 @@ class KubernetesDaemonSetConverter { const payload = new KubernetesDaemonSetCreatePayload(); payload.metadata.name = daemonSet.Name; payload.metadata.namespace = daemonSet.Namespace; - payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName; + if (daemonSet.StackName) { + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName; + } payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner; payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note; diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js index 1733878df..d7dfa6d0d 100644 --- a/app/kubernetes/converters/deployment.js +++ b/app/kubernetes/converters/deployment.js @@ -45,7 +45,9 @@ class KubernetesDeploymentConverter { const payload = new KubernetesDeploymentCreatePayload(); payload.metadata.name = deployment.Name; payload.metadata.namespace = deployment.Namespace; - payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName; + if (deployment.StackName) { + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName; + } payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner; payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note; diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js index d405770e3..8b3b965bd 100644 --- a/app/kubernetes/converters/statefulSet.js +++ b/app/kubernetes/converters/statefulSet.js @@ -46,7 +46,9 @@ class KubernetesStatefulSetConverter { const payload = new KubernetesStatefulSetCreatePayload(); payload.metadata.name = statefulSet.Name; payload.metadata.namespace = statefulSet.Namespace; - payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName; + if (statefulSet.StackName) { + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName; + } payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner; payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note; diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index d5609c2e6..2794437ff 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -22,6 +22,7 @@ import { withFormValidation } from '@/react-tools/withFormValidation'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector'; import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable'; +import { StackName } from '@/react/kubernetes/DeployView/StackName/StackName'; export const ngModule = angular .module('portainer.kubernetes.react.components', []) @@ -101,6 +102,14 @@ export const ngModule = angular 'hideMessage', ]) ) + .component( + 'kubeStackName', + r2a(withUIRouter(withReactQuery(withCurrentUser(StackName))), [ + 'setStackName', + 'isAdmin', + 'stackName', + ]) + ) .component( 'applicationSummaryWidget', r2a( diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 79a306c32..9b804f986 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -210,10 +210,14 @@ class KubernetesApplicationService { * To synchronise with kubernetes resource creation summary output, any new resources created in this method should * also be displayed in the summary output (getCreatedApplicationResources) */ - async createAsync(formValues) { + async createAsync(formValues, hideStacks) { // formValues -> Application let [app, headlessService, services, , claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + if (hideStacks) { + app.StackName = ''; + } + if (services) { services.forEach(async (service) => { try { @@ -264,8 +268,8 @@ class KubernetesApplicationService { await apiService.create(app); } - create(formValues) { - return this.$async(this.createAsync, formValues); + create(formValues, _, hideStacks) { + return this.$async(this.createAsync, formValues, hideStacks); } /* #endregion */ diff --git a/app/kubernetes/views/applications/applications.html b/app/kubernetes/views/applications/applications.html index 67805ed38..a08c779d0 100644 --- a/app/kubernetes/views/applications/applications.html +++ b/app/kubernetes/views/applications/applications.html @@ -25,6 +25,7 @@ is-apps-loading="ctrl.state.isAppsLoading" is-system-resources="ctrl.state.isSystemResources" set-system-resources="(ctrl.setSystemResources)" + hide-stacks-functionality="ctrl.deploymentOptions.hideStacksFunctionality" > diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 9010118a3..1d7dd1253 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -6,6 +6,7 @@ import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelpe import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models'; import { confirmDelete } from '@@/modals/confirm'; +import { getDeploymentOptions } from '@/react/portainer/environments/environment.service'; class KubernetesApplicationsController { /* @ngInject */ @@ -196,6 +197,8 @@ class KubernetesApplicationsController { isSystemResources: undefined, }; + this.deploymentOptions = await getDeploymentOptions(); + this.user = this.Authentication.getUserDetails(); this.state.namespaces = await this.KubernetesNamespaceService.get(); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 5593981f6..8d69b08f2 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -99,6 +99,54 @@ + +
+
+ + Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use + the application name. +
+
+ +
+ +
+ +
+
+ + -
+
Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to @@ -226,7 +274,7 @@
-
+
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 2adcfcd79..06902d791 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -5,6 +5,7 @@ import * as JsonPatch from 'fast-json-patch'; import { RegistryTypes } from '@/portainer/models/registryTypes'; import { getServices } from '@/react/kubernetes/networks/services/service'; import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models'; +import { getGlobalDeploymentOptions } from '@/react/portainer/settings/settings.service'; import { KubernetesApplicationDataAccessPolicies, @@ -196,7 +197,10 @@ class KubernetesCreateApplicationController { } this.state.updateWebEditorInProgress = true; - await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, { stackFile: this.stackFileContent }); + await this.StackService.updateKubeStack( + { EndpointId: this.endpoint.Id, Id: this.application.StackId }, + { stackFile: this.stackFileContent, stackName: this.formValues.StackName } + ); this.state.isEditorDirty = false; await this.$state.reload(this.$state.current); } catch (err) { @@ -932,7 +936,7 @@ class KubernetesCreateApplicationController { this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username; // combine the secrets and configmap form values when submitting the form _.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined); - await this.KubernetesApplicationService.create(this.formValues, this.originalServicePorts); + await this.KubernetesApplicationService.create(this.formValues, this.originalServicePorts, this.deploymentOptions.hideStacksFunctionality); this.Notifications.success('Request to deploy application successfully submitted', this.formValues.Name); this.$state.go('kubernetes.applications'); } catch (err) { @@ -1092,6 +1096,8 @@ class KubernetesCreateApplicationController { this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; + this.deploymentOptions = await getGlobalDeploymentOptions(); + const [resourcePools, nodes, nodesLimits] = await Promise.all([ this.KubernetesResourcePoolService.get(), this.KubernetesNodeService.get(), @@ -1140,6 +1146,8 @@ class KubernetesCreateApplicationController { this.nodesLabels, this.ingresses ); + + this.formValues.Services = this.formValues.Services || []; this.originalServicePorts = structuredClone(this.formValues.Services.flatMap((service) => service.Ports)); this.originalIngressPaths = structuredClone(this.originalServicePorts.flatMap((port) => port.ingressPaths).filter((ingressPath) => ingressPath.Host)); diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index 3a7a81cae..6927680e8 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -32,11 +32,13 @@
+ Namespaces specified in the manifest will be used
@@ -48,12 +50,17 @@
- -
- -
+ +
Resource names specified in the manifest will be used
+ +
Deploy from
void; + isAdmin?: boolean; +}; + +export function StackName({ stackName, setStackName, isAdmin = false }: Props) { + const tooltip = ( + <> + You may specify a stack name to label resources that you want to group. + This includes Deployments, DaemonSets, StatefulSets and Pods. + {isAdmin && ( + <> +
+ You can leave the stack name empty, or even turn off Kubernetes Stacks + functionality entirely via{' '} + + Kubernetes Settings + + . + + )} + + ); + + const insightsBoxContent = ( + <> + The stack field below was previously labelled 'Name' but, in + fact, it's always been the stack name (hence the relabelling). + {isAdmin && ( + <> +
+ Kubernetes Stacks functionality can be turned off entirely via{' '} + + Kubernetes Settings + + . + + )} + + ); + + return ( + <> +
+ +
+ + + Enter or select a 'stack' name to group multiple deployments + together, or else leave empty to ignore. + +
+ +
+ setStackName(e.target.value)} + id="stack_name" + placeholder="myStack" + /> +
+
+ + ); +} diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx index 608554b2e..1544ff579 100644 --- a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx +++ b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx @@ -6,6 +6,8 @@ import { useCurrentStateAndParams } from '@uirouter/react'; import { Authorized } from '@/react/hooks/useUser'; import { notifyError, notifySuccess } from '@/portainer/services/notifications'; +import { usePublicSettings } from '@/react/portainer/settings/queries'; +import { GlobalDeploymentOptions } from '@/react/portainer/settings/types'; import { DetailsTable } from '@@/DetailsTable'; import { Badge } from '@@/Badge'; @@ -69,6 +71,11 @@ export function ApplicationSummaryWidget() { setApplicationNoteFormValues(applicationNote || ''); }, [applicationNote]); + const globalDeploymentOptionsQuery = + usePublicSettings({ + select: (settings) => settings.GlobalDeploymentOptions, + }); + const failedCreateCondition = application?.status?.conditions?.find( (condition) => condition.reason === 'FailedCreate' ); @@ -117,13 +124,17 @@ export function ApplicationSummaryWidget() {
- - Stack - - {application?.metadata?.labels?.[appStackNameLabel] || - '-'} - - + {globalDeploymentOptionsQuery.data && + !globalDeploymentOptionsQuery.data + .hideStacksFunctionality && ( + + Stack + + {application?.metadata?.labels?.[appStackNameLabel] || + '-'} + + + )} Namespace diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts index 2fa1bc2bd..35b7f623c 100644 --- a/app/react/portainer/environments/environment.service/index.ts +++ b/app/react/portainer/environments/environment.service/index.ts @@ -8,6 +8,7 @@ import { StatusType as EdgeStackStatusType, } from '@/react/edge/edge-stacks/types'; +import { getPublicSettings } from '../../settings/settings.service'; import type { Environment, EnvironmentId, @@ -131,6 +132,20 @@ export async function snapshotEndpoints() { } } +export async function getDeploymentOptions(environmentId: EnvironmentId) { + const publicSettings = await getPublicSettings(); + const endpoint = await getEndpoint(environmentId); + + if ( + publicSettings.GlobalDeploymentOptions.perEnvOverride && + endpoint.DeploymentOptions?.overrideGlobalOptions + ) { + return endpoint.DeploymentOptions; + } + + return publicSettings.GlobalDeploymentOptions; +} + export async function snapshotEndpoint(id: EnvironmentId) { try { await axios.post(buildUrl(id, 'snapshot')); diff --git a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/DeploymentOptionsSection.tsx b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/DeploymentOptionsSection.tsx index ae03283ee..4a63214a1 100644 --- a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/DeploymentOptionsSection.tsx +++ b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/DeploymentOptionsSection.tsx @@ -14,6 +14,7 @@ export function DeploymentOptionsSection() { values: { globalDeploymentOptions: values }, setFieldValue, } = useFormikContext(); + const limitedFeature = isLimitedToBE(FeatureId.ENFORCE_DEPLOYMENT_OPTIONS); return ( @@ -74,6 +75,24 @@ export function DeploymentOptionsSection() { )} + +
+
+ + setFieldValue( + 'globalDeploymentOptions.hideStacksFunctionality', + !value + ) + } + name="toggle_stacksFunctionality" + labelClass="col-sm-3 col-lg-2" + tooltip="This allows you to group your applications/workloads into a single ‘stack’, and then view or delete an entire stack. If disabled, stacks functionality will not show in the UI." + /> +
+
); diff --git a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/KubeSettingsPanel.tsx b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/KubeSettingsPanel.tsx index 931763198..b51e2b190 100644 --- a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/KubeSettingsPanel.tsx +++ b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/KubeSettingsPanel.tsx @@ -29,13 +29,17 @@ export function KubeSettingsPanel() { const initialValues: FormValues = { helmRepositoryUrl: settingsQuery.data.HelmRepositoryURL || '', kubeconfigExpiry: settingsQuery.data.KubeconfigExpiry || '0', - globalDeploymentOptions: settingsQuery.data.GlobalDeploymentOptions || { - requireNoteOnApplications: false, - minApplicationNoteLength: 0, - hideAddWithForm: false, - hideFileUpload: false, - hideWebEditor: false, - perEnvOverride: false, + globalDeploymentOptions: { + ...{ + requireNoteOnApplications: false, + minApplicationNoteLength: 0, + hideAddWithForm: false, + hideFileUpload: false, + hideWebEditor: false, + perEnvOverride: false, + hideStacksFunctionality: false, + }, + ...settingsQuery.data.GlobalDeploymentOptions, }, }; diff --git a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/types.ts b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/types.ts index 1fc4b6710..77fa97e31 100644 --- a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/types.ts +++ b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/types.ts @@ -8,5 +8,6 @@ export interface FormValues { hideFileUpload: boolean; requireNoteOnApplications: boolean; minApplicationNoteLength: number; + hideStacksFunctionality: boolean; }; } diff --git a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/validation.ts b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/validation.ts index d9a6717cb..7cd57d683 100644 --- a/app/react/portainer/settings/SettingsView/KubeSettingsPanel/validation.ts +++ b/app/react/portainer/settings/SettingsView/KubeSettingsPanel/validation.ts @@ -16,6 +16,7 @@ export function validation(): SchemaOf { hideWebEditor: boolean().required(), hideFileUpload: boolean().required(), requireNoteOnApplications: boolean().required(), + hideStacksFunctionality: boolean().required(), minApplicationNoteLength: number() .typeError('Must be a number') .default(0) diff --git a/app/react/portainer/settings/types.ts b/app/react/portainer/settings/types.ts index 8193c570f..f2523259c 100644 --- a/app/react/portainer/settings/types.ts +++ b/app/react/portainer/settings/types.ts @@ -145,7 +145,7 @@ export interface Settings { }; } -interface GlobalDeploymentOptions { +export interface GlobalDeploymentOptions { /** Hide manual deploy forms in portainer */ hideAddWithForm: boolean; /** Configure this per environment or globally */ @@ -157,6 +157,8 @@ interface GlobalDeploymentOptions { /** Make note on application add/edit screen required */ requireNoteOnApplications: boolean; minApplicationNoteLength: number; + + hideStacksFunctionality: boolean; } export interface PublicSettingsResponse {