diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 524c92511..93dcf1420 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -22,6 +22,7 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment'; import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet'; import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; +import KubernetesPodConverter from 'Kubernetes/pod/converter'; import KubernetesServiceConverter from 'Kubernetes/converters/service'; import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; import PortainerError from 'Portainer/error'; @@ -55,6 +56,8 @@ class KubernetesApplicationConverter { res.Id = data.metadata.uid; res.Name = data.metadata.name; res.Metadata = data.metadata; + res.ApplicationType = data.kind; + res.Labels = data.metadata.labels || {}; if (data.metadata.labels) { const { labels } = data.metadata; @@ -281,6 +284,7 @@ class KubernetesApplicationConverter { res.ApplicationType = app.ApplicationType; res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); res.Name = app.Name; + res.Labels = app.Labels; res.Services = KubernetesApplicationHelper.generateServicesFormValuesFromServices(app, ingresses); res.Selector = KubernetesApplicationHelper.generateSelectorFromService(app); res.StackName = app.StackName; @@ -341,6 +345,8 @@ class KubernetesApplicationConverter { (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Shared && rwx))) || formValues.ApplicationType === KubernetesApplicationTypes.DaemonSet; + const pod = formValues.ApplicationType === KubernetesApplicationTypes.POD; + let app; if (deployment) { app = KubernetesDeploymentConverter.applicationFormValuesToDeployment(formValues, claims); @@ -348,6 +354,8 @@ class KubernetesApplicationConverter { app = KubernetesStatefulSetConverter.applicationFormValuesToStatefulSet(formValues, claims); } else if (daemonSet) { app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims); + } else if (pod) { + app = KubernetesPodConverter.applicationFormValuesToPod(formValues, claims); } else { throw new PortainerError('Unable to determine which association to use to convert form'); } diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 9d21e469e..25dcfd271 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -315,7 +315,12 @@ class KubernetesApplicationHelper { ports.push(svcport); }); svc.Ports = ports; - svc.Selector = app.Raw.spec.selector.matchLabels; + // if the app is a pod (doesn't have a selector), then get the pod labels + if (app.Raw.spec.selector) { + svc.Selector = app.Raw.spec.selector.matchLabels; + } else { + svc.Selector = app.Raw.metadata.labels || { app: app.Name }; + } services.push(svc); } }); diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js index 987a7fa4e..206474ffc 100644 --- a/app/kubernetes/helpers/serviceHelper.js +++ b/app/kubernetes/helpers/serviceHelper.js @@ -14,10 +14,12 @@ class KubernetesServiceHelper { } static findApplicationBoundServices(services, rawApp) { - if (!rawApp.spec.template) { + // if the app is a naked pod (doesn't have a template), then get the pod labels + const appLabels = rawApp.spec.template ? rawApp.spec.template.metadata.labels : rawApp.metadata.labels; + if (!appLabels) { return undefined; } - return _.filter(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); + return _.filter(services, (item) => item.spec.selector && _.isMatch(appLabels, item.spec.selector)); } } export default KubernetesServiceHelper; diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js index e3ca39239..89503d8ab 100644 --- a/app/kubernetes/pod/converter.js +++ b/app/kubernetes/pod/converter.js @@ -9,7 +9,9 @@ import { KubernetesPortainerApplicationNote, } from 'Kubernetes/models/application/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes, KubernetesPodEviction } from 'Kubernetes/pod/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import { createPayloadFactory } from './payloads/create'; function computeStatus(statuses) { @@ -100,6 +102,32 @@ function computeContainers(data) { } export default class KubernetesPodConverter { + static applicationFormValuesToPod(formValues, volumeClaims) { + let serviceSelector = {}; + if (formValues.Services.length) { + serviceSelector = formValues.Services[0].Selector || { app: formValues.Name }; + } + + const res = new KubernetesPod(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.ImageModel = formValues.ImageModel; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + res.Containers = formValues.Containers; + res.ApplicationType = formValues.ApplicationType; + res.ServiceSelector = serviceSelector; + res.Labels = formValues.Labels; + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.ConfigMaps, formValues.Secrets); + KubernetesApplicationHelper.generateAffinityFromPlacements(res, formValues); + return res; + } + static apiToModel(data) { const res = new KubernetesPod(); res.Id = data.metadata.uid; @@ -115,6 +143,7 @@ export default class KubernetesPodConverter { res.Affinity = computeAffinity(data.spec.affinity); res.NodeSelector = data.spec.nodeSelector; res.Tolerations = computeTolerations(data.spec.tolerations); + res.Labels = data.metadata.labels; return res; } @@ -140,6 +169,7 @@ function createPayload(pod) { payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = pod.StackName; payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = pod.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pod.ApplicationOwner; + payload.metadata.labels = { ...(pod.Labels || {}), ...(pod.ServiceSelector || {}), ...payload.metadata.labels }; if (pod.Note) { payload.metadata.annotations[KubernetesPortainerApplicationNote] = pod.Note; } else { diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 9d620b228..2ef06c764 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -199,13 +199,6 @@ class KubernetesCreateApplicationController { this.updateApplicationType(); } - updateApplicationType() { - return this.$scope.$evalAsync(() => { - this.formValues.ApplicationType = this.getAppType(); - this.validatePersistedFolders(); - }); - } - getAppType() { if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) { return this.ApplicationTypes.DaemonSet; @@ -216,6 +209,14 @@ class KubernetesCreateApplicationController { return this.ApplicationTypes.Deployment; } + // keep the application type up to date + updateApplicationType() { + return this.$scope.$evalAsync(() => { + this.formValues.ApplicationType = this.getAppType(); + this.validatePersistedFolders(); + }); + } + onChangeFileContent(value) { this.$scope.$evalAsync(() => { if (this.oldStackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== value.replace(/(\r\n|\n|\r)/gm, '')) { @@ -273,7 +274,9 @@ class KubernetesCreateApplicationController { } imageValidityIsValid() { - return this.state.pullImageValidity || (this.formValues.registryDetails && this.formValues.registryDetails.Registry.Type !== RegistryTypes.DOCKERHUB); + return ( + this.isExternalApplication() || this.state.pullImageValidity || (this.formValues.registryDetails && this.formValues.registryDetails.Registry.Type !== RegistryTypes.DOCKERHUB) + ); } onChangeAppName(appName) { @@ -349,6 +352,7 @@ class KubernetesCreateApplicationController { persistedFolder.useNewVolume = true; }); this.validatePersistedFolders(); + this.updateApplicationType(); } /* #endregion */