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 @@
+
+
+
+
+
+
- |