diff --git a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html index 8c291971d..1d77ab609 100644 --- a/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html +++ b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html @@ -57,7 +57,7 @@ - + - + {{ item.RunningPodsCount }} / {{ item.TotalPodsCount }} + + - +
+ Pod @@ -107,7 +107,7 @@ dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))" pagination-id="$ctrl.tableKey" > - {{ item.PodName }}{{ item.PodName }} {{ item.Name }} {{ item.Image }} {{ item.Image }} + {{ item.Containers.length - 1 }} {{ item.ApplicationType | kubernetesApplicationTypeText }} + Replicated Global - {{ item.RunningPodsCount }} / {{ item.TotalPodsCount }} + {{ item.Pods[0].Status }} + diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js index aaee33363..f195c2bc0 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js @@ -1,4 +1,4 @@ -import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [ @@ -42,6 +42,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo this.$onInit = function () { this.isAdmin = Authentication.isAdmin(); this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.KubernetesApplicationTypes = KubernetesApplicationTypes; this.setDefaults(); this.prepareTableFromDataset(); diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 56cfdf2bb..201451f82 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -50,7 +50,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) { class KubernetesApplicationConverter { static applicationCommon(res, data, pods, service, ingresses) { - const containers = _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined); + const containers = data.spec.template ? _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined) : data.spec.containers; res.Id = data.metadata.uid; res.Name = data.metadata.name; res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; @@ -61,7 +61,7 @@ class KubernetesApplicationConverter { res.Image = containers[0].image; res.CreationDate = data.metadata.creationTimestamp; res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined); - res.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, data); + res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data]; const limits = { Cpu: 0, @@ -118,7 +118,11 @@ class KubernetesApplicationConverter { res.PublishedPorts = ports; } - res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : []; + if (data.spec.templates) { + res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : []; + } else { + res.Volumes = data.spec.volumes; + } // TODO: review // this if() fixs direct use of PVC reference inside spec.template.spec.containers[0].volumeMounts @@ -169,7 +173,7 @@ class KubernetesApplicationConverter { res.PersistedFolders = _.without(res.PersistedFolders, undefined); res.ConfigurationVolumes = _.reduce( - data.spec.template.spec.volumes, + res.Volumes, (acc, volume) => { if (volume.configMap || volume.secret) { const matchingVolumeMount = _.find(_.flatMap(_.map(containers, 'volumeMounts')), { name: volume.name }); @@ -213,6 +217,13 @@ class KubernetesApplicationConverter { ); } + static apiPodToApplication(data, pods, service, ingresses) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses); + res.ApplicationType = KubernetesApplicationTypes.POD; + return res; + } + static apiDeploymentToApplication(data, pods, service, ingresses) { const res = new KubernetesApplication(); KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses); @@ -310,7 +321,7 @@ class KubernetesApplicationConverter { } else if (daemonSet) { app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims); } else { - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use to convert form'); } let headlessService; diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index 0629c12ff..0ce2d3831 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -57,6 +57,8 @@ angular return KubernetesApplicationTypeStrings.DAEMONSET; case KubernetesApplicationTypes.STATEFULSET: return KubernetesApplicationTypeStrings.STATEFULSET; + case KubernetesApplicationTypes.POD: + return KubernetesApplicationTypeStrings.POD; default: return '-'; } diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 9bc898fca..3124f4247 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -38,8 +38,8 @@ class KubernetesApplicationHelper { return !application.ApplicationOwner; } - static associatePodsAndApplication(pods, app) { - return _.filter(pods, { Labels: app.spec.selector.matchLabels }); + static associatePodsAndApplication(pods, selector) { + return _.filter(pods, ['metadata.labels', selector.matchLabels]); } static associateContainerPersistedFoldersAndConfigurations(app, containers) { diff --git a/app/kubernetes/helpers/application/rollback.js b/app/kubernetes/helpers/application/rollback.js index f593e0cf5..7f110e8f8 100644 --- a/app/kubernetes/helpers/application/rollback.js +++ b/app/kubernetes/helpers/application/rollback.js @@ -20,7 +20,7 @@ class KubernetesApplicationRollbackHelper { result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision); break; default: - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use to convert patch'); } return result; } diff --git a/app/kubernetes/helpers/history/index.js b/app/kubernetes/helpers/history/index.js index d611644a0..5f8851ab4 100644 --- a/app/kubernetes/helpers/history/index.js +++ b/app/kubernetes/helpers/history/index.js @@ -21,7 +21,7 @@ class KubernetesHistoryHelper { [currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw); break; default: - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use to get revisions'); } revisionsList = _.sortBy(revisionsList, 'revision'); return [currentRevision, revisionsList]; diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js index c263a3766..247e4a441 100644 --- a/app/kubernetes/helpers/serviceHelper.js +++ b/app/kubernetes/helpers/serviceHelper.js @@ -7,6 +7,9 @@ class KubernetesServiceHelper { } static findApplicationBoundService(services, rawApp) { + if (!rawApp.spec.template) { + return undefined; + } return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); } } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/helper.js b/app/kubernetes/horizontal-pod-auto-scaler/helper.js index 663c3baf8..44bc10c49 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/helper.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/helper.js @@ -18,7 +18,8 @@ export class KubernetesHorizontalPodAutoScalerHelper { return KubernetesApplicationTypeStrings.DAEMONSET; } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { return KubernetesApplicationTypeStrings.STATEFULSET; - // } else if () { ---> TODO: refactor - handle bare pod type ! + } else if (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.POD) { + return KubernetesApplicationTypeStrings.POD; } else { throw new PortainerError('Unable to determine application type'); } diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js index 2d42bba05..970cb3557 100644 --- a/app/kubernetes/models/application/models/constants.js +++ b/app/kubernetes/models/application/models/constants.js @@ -12,12 +12,14 @@ export const KubernetesApplicationTypes = Object.freeze({ DEPLOYMENT: 1, DAEMONSET: 2, STATEFULSET: 3, + POD: 4, }); export const KubernetesApplicationTypeStrings = Object.freeze({ DEPLOYMENT: 'Deployment', DAEMONSET: 'DaemonSet', STATEFULSET: 'StatefulSet', + POD: 'Pod', }); export const KubernetesApplicationPublishingTypes = Object.freeze({ diff --git a/app/kubernetes/models/common/params.js b/app/kubernetes/models/common/params.js index fd37b8764..5c7de8d34 100644 --- a/app/kubernetes/models/common/params.js +++ b/app/kubernetes/models/common/params.js @@ -1,11 +1,8 @@ /** * Generic params */ -const _KubernetesCommonParams = Object.freeze({ - id: '', -}); -export class KubernetesCommonParams { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams))); - } +export function KubernetesCommonParams() { + return { + id: '', + }; } diff --git a/app/kubernetes/pod/service.js b/app/kubernetes/pod/service.js index c2de34a2e..2aa3df981 100644 --- a/app/kubernetes/pod/service.js +++ b/app/kubernetes/pod/service.js @@ -1,9 +1,7 @@ -import _ from 'lodash-es'; import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; -import KubernetesPodConverter from 'Kubernetes/pod/converter'; class KubernetesPodService { /* @ngInject */ @@ -11,23 +9,43 @@ class KubernetesPodService { this.$async = $async; this.KubernetesPods = KubernetesPods; + this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); this.logsAsync = this.logsAsync.bind(this); this.deleteAsync = this.deleteAsync.bind(this); } + + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesPods(namespace).get(params).$promise, this.KubernetesPods(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve pod', err); + } + } + /** * GET ALL */ async getAllAsync(namespace) { try { const data = await this.KubernetesPods(namespace).get().$promise; - return _.map(data.items, (item) => KubernetesPodConverter.apiToModel(item)); + return data.items; } catch (err) { throw new PortainerError('Unable to retrieve pods', err); } } - get(namespace) { + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } return this.$async(this.getAllAsync, namespace); } diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 55bfa9908..075adb91f 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -18,6 +18,7 @@ import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter'; import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; +import KubernetesPodConverter from 'Kubernetes/pod/converter'; class KubernetesApplicationService { /* #region CONSTRUCTOR */ @@ -71,7 +72,7 @@ class KubernetesApplicationService { } else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) { apiService = this.KubernetesStatefulSetService; } else { - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use to retrieve API Service'); } return apiService; } @@ -87,15 +88,18 @@ class KubernetesApplicationService { /* #region GET */ async getAsync(namespace, name) { try { - const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([ + const [deployment, daemonSet, statefulSet, pod, pods, autoScalers, ingresses] = await Promise.allSettled([ this.KubernetesDeploymentService.get(namespace, name), this.KubernetesDaemonSetService.get(namespace, name), this.KubernetesStatefulSetService.get(namespace, name), + this.KubernetesPodService.get(namespace, name), this.KubernetesPodService.get(namespace), this.KubernetesHorizontalPodAutoScalerService.get(namespace), this.KubernetesIngressService.get(namespace), ]); + // const pod = _.find(pods.value, ['metadata.namespace', namespace, 'metadata.name', name]); + let rootItem; let converterFunc; if (deployment.status === 'fulfilled') { @@ -107,8 +111,11 @@ class KubernetesApplicationService { } else if (statefulSet.status === 'fulfilled') { rootItem = statefulSet; converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication; + } else if (pod.status === 'fulfilled') { + rootItem = pod; + converterFunc = KubernetesApplicationConverter.apiPodToApplication; } else { - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use to convert application'); } const services = await this.KubernetesServiceService.get(namespace); @@ -118,6 +125,7 @@ class KubernetesApplicationService { const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value); application.Yaml = rootItem.value.Yaml; application.Raw = rootItem.value.Raw; + application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item)); application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); @@ -173,7 +181,14 @@ class KubernetesApplicationService { convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) ); - const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + const boundPods = _.concat(_.flatMap(deploymentApplications, 'Pods'), _.flatMap(daemonSetApplications, 'Pods'), _.flatMap(statefulSetApplications, 'Pods')); + const unboundPods = _.without(pods, ...boundPods); + const nakedPodsApplications = _.map(unboundPods, (item) => convertToApplication(item, KubernetesApplicationConverter.apiPodToApplication, services, pods, ingresses)); + + const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications, nakedPodsApplications); + _.forEach(applications, (app) => { + app.Pods = _.map(app.Pods, (item) => KubernetesPodConverter.apiToModel(item)); + }); await Promise.all( _.forEach(applications, async (application) => { const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); diff --git a/app/kubernetes/services/historyService.js b/app/kubernetes/services/historyService.js index df0284166..b00bf7f3e 100644 --- a/app/kubernetes/services/historyService.js +++ b/app/kubernetes/services/historyService.js @@ -32,13 +32,17 @@ class KubernetesHistoryService { case KubernetesApplicationTypes.STATEFULSET: rawRevisions = await this.KubernetesControllerRevisionService.get(namespace); break; + case KubernetesApplicationTypes.POD: + rawRevisions = []; + break; default: - throw new PortainerError('Unable to determine which association to use'); + throw new PortainerError('Unable to determine which association to use for history'); + } + if (rawRevisions.length) { + const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application); + application.CurrentRevision = currentRevision; + application.Revisions = revisionsList; } - - const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application); - application.CurrentRevision = currentRevision; - application.Revisions = revisionsList; return application; } catch (err) { throw new PortainerError('', err); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 197a3e5ea..822ef9adc 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -43,16 +43,21 @@
Status + Replicated Global {{ ctrl.application.RunningPodsCount }} / {{ ctrl.application.TotalPodsCount }} + {{ ctrl.application.Pods[0].Status }} +
Resource reservations
-
per instance
+
+ per instance +
CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}
@@ -557,7 +562,8 @@ title-icon="fa-server" dataset="ctrl.allContainers" table-key="kubernetes.application.containers" - order-by="PodName" + is-pod="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD" + order-by="{{ ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD ? 'Name' : 'PodName' }}" > diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index b3304df8c..773e2099c 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -1,7 +1,7 @@ import angular from 'angular'; import * as _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; -import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; @@ -123,6 +123,8 @@ class KubernetesApplicationController { this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.KubernetesApplicationTypes = KubernetesApplicationTypes; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; this.KubernetesServiceTypes = KubernetesServiceTypes; this.KubernetesPodContainerTypes = KubernetesPodContainerTypes; @@ -340,7 +342,6 @@ class KubernetesApplicationController { SelectedRevision: undefined, }; - this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; await this.getApplication(); await this.getEvents(); this.state.viewReady = true; diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index 15a2faa87..c5a39a737 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -61,7 +61,7 @@ class InitEndpointController { case PortainerEndpointConnectionTypes.AGENT: return this.createAgentEndpoint(); default: - this.Notifications.error('Failure', 'Unable to determine which action to do'); + this.Notifications.error('Failure', 'Unable to determine which action to do to create endpoint'); } }