mirror of https://github.com/portainer/portainer
feature(kubernetes): stack name made optional & add toggle to disable stack in kubernetes [EE-6170] (#10436)
parent
44d66cc633
commit
7840e0bfe1
|
@ -602,6 +602,9 @@
|
||||||
"EnableTelemetry": true,
|
"EnableTelemetry": true,
|
||||||
"EnforceEdgeID": false,
|
"EnforceEdgeID": false,
|
||||||
"FeatureFlagSettings": null,
|
"FeatureFlagSettings": null,
|
||||||
|
"GlobalDeploymentOptions": {
|
||||||
|
"hideStacksFunctionality": false
|
||||||
|
},
|
||||||
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
||||||
"InternalAuthSettings": {
|
"InternalAuthSettings": {
|
||||||
"RequiredPasswordLength": 12
|
"RequiredPasswordLength": 12
|
||||||
|
|
|
@ -17,6 +17,8 @@ type publicSettingsResponse struct {
|
||||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
|
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
|
||||||
// The minimum required length for a password of any user when using internal auth mode
|
// The minimum required length for a password of any user when using internal auth mode
|
||||||
RequiredPasswordLength int `json:"RequiredPasswordLength" example:"1"`
|
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)
|
// Show the Kompose build option (discontinued in 2.18)
|
||||||
ShowKomposeBuildOption bool `json:"ShowKomposeBuildOption" example:"false"`
|
ShowKomposeBuildOption bool `json:"ShowKomposeBuildOption" example:"false"`
|
||||||
// Whether edge compute features are enabled
|
// Whether edge compute features are enabled
|
||||||
|
@ -78,6 +80,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||||
AuthenticationMethod: appSettings.AuthenticationMethod,
|
AuthenticationMethod: appSettings.AuthenticationMethod,
|
||||||
RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength,
|
RequiredPasswordLength: appSettings.InternalAuthSettings.RequiredPasswordLength,
|
||||||
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
|
||||||
|
GlobalDeploymentOptions: appSettings.GlobalDeploymentOptions,
|
||||||
ShowKomposeBuildOption: appSettings.ShowKomposeBuildOption,
|
ShowKomposeBuildOption: appSettings.ShowKomposeBuildOption,
|
||||||
EnableTelemetry: appSettings.EnableTelemetry,
|
EnableTelemetry: appSettings.EnableTelemetry,
|
||||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||||
|
|
|
@ -32,7 +32,8 @@ type settingsUpdatePayload struct {
|
||||||
SnapshotInterval *string `example:"5m"`
|
SnapshotInterval *string `example:"5m"`
|
||||||
// URL to the templates that will be displayed in the UI when navigating to App Templates
|
// 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"`
|
TemplatesURL *string `example:"https://raw.githubusercontent.com/portainer/templates/master/templates.json"`
|
||||||
// The default check in interval for edge agent (in seconds)
|
// Deployment options for encouraging deployment as code
|
||||||
|
GlobalDeploymentOptions *portainer.GlobalDeploymentOptions // The default check in interval for edge agent (in seconds)
|
||||||
EdgeAgentCheckinInterval *int `example:"5"`
|
EdgeAgentCheckinInterval *int `example:"5"`
|
||||||
// Show the Kompose build option (discontinued in 2.18)
|
// Show the Kompose build option (discontinued in 2.18)
|
||||||
ShowKomposeBuildOption *bool `json:"ShowKomposeBuildOption" example:"false"`
|
ShowKomposeBuildOption *bool `json:"ShowKomposeBuildOption" example:"false"`
|
||||||
|
@ -159,6 +160,11 @@ func (handler *Handler) updateSettings(tx dataservices.DataStoreTx, payload sett
|
||||||
settings.TemplatesURL = *payload.TemplatesURL
|
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 {
|
if payload.ShowKomposeBuildOption != nil {
|
||||||
settings.ShowKomposeBuildOption = *payload.ShowKomposeBuildOption
|
settings.ShowKomposeBuildOption = *payload.ShowKomposeBuildOption
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,9 +94,7 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
|
||||||
if govalidator.IsNull(payload.StackFileContent) {
|
if govalidator.IsNull(payload.StackFileContent) {
|
||||||
return errors.New("Invalid stack file content")
|
return errors.New("Invalid stack file content")
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.StackName) {
|
|
||||||
return errors.New("Invalid stack name")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +111,6 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
|
||||||
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
if err := update.ValidateAutoUpdateSettings(payload.AutoUpdate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.StackName) {
|
|
||||||
return errors.New("Invalid stack name")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,9 +118,6 @@ func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request)
|
||||||
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
|
||||||
return errors.New("Invalid manifest URL")
|
return errors.New("Invalid manifest URL")
|
||||||
}
|
}
|
||||||
if govalidator.IsNull(payload.StackName) {
|
|
||||||
return errors.New("Invalid stack name")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import (
|
||||||
|
|
||||||
type kubernetesFileStackUpdatePayload struct {
|
type kubernetesFileStackUpdatePayload struct {
|
||||||
StackFileContent string
|
StackFileContent string
|
||||||
|
// Name of the stack
|
||||||
|
StackName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type kubernetesGitStackUpdatePayload struct {
|
type kubernetesGitStackUpdatePayload struct {
|
||||||
|
@ -39,6 +41,9 @@ func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error
|
||||||
if govalidator.IsNull(payload.StackFileContent) {
|
if govalidator.IsNull(payload.StackFileContent) {
|
||||||
return errors.New("Invalid stack file content")
|
return errors.New("Invalid stack file content")
|
||||||
}
|
}
|
||||||
|
if govalidator.IsNull(payload.StackName) {
|
||||||
|
return errors.New("Invalid stack name")
|
||||||
|
}
|
||||||
return nil
|
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)
|
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
|
// Refresh ECR registry secret if needed
|
||||||
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
// RefreshEcrSecret method checks if the namespace has any ECR registry
|
||||||
// otherwise return nil
|
// otherwise return nil
|
||||||
|
|
|
@ -933,6 +933,10 @@ type (
|
||||||
RetryInterval int
|
RetryInterval int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalDeploymentOptions struct {
|
||||||
|
HideStacksFunctionality bool `json:"hideStacksFunctionality" example:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
// Settings represents the application settings
|
// Settings represents the application settings
|
||||||
Settings struct {
|
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
|
// 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"`
|
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
||||||
// URL to the templates that will be displayed in the UI when navigating to App Templates
|
// 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"`
|
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)
|
// The default check in interval for edge agent (in seconds)
|
||||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"`
|
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval" example:"5"`
|
||||||
// Show the Kompose build option (discontinued in 2.18)
|
// Show the Kompose build option (discontinued in 2.18)
|
||||||
|
|
|
@ -172,7 +172,7 @@
|
||||||
ng-click="$ctrl.changeOrderBy('Name')"
|
ng-click="$ctrl.changeOrderBy('Name')"
|
||||||
></table-column-header>
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th ng-if="!$ctrl.hideStacksFunctionality">
|
||||||
<table-column-header
|
<table-column-header
|
||||||
col-title="'Stack'"
|
col-title="'Stack'"
|
||||||
can-sort="true"
|
can-sort="true"
|
||||||
|
@ -290,7 +290,7 @@
|
||||||
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
|
||||||
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName || '-' }}</td>
|
<td ng-if="!$ctrl.hideStacksFunctionality">{{ item.StackName || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation()">{{ item.ResourcePool }}</a>
|
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation()">{{ item.ResourcePool }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -330,6 +330,7 @@
|
||||||
refresh-callback="$ctrl.refreshCallback"
|
refresh-callback="$ctrl.refreshCallback"
|
||||||
on-publishing-mode-click="($ctrl.onPublishingModeClick)"
|
on-publishing-mode-click="($ctrl.onPublishingModeClick)"
|
||||||
is-primary="false"
|
is-primary="false"
|
||||||
|
hide-stacks-functionality="$ctrl.hideStacksFunctionality"
|
||||||
>
|
>
|
||||||
</kubernetes-applications-datatable>
|
</kubernetes-applications-datatable>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -21,5 +21,6 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatabl
|
||||||
isAppsLoading: '<',
|
isAppsLoading: '<',
|
||||||
isSystemResources: '<',
|
isSystemResources: '<',
|
||||||
setSystemResources: '<',
|
setSystemResources: '<',
|
||||||
|
hideStacksFunctionality: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -185,8 +185,9 @@ export default class HelmTemplatesController {
|
||||||
};
|
};
|
||||||
|
|
||||||
const helmRepos = await this.getHelmRepoURLs();
|
const helmRepos = await this.getHelmRepoURLs();
|
||||||
|
if (helmRepos) {
|
||||||
await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]);
|
await Promise.all([this.getLatestCharts(helmRepos), this.getResourcePools()]);
|
||||||
|
}
|
||||||
if (this.state.charts.length > 0 && this.$state.params.chartName) {
|
if (this.state.charts.length > 0 && this.$state.params.chartName) {
|
||||||
const chart = this.state.charts.find((chart) => chart.name === this.$state.params.chartName);
|
const chart = this.state.charts.find((chart) => chart.name === this.$state.params.chartName);
|
||||||
if (chart) {
|
if (chart) {
|
||||||
|
|
|
@ -42,7 +42,9 @@ class KubernetesDaemonSetConverter {
|
||||||
const payload = new KubernetesDaemonSetCreatePayload();
|
const payload = new KubernetesDaemonSetCreatePayload();
|
||||||
payload.metadata.name = daemonSet.Name;
|
payload.metadata.name = daemonSet.Name;
|
||||||
payload.metadata.namespace = daemonSet.Namespace;
|
payload.metadata.namespace = daemonSet.Namespace;
|
||||||
|
if (daemonSet.StackName) {
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName;
|
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName;
|
||||||
|
}
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
|
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner;
|
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner;
|
||||||
payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note;
|
payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note;
|
||||||
|
|
|
@ -45,7 +45,9 @@ class KubernetesDeploymentConverter {
|
||||||
const payload = new KubernetesDeploymentCreatePayload();
|
const payload = new KubernetesDeploymentCreatePayload();
|
||||||
payload.metadata.name = deployment.Name;
|
payload.metadata.name = deployment.Name;
|
||||||
payload.metadata.namespace = deployment.Namespace;
|
payload.metadata.namespace = deployment.Namespace;
|
||||||
|
if (deployment.StackName) {
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName;
|
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName;
|
||||||
|
}
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
|
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner;
|
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner;
|
||||||
payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note;
|
payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note;
|
||||||
|
|
|
@ -46,7 +46,9 @@ class KubernetesStatefulSetConverter {
|
||||||
const payload = new KubernetesStatefulSetCreatePayload();
|
const payload = new KubernetesStatefulSetCreatePayload();
|
||||||
payload.metadata.name = statefulSet.Name;
|
payload.metadata.name = statefulSet.Name;
|
||||||
payload.metadata.namespace = statefulSet.Namespace;
|
payload.metadata.namespace = statefulSet.Namespace;
|
||||||
|
if (statefulSet.StackName) {
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName;
|
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName;
|
||||||
|
}
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
|
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
|
||||||
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner;
|
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner;
|
||||||
payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note;
|
payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
|
import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector';
|
||||||
import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable';
|
import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable';
|
||||||
|
import { StackName } from '@/react/kubernetes/DeployView/StackName/StackName';
|
||||||
|
|
||||||
export const ngModule = angular
|
export const ngModule = angular
|
||||||
.module('portainer.kubernetes.react.components', [])
|
.module('portainer.kubernetes.react.components', [])
|
||||||
|
@ -101,6 +102,14 @@ export const ngModule = angular
|
||||||
'hideMessage',
|
'hideMessage',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'kubeStackName',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(StackName))), [
|
||||||
|
'setStackName',
|
||||||
|
'isAdmin',
|
||||||
|
'stackName',
|
||||||
|
])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'applicationSummaryWidget',
|
'applicationSummaryWidget',
|
||||||
r2a(
|
r2a(
|
||||||
|
|
|
@ -210,10 +210,14 @@ class KubernetesApplicationService {
|
||||||
* To synchronise with kubernetes resource creation summary output, any new resources created in this method should
|
* To synchronise with kubernetes resource creation summary output, any new resources created in this method should
|
||||||
* also be displayed in the summary output (getCreatedApplicationResources)
|
* also be displayed in the summary output (getCreatedApplicationResources)
|
||||||
*/
|
*/
|
||||||
async createAsync(formValues) {
|
async createAsync(formValues, hideStacks) {
|
||||||
// formValues -> Application
|
// formValues -> Application
|
||||||
let [app, headlessService, services, , claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
let [app, headlessService, services, , claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||||
|
|
||||||
|
if (hideStacks) {
|
||||||
|
app.StackName = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (services) {
|
if (services) {
|
||||||
services.forEach(async (service) => {
|
services.forEach(async (service) => {
|
||||||
try {
|
try {
|
||||||
|
@ -264,8 +268,8 @@ class KubernetesApplicationService {
|
||||||
await apiService.create(app);
|
await apiService.create(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
create(formValues) {
|
create(formValues, _, hideStacks) {
|
||||||
return this.$async(this.createAsync, formValues);
|
return this.$async(this.createAsync, formValues, hideStacks);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
is-apps-loading="ctrl.state.isAppsLoading"
|
is-apps-loading="ctrl.state.isAppsLoading"
|
||||||
is-system-resources="ctrl.state.isSystemResources"
|
is-system-resources="ctrl.state.isSystemResources"
|
||||||
set-system-resources="(ctrl.setSystemResources)"
|
set-system-resources="(ctrl.setSystemResources)"
|
||||||
|
hide-stacks-functionality="ctrl.deploymentOptions.hideStacksFunctionality"
|
||||||
>
|
>
|
||||||
</kubernetes-applications-datatable>
|
</kubernetes-applications-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelpe
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||||
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
import { KubernetesPortainerApplicationStackNameLabel } from 'Kubernetes/models/application/models';
|
||||||
import { confirmDelete } from '@@/modals/confirm';
|
import { confirmDelete } from '@@/modals/confirm';
|
||||||
|
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
||||||
|
|
||||||
class KubernetesApplicationsController {
|
class KubernetesApplicationsController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -196,6 +197,8 @@ class KubernetesApplicationsController {
|
||||||
isSystemResources: undefined,
|
isSystemResources: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.deploymentOptions = await getDeploymentOptions();
|
||||||
|
|
||||||
this.user = this.Authentication.getUserDetails();
|
this.user = this.Authentication.getUserDetails();
|
||||||
this.state.namespaces = await this.KubernetesNamespaceService.get();
|
this.state.namespaces = await this.KubernetesNamespaceService.get();
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,54 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region STACK -->
|
||||||
|
<div class="form-group" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality && ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
||||||
|
<div class="col-sm-12 small text-muted vertical-center">
|
||||||
|
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality && ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
||||||
|
<label for="stack_name" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Stack
|
||||||
|
<portainer-tooltip
|
||||||
|
ng-if="!ctrl.isAdmin"
|
||||||
|
message="'The stack field below was previously labelled \'Name\' but, in
|
||||||
|
fact, it\'s always been the stack name (hence the relabelling).'"
|
||||||
|
class-name="'[&>span]:!text-left'"
|
||||||
|
set-html-message="true"
|
||||||
|
>
|
||||||
|
</portainer-tooltip>
|
||||||
|
<portainer-tooltip
|
||||||
|
ng-if="ctrl.isAdmin"
|
||||||
|
message="'The stack field below was previously labelled \'Name\' but, in
|
||||||
|
fact, it\'s always been the stack name (hence the relabelling).<br/>
|
||||||
|
Kubernetes Stacks functionality can be turned off entirely via
|
||||||
|
<a href=\'/#!/settings\' target=\'_blank\'>
|
||||||
|
Kubernetes Settings
|
||||||
|
</a>.'"
|
||||||
|
class-name="'[&>span]:!text-left'"
|
||||||
|
set-html-message="true"
|
||||||
|
>
|
||||||
|
</portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="myStack"
|
||||||
|
ng-model="ctrl.formValues.StackName"
|
||||||
|
name="stack_name"
|
||||||
|
uib-typeahead="stack for stack in ctrl.stacks | filter:$viewValue | limitTo:7"
|
||||||
|
typeahead-min-length="0"
|
||||||
|
data-cy="k8sAppCreate-stackName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region Git repository -->
|
<!-- #region Git repository -->
|
||||||
<kubernetes-redeploy-app-git-form
|
<kubernetes-redeploy-app-git-form
|
||||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
|
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
|
||||||
|
@ -218,7 +266,7 @@
|
||||||
|
|
||||||
<div ng-if="ctrl.formValues.ResourcePool">
|
<div ng-if="ctrl.formValues.ResourcePool">
|
||||||
<!-- #region STACK -->
|
<!-- #region STACK -->
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality">
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
<div class="col-sm-12 small text-muted vertical-center">
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
||||||
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
|
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 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="!ctrl.deploymentOptions.hideStacksFunctionality">
|
||||||
<label for="stack_name" class="col-sm-3 col-lg-2 control-label text-left">Stack</label>
|
<label for="stack_name" class="col-sm-3 col-lg-2 control-label text-left">Stack</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<input
|
<input
|
||||||
|
@ -1401,7 +1449,7 @@
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
ng-click="ctrl.updateApplicationViaWebEditor()"
|
ng-click="ctrl.updateApplicationViaWebEditor()"
|
||||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
|
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
|
||||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
|
ng-disabled="kubernetesApplicationCreationForm.$valid && !ctrl.state.isEditorDirty && ctrl.savedFormValues.StackName === ctrl.formValues.StackName || ctrl.state.updateWebEditorInProgress"
|
||||||
style="margin-top: 7px; margin-left: 0"
|
style="margin-top: 7px; margin-left: 0"
|
||||||
button-spinner="ctrl.state.updateWebEditorInProgress"
|
button-spinner="ctrl.state.updateWebEditorInProgress"
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as JsonPatch from 'fast-json-patch';
|
||||||
import { RegistryTypes } from '@/portainer/models/registryTypes';
|
import { RegistryTypes } from '@/portainer/models/registryTypes';
|
||||||
import { getServices } from '@/react/kubernetes/networks/services/service';
|
import { getServices } from '@/react/kubernetes/networks/services/service';
|
||||||
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
|
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
|
||||||
|
import { getGlobalDeploymentOptions } from '@/react/portainer/settings/settings.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationDataAccessPolicies,
|
KubernetesApplicationDataAccessPolicies,
|
||||||
|
@ -196,7 +197,10 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.updateWebEditorInProgress = true;
|
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;
|
this.state.isEditorDirty = false;
|
||||||
await this.$state.reload(this.$state.current);
|
await this.$state.reload(this.$state.current);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -932,7 +936,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
|
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
|
||||||
// combine the secrets and configmap form values when submitting the form
|
// combine the secrets and configmap form values when submitting the form
|
||||||
_.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined);
|
_.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.Notifications.success('Request to deploy application successfully submitted', this.formValues.Name);
|
||||||
this.$state.go('kubernetes.applications');
|
this.$state.go('kubernetes.applications');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -1092,6 +1096,8 @@ class KubernetesCreateApplicationController {
|
||||||
this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
this.state.useLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||||
this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||||
|
|
||||||
|
this.deploymentOptions = await getGlobalDeploymentOptions();
|
||||||
|
|
||||||
const [resourcePools, nodes, nodesLimits] = await Promise.all([
|
const [resourcePools, nodes, nodesLimits] = await Promise.all([
|
||||||
this.KubernetesResourcePoolService.get(),
|
this.KubernetesResourcePoolService.get(),
|
||||||
this.KubernetesNodeService.get(),
|
this.KubernetesNodeService.get(),
|
||||||
|
@ -1140,6 +1146,8 @@ class KubernetesCreateApplicationController {
|
||||||
this.nodesLabels,
|
this.nodesLabels,
|
||||||
this.ingresses
|
this.ingresses
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.formValues.Services = this.formValues.Services || [];
|
||||||
this.originalServicePorts = structuredClone(this.formValues.Services.flatMap((service) => service.Ports));
|
this.originalServicePorts = structuredClone(this.formValues.Services.flatMap((service) => service.Ports));
|
||||||
this.originalIngressPaths = structuredClone(this.originalServicePorts.flatMap((port) => port.ingressPaths).filter((ingressPath) => ingressPath.Host));
|
this.originalIngressPaths = structuredClone(this.originalServicePorts.flatMap((port) => port.ingressPaths).filter((ingressPath) => ingressPath.Host));
|
||||||
|
|
||||||
|
|
|
@ -32,11 +32,13 @@
|
||||||
<label for="target_node" class="col-lg-2 col-sm-3 control-label text-left">Namespace</label>
|
<label for="target_node" class="col-lg-2 col-sm-3 control-label text-left">Namespace</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<select
|
<select
|
||||||
|
ng-if="!ctrl.formValues.namespace_toggle"
|
||||||
ng-disabled="ctrl.formValues.namespace_toggle"
|
ng-disabled="ctrl.formValues.namespace_toggle"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
ng-model="ctrl.formValues.Namespace"
|
ng-model="ctrl.formValues.Namespace"
|
||||||
ng-options="namespace.Name as namespace.Name for namespace in ctrl.namespaces"
|
ng-options="namespace.Name as namespace.Name for namespace in ctrl.namespaces"
|
||||||
></select>
|
></select>
|
||||||
|
<span ng-if="ctrl.formValues.namespace_toggle">Namespaces specified in the manifest will be used</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -48,12 +50,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="stack_name" class="col-lg-2 col-sm-3 control-label required text-left">Name</label>
|
<label for="stack_name" class="col-lg-2 col-sm-3 control-label text-left">Name</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8"> Resource names specified in the manifest will be used </div>
|
||||||
<input type="text" class="form-control" ng-model="ctrl.formValues.StackName" id="stack_name" placeholder="my-app" auto-focus />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<kube-stack-name
|
||||||
|
ng-if="!ctrl.deploymentOptions.hideStacksFunctionality"
|
||||||
|
stack-name="ctrl.formValues.StackName"
|
||||||
|
set-stack-name="(ctrl.setStackName)"
|
||||||
|
is-admin="ctrl.currentUser.isAdmin"
|
||||||
|
></kube-stack-name>
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Deploy from </div>
|
<div class="col-sm-12 form-section-title"> Deploy from </div>
|
||||||
<box-selector
|
<box-selector
|
||||||
slim="true"
|
slim="true"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import stripAnsi from 'strip-ansi';
|
||||||
import PortainerError from '@/portainer/error';
|
import PortainerError from '@/portainer/error';
|
||||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { getDeploymentOptions } from '@/react/portainer/environments/environment.service';
|
||||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
import { kubernetes } from '@@/BoxSelector/common-options/deployment-methods';
|
||||||
import { editor, git, customTemplate, url, helm } from '@@/BoxSelector/common-options/build-methods';
|
import { editor, git, customTemplate, url, helm } from '@@/BoxSelector/common-options/build-methods';
|
||||||
|
@ -89,6 +90,7 @@ class KubernetesDeployController {
|
||||||
this.onChangeMethod = this.onChangeMethod.bind(this);
|
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||||
this.onChangeDeployType = this.onChangeDeployType.bind(this);
|
this.onChangeDeployType = this.onChangeDeployType.bind(this);
|
||||||
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
|
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
|
||||||
|
this.setStackName = this.setStackName.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectHelmChart(chart) {
|
onSelectHelmChart(chart) {
|
||||||
|
@ -101,6 +103,10 @@ class KubernetesDeployController {
|
||||||
this.renderTemplate();
|
this.renderTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStackName(name) {
|
||||||
|
this.formValues.StackName = name;
|
||||||
|
}
|
||||||
|
|
||||||
renderTemplate() {
|
renderTemplate() {
|
||||||
if (!this.isTemplateVariablesEnabled) {
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
return;
|
return;
|
||||||
|
@ -180,7 +186,7 @@ class KubernetesDeployController {
|
||||||
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
|
const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL);
|
||||||
|
|
||||||
const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
|
const isNamespaceInvalid = _.isEmpty(this.formValues.Namespace);
|
||||||
return !this.formValues.StackName || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress || isNamespaceInvalid;
|
return isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress || isNamespaceInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeFormValues(newValues) {
|
onChangeFormValues(newValues) {
|
||||||
|
@ -360,6 +366,8 @@ class KubernetesDeployController {
|
||||||
this.formValues.namespace_toggle = false;
|
this.formValues.namespace_toggle = false;
|
||||||
await this.getNamespaces();
|
await this.getNamespaces();
|
||||||
|
|
||||||
|
this.deploymentOptions = await getDeploymentOptions(this.endpoint.Id);
|
||||||
|
|
||||||
if (this.$state.params.templateId) {
|
if (this.$state.params.templateId) {
|
||||||
const templateId = parseInt(this.$state.params.templateId, 10);
|
const templateId = parseInt(this.$state.params.templateId, 10);
|
||||||
if (templateId && !Number.isNaN(templateId)) {
|
if (templateId && !Number.isNaN(templateId)) {
|
||||||
|
|
|
@ -262,12 +262,13 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
).$promise;
|
).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateKubeStack = function (stack, { stackFile, gitConfig, webhookId }) {
|
service.updateKubeStack = function (stack, { stackFile, gitConfig, webhookId, stackName }) {
|
||||||
let payload = {};
|
let payload = {};
|
||||||
|
|
||||||
if (stackFile) {
|
if (stackFile) {
|
||||||
payload = {
|
payload = {
|
||||||
StackFileContent: stackFile,
|
StackFileContent: stackFile,
|
||||||
|
StackName: stackName,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { InsightsBox } from '@@/InsightsBox';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
stackName: string;
|
||||||
|
setStackName: (name: string) => 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 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
You can leave the stack name empty, or even turn off Kubernetes Stacks
|
||||||
|
functionality entirely via{' '}
|
||||||
|
<Link to="portainer.settings" target="_blank">
|
||||||
|
Kubernetes Settings
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const insightsBoxContent = (
|
||||||
|
<>
|
||||||
|
The stack field below was previously labelled 'Name' but, in
|
||||||
|
fact, it's always been the stack name (hence the relabelling).
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
Kubernetes Stacks functionality can be turned off entirely via{' '}
|
||||||
|
<Link to="portainer.settings" target="_blank">
|
||||||
|
Kubernetes Settings
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-fit mb-4">
|
||||||
|
<InsightsBox
|
||||||
|
type="slim"
|
||||||
|
header="Stack"
|
||||||
|
content={insightsBoxContent}
|
||||||
|
insightCloseId="k8s-stacks-name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextTip className="mb-4" color="blue">
|
||||||
|
Enter or select a 'stack' name to group multiple deployments
|
||||||
|
together, or else leave empty to ignore.
|
||||||
|
</TextTip>
|
||||||
|
<div className="form-group">
|
||||||
|
<label
|
||||||
|
htmlFor="stack_name"
|
||||||
|
className="col-lg-2 col-sm-3 control-label text-left"
|
||||||
|
>
|
||||||
|
Stack
|
||||||
|
<Tooltip message={tooltip} setHtmlMessage />
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-8">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
defaultValue={stackName}
|
||||||
|
onChange={(e) => setStackName(e.target.value)}
|
||||||
|
id="stack_name"
|
||||||
|
placeholder="myStack"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
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 { DetailsTable } from '@@/DetailsTable';
|
||||||
import { Badge } from '@@/Badge';
|
import { Badge } from '@@/Badge';
|
||||||
|
@ -69,6 +71,11 @@ export function ApplicationSummaryWidget() {
|
||||||
setApplicationNoteFormValues(applicationNote || '');
|
setApplicationNoteFormValues(applicationNote || '');
|
||||||
}, [applicationNote]);
|
}, [applicationNote]);
|
||||||
|
|
||||||
|
const globalDeploymentOptionsQuery =
|
||||||
|
usePublicSettings<GlobalDeploymentOptions>({
|
||||||
|
select: (settings) => settings.GlobalDeploymentOptions,
|
||||||
|
});
|
||||||
|
|
||||||
const failedCreateCondition = application?.status?.conditions?.find(
|
const failedCreateCondition = application?.status?.conditions?.find(
|
||||||
(condition) => condition.reason === 'FailedCreate'
|
(condition) => condition.reason === 'FailedCreate'
|
||||||
);
|
);
|
||||||
|
@ -117,6 +124,9 @@ export function ApplicationSummaryWidget() {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{globalDeploymentOptionsQuery.data &&
|
||||||
|
!globalDeploymentOptionsQuery.data
|
||||||
|
.hideStacksFunctionality && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stack</td>
|
<td>Stack</td>
|
||||||
<td data-cy="k8sAppDetail-stackName">
|
<td data-cy="k8sAppDetail-stackName">
|
||||||
|
@ -124,6 +134,7 @@ export function ApplicationSummaryWidget() {
|
||||||
'-'}
|
'-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Namespace</td>
|
<td>Namespace</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
StatusType as EdgeStackStatusType,
|
StatusType as EdgeStackStatusType,
|
||||||
} from '@/react/edge/edge-stacks/types';
|
} from '@/react/edge/edge-stacks/types';
|
||||||
|
|
||||||
|
import { getPublicSettings } from '../../settings/settings.service';
|
||||||
import type {
|
import type {
|
||||||
Environment,
|
Environment,
|
||||||
EnvironmentId,
|
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) {
|
export async function snapshotEndpoint(id: EnvironmentId) {
|
||||||
try {
|
try {
|
||||||
await axios.post<void>(buildUrl(id, 'snapshot'));
|
await axios.post<void>(buildUrl(id, 'snapshot'));
|
||||||
|
|
|
@ -14,6 +14,7 @@ export function DeploymentOptionsSection() {
|
||||||
values: { globalDeploymentOptions: values },
|
values: { globalDeploymentOptions: values },
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
} = useFormikContext<FormValues>();
|
} = useFormikContext<FormValues>();
|
||||||
|
|
||||||
const limitedFeature = isLimitedToBE(FeatureId.ENFORCE_DEPLOYMENT_OPTIONS);
|
const limitedFeature = isLimitedToBE(FeatureId.ENFORCE_DEPLOYMENT_OPTIONS);
|
||||||
return (
|
return (
|
||||||
<FormSection title="Deployment Options">
|
<FormSection title="Deployment Options">
|
||||||
|
@ -74,6 +75,24 @@ export function DeploymentOptionsSection() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<KubeNoteMinimumCharacters />
|
<KubeNoteMinimumCharacters />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<SwitchField
|
||||||
|
label="Allow stacks functionality with Kubernetes environments"
|
||||||
|
checked={!values.hideStacksFunctionality}
|
||||||
|
onChange={(value) =>
|
||||||
|
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."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,17 @@ export function KubeSettingsPanel() {
|
||||||
const initialValues: FormValues = {
|
const initialValues: FormValues = {
|
||||||
helmRepositoryUrl: settingsQuery.data.HelmRepositoryURL || '',
|
helmRepositoryUrl: settingsQuery.data.HelmRepositoryURL || '',
|
||||||
kubeconfigExpiry: settingsQuery.data.KubeconfigExpiry || '0',
|
kubeconfigExpiry: settingsQuery.data.KubeconfigExpiry || '0',
|
||||||
globalDeploymentOptions: settingsQuery.data.GlobalDeploymentOptions || {
|
globalDeploymentOptions: {
|
||||||
|
...{
|
||||||
requireNoteOnApplications: false,
|
requireNoteOnApplications: false,
|
||||||
minApplicationNoteLength: 0,
|
minApplicationNoteLength: 0,
|
||||||
hideAddWithForm: false,
|
hideAddWithForm: false,
|
||||||
hideFileUpload: false,
|
hideFileUpload: false,
|
||||||
hideWebEditor: false,
|
hideWebEditor: false,
|
||||||
perEnvOverride: false,
|
perEnvOverride: false,
|
||||||
|
hideStacksFunctionality: false,
|
||||||
|
},
|
||||||
|
...settingsQuery.data.GlobalDeploymentOptions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,5 +8,6 @@ export interface FormValues {
|
||||||
hideFileUpload: boolean;
|
hideFileUpload: boolean;
|
||||||
requireNoteOnApplications: boolean;
|
requireNoteOnApplications: boolean;
|
||||||
minApplicationNoteLength: number;
|
minApplicationNoteLength: number;
|
||||||
|
hideStacksFunctionality: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function validation(): SchemaOf<FormValues> {
|
||||||
hideWebEditor: boolean().required(),
|
hideWebEditor: boolean().required(),
|
||||||
hideFileUpload: boolean().required(),
|
hideFileUpload: boolean().required(),
|
||||||
requireNoteOnApplications: boolean().required(),
|
requireNoteOnApplications: boolean().required(),
|
||||||
|
hideStacksFunctionality: boolean().required(),
|
||||||
minApplicationNoteLength: number()
|
minApplicationNoteLength: number()
|
||||||
.typeError('Must be a number')
|
.typeError('Must be a number')
|
||||||
.default(0)
|
.default(0)
|
||||||
|
|
|
@ -145,7 +145,7 @@ export interface Settings {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GlobalDeploymentOptions {
|
export interface GlobalDeploymentOptions {
|
||||||
/** Hide manual deploy forms in portainer */
|
/** Hide manual deploy forms in portainer */
|
||||||
hideAddWithForm: boolean;
|
hideAddWithForm: boolean;
|
||||||
/** Configure this per environment or globally */
|
/** Configure this per environment or globally */
|
||||||
|
@ -157,6 +157,8 @@ interface GlobalDeploymentOptions {
|
||||||
/** Make note on application add/edit screen required */
|
/** Make note on application add/edit screen required */
|
||||||
requireNoteOnApplications: boolean;
|
requireNoteOnApplications: boolean;
|
||||||
minApplicationNoteLength: number;
|
minApplicationNoteLength: number;
|
||||||
|
|
||||||
|
hideStacksFunctionality: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicSettingsResponse {
|
export interface PublicSettingsResponse {
|
||||||
|
|
Loading…
Reference in New Issue