diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 6bc5c909a..0d47611b1 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -77,7 +77,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([ const applicationConsole = { name: 'kubernetes.applications.application.console', - url: '/:pod/console', + url: '/:pod/:container/console', views: { 'content@': { component: 'kubernetesApplicationConsoleView', @@ -87,7 +87,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([ const applicationLogs = { name: 'kubernetes.applications.application.logs', - url: '/:pod/logs', + url: '/:pod/:container/logs', views: { 'content@': { component: 'kubernetesApplicationLogsView', diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html similarity index 88% rename from app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html rename to app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html index 0b5ba805a..b9a86574f 100644 --- a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html +++ b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.html @@ -65,10 +65,17 @@ - + + Pod + + + + + + Image - - + + @@ -101,9 +108,8 @@ pagination-id="$ctrl.tableKey" > {{ item.Name }} - {{ image }}
+ {{ item.PodName }} + {{ item.Image }} {{ item.Status }} @@ -117,8 +123,8 @@ {{ item.CreationDate | getisodate }} - Logs - + Logs + Console diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js similarity index 59% rename from app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js rename to app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js index faa7df25f..6d8b5ca99 100644 --- a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js +++ b/app/kubernetes/components/datatables/application/containers-datatable/containersDatatable.js @@ -1,5 +1,5 @@ -angular.module('portainer.kubernetes').component('kubernetesPodsDatatable', { - templateUrl: './podsDatatable.html', +angular.module('portainer.kubernetes').component('kubernetesContainersDatatable', { + templateUrl: './containersDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index f1efdf899..5d0ece02c 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -147,7 +147,9 @@ {{ item.ResourcePool }} - {{ item.Image }} + {{ item.Image }} + {{ item.Containers.length - 1 }} {{ item.ApplicationType | kubernetesApplicationTypeText }} Replicated diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html index 4ab919bd1..f6f2e66ea 100644 --- a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html @@ -118,7 +118,9 @@ {{ item.ResourcePool }} - {{ item.Image }} + {{ item.Image }} + {{ item.Containers.length - 1 }} {{ item.CPU | kubernetesApplicationCPUValue }} {{ item.Memory | humansize }} diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html index c08cd865b..2ba9f703b 100644 --- a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html @@ -107,7 +107,9 @@ external {{ item.StackName }} - {{ item.Image }} + {{ item.Image }} + {{ item.Containers.length - 1 }} {{ item.CPU | kubernetesApplicationCPUValue }} {{ item.Memory | humansize }} diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 4fb313acc..ff2ef43b0 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -50,6 +50,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) { class KubernetesApplicationConverter { static applicationCommon(res, data, service, ingresses) { + const containers = _.without(_.concat(data.spec.template.spec.containers, data.spec.template.spec.initContainers), undefined); res.Id = data.metadata.uid; res.Name = data.metadata.name; res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; @@ -57,16 +58,16 @@ class KubernetesApplicationConverter { res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : ''; res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name; res.ResourcePool = data.metadata.namespace; - res.Image = data.spec.template.spec.containers[0].image; + res.Image = containers[0].image; res.CreationDate = data.metadata.creationTimestamp; - res.Pods = data.Pods; - res.Env = data.spec.template.spec.containers[0].env; + res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined); + const limits = { Cpu: 0, Memory: 0, }; res.Limits = _.reduce( - data.spec.template.spec.containers, + containers, (acc, item) => { if (item.resources.limits && item.resources.limits.cpu) { acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.limits.cpu); @@ -84,7 +85,7 @@ class KubernetesApplicationConverter { Memory: 0, }; res.Requests = _.reduce( - data.spec.template.spec.containers, + containers, (acc, item) => { if (item.resources.requests && item.resources.requests.cpu) { acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.requests.cpu); @@ -109,7 +110,7 @@ class KubernetesApplicationConverter { } } - const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); + const portsRefs = _.concat(..._.map(containers, (container) => container.ports)); const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs); const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingresses, service.metadata.name); _.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port))); @@ -147,7 +148,8 @@ class KubernetesApplicationConverter { const persistedFolders = _.filter(res.Volumes, (volume) => volume.persistentVolumeClaim || volume.hostPath); res.PersistedFolders = _.map(persistedFolders, (volume) => { - const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + const volumeMounts = _.uniq(_.flatMap(_.map(containers, 'volumeMounts')), 'name'); + const matchingVolumeMount = _.find(volumeMounts, { name: volume.name }); if (matchingVolumeMount) { const persistedFolder = new KubernetesApplicationPersistedFolder(); @@ -169,7 +171,7 @@ class KubernetesApplicationConverter { data.spec.template.spec.volumes, (acc, volume) => { if (volume.configMap || volume.secret) { - const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + const matchingVolumeMount = _.find(_.flatMap(_.map(containers, 'volumeMounts')), { name: volume.name }); if (matchingVolumeMount) { let items = []; @@ -262,6 +264,7 @@ class KubernetesApplicationConverter { res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount); res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); + res.Containers = app.Containers; const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length; if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js index a38126d5e..dad947923 100644 --- a/app/kubernetes/converters/deployment.js +++ b/app/kubernetes/converters/deployment.js @@ -29,6 +29,7 @@ class KubernetesDeploymentConverter { res.CpuLimit = formValues.CpuLimit; res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + res.Containers = formValues.Containers; KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); return res; diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index f14d493db..7a39186ca 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -28,6 +28,43 @@ class KubernetesApplicationHelper { return _.filter(pods, { Labels: app.spec.selector.matchLabels }); } + static associateContainerPersistedFoldersAndConfigurations(app, containers) { + _.forEach(containers, (container) => { + container.PersistedFolders = _.without( + _.map(app.PersistedFolders, (pf) => { + if (pf.MountPath && _.includes(_.map(container.VolumeMounts, 'mountPath'), pf.MountPath)) { + return pf; + } + }), + undefined + ); + + container.ConfigurationVolumes = _.without( + _.map(app.ConfigurationVolumes, (cv) => { + if (cv.rootMountPath && _.includes(_.map(container.VolumeMounts, 'mountPath'), cv.rootMountPath)) { + return cv; + } + }), + undefined + ); + }); + } + + static associateContainersAndApplication(app) { + if (!app.Pods) { + return []; + } + const containers = app.Pods[0].Containers; + KubernetesApplicationHelper.associateContainerPersistedFoldersAndConfigurations(app, containers); + return containers; + } + + static associateAllContainersAndApplication(app) { + const containers = _.flatMap(_.map(app.Pods, 'Containers')); + KubernetesApplicationHelper.associateContainerPersistedFoldersAndConfigurations(app, containers); + return containers; + } + static portMappingsFromApplications(applications) { const res = _.reduce( applications, diff --git a/app/kubernetes/helpers/resourceReservationHelper.js b/app/kubernetes/helpers/resourceReservationHelper.js index df24ee494..5df674fc5 100644 --- a/app/kubernetes/helpers/resourceReservationHelper.js +++ b/app/kubernetes/helpers/resourceReservationHelper.js @@ -9,13 +9,13 @@ class KubernetesResourceReservationHelper { return _.reduce( containers, (acc, container) => { - if (container.resources && container.resources.requests) { - if (container.resources.requests.memory) { - acc.Memory += filesizeParser(container.resources.requests.memory, { base: 10 }); + if (container.Requests) { + if (container.Requests.memory) { + acc.Memory += filesizeParser(container.Requests.memory, { base: 10 }); } - if (container.resources.requests.cpu) { - acc.CPU += KubernetesResourceReservationHelper.parseCPU(container.resources.requests.cpu); + if (container.Requests.cpu) { + acc.CPU += KubernetesResourceReservationHelper.parseCPU(container.Requests.cpu); } } diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 8b658141a..6d789c10b 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -21,6 +21,7 @@ const _KubernetesApplicationFormValues = Object.freeze({ PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, Configurations: [], // KubernetesApplicationConfigurationFormValue list + Containers: [], AutoScaler: {}, OriginalIngresses: undefined, }); diff --git a/app/kubernetes/models/application/models/index.js b/app/kubernetes/models/application/models/index.js index dbd7b8fbb..35520acfd 100644 --- a/app/kubernetes/models/application/models/index.js +++ b/app/kubernetes/models/application/models/index.js @@ -13,6 +13,7 @@ const _KubernetesApplication = Object.freeze({ Image: '', CreationDate: 0, Pods: [], + Containers: [], Limits: {}, ServiceType: '', ServiceId: '', diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js index 121f312bc..3aae29482 100644 --- a/app/kubernetes/pod/converter.js +++ b/app/kubernetes/pod/converter.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity } from 'Kubernetes/pod/models'; +import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes } from 'Kubernetes/pod/models'; function computeStatus(statuses) { const containerStatuses = _.map(statuses, 'state'); @@ -13,6 +13,21 @@ function computeStatus(statuses) { return 'Running'; } +function computeContainerStatus(statuses, name) { + const status = _.find(statuses, { name: name }); + if (!status) { + return 'Terminated'; + } + const state = status.state; + if (state.waiting) { + return 'Waiting'; + } + if (!state.running) { + return 'Terminated'; + } + return 'Running'; +} + function computeAffinity(affinity) { const res = new KubernetesPodAffinity(); if (affinity) { @@ -33,6 +48,44 @@ function computeTolerations(tolerations) { }); } +function computeContainers(data) { + const containers = data.spec.containers; + const initContainers = data.spec.initContainers; + + return _.concat( + _.map(containers, (item) => { + const res = new KubernetesPodContainer(); + res.Type = KubernetesPodContainerTypes.APP; + res.PodName = data.metadata.name; + res.Name = item.name; + res.Image = item.image; + res.Node = data.spec.nodeName; + res.CreationDate = data.status.startTime; + res.Status = computeContainerStatus(data.status.containerStatuses, item.name); + res.Limits = item.resources.limits; + res.Requests = item.resources.requests; + res.VolumeMounts = item.volumeMounts; + res.Env = item.env; + return res; + }), + _.map(initContainers, (item) => { + const res = new KubernetesPodContainer(); + res.Type = KubernetesPodContainerTypes.INIT; + res.PodName = data.metadata.name; + res.Name = item.name; + res.Image = item.image; + res.Node = data.spec.nodeName; + res.CreationDate = data.status.startTime; + res.Status = computeContainerStatus(data.status.containerStatuses, item.name); + res.Limits = item.resources.limits; + res.Requests = item.resources.requests; + res.VolumeMounts = item.volumeMounts; + res.Env = item.env; + return res; + }) + ); +} + export default class KubernetesPodConverter { static apiToModel(data) { const res = new KubernetesPod(); @@ -44,7 +97,7 @@ export default class KubernetesPodConverter { res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount'); res.Node = data.spec.nodeName; res.CreationDate = data.status.startTime; - res.Containers = data.spec.containers; + res.Containers = computeContainers(data); res.Labels = data.metadata.labels; res.Affinity = computeAffinity(data.spec.affinity); res.NodeSelector = data.spec.nodeSelector; diff --git a/app/kubernetes/pod/models/index.js b/app/kubernetes/pod/models/index.js index cf76e0386..3cf9488b7 100644 --- a/app/kubernetes/pod/models/index.js +++ b/app/kubernetes/pod/models/index.js @@ -40,3 +40,30 @@ export class KubernetesPodToleration { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodToleration))); } } + +const _KubernetesPodContainer = Object.freeze({ + Type: 0, + PodName: '', + Name: '', + Image: '', + Node: '', + CreationDate: '', + Status: '', + Limits: {}, + Requests: {}, + VolumeMounts: {}, + ConfigurationVolumes: [], + PersistedFolders: [], + Env: [], +}); + +export class KubernetesPodContainer { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodContainer))); + } +} + +export const KubernetesPodContainerTypes = { + INIT: 1, + APP: 2, +}; diff --git a/app/kubernetes/pod/service.js b/app/kubernetes/pod/service.js index 90bca7be5..c2de34a2e 100644 --- a/app/kubernetes/pod/service.js +++ b/app/kubernetes/pod/service.js @@ -36,11 +36,15 @@ class KubernetesPodService { * * @param {string} namespace * @param {string} podName + * @param {string} containerName */ - async logsAsync(namespace, podName) { + async logsAsync(namespace, podName, containerName) { try { const params = new KubernetesCommonParams(); params.id = podName; + if (containerName) { + params.container = containerName; + } const data = await this.KubernetesPods(namespace).logs(params).$promise; return data.logs.length === 0 ? [] : data.logs.split('\n'); } catch (err) { @@ -48,8 +52,8 @@ class KubernetesPodService { } } - logs(namespace, podName) { - return this.$async(this.logsAsync, namespace, podName); + logs(namespace, podName, containerName) { + return this.$async(this.logsAsync, namespace, podName, containerName); } /** diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 1afe88de5..27ce3e9d9 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -115,6 +115,7 @@ class KubernetesApplicationService { application.Yaml = rootItem.value.Yaml; application.Raw = rootItem.value.Raw; application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw); + application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; @@ -144,6 +145,7 @@ class KubernetesApplicationService { const service = KubernetesServiceHelper.findApplicationBoundService(services, item); const application = converterFunc(item, service, ingresses); application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); return application; }; diff --git a/app/kubernetes/views/applications/console/console.html b/app/kubernetes/views/applications/console/console.html index 51bb6ea3e..b4fa3a5a1 100644 --- a/app/kubernetes/views/applications/console/console.html +++ b/app/kubernetes/views/applications/console/console.html @@ -3,7 +3,7 @@ {{ ctrl.application.ResourcePool }} > Applications > {{ ctrl.application.Name }} > Pods > - {{ ctrl.podName }} > Console + {{ ctrl.podName }} > Containers > {{ ctrl.containerName }} > Console diff --git a/app/kubernetes/views/applications/console/consoleController.js b/app/kubernetes/views/applications/console/consoleController.js index ccf8e37ed..39310b140 100644 --- a/app/kubernetes/views/applications/console/consoleController.js +++ b/app/kubernetes/views/applications/console/consoleController.js @@ -54,7 +54,7 @@ class KubernetesApplicationConsoleController { endpointId: this.EndpointProvider.endpointID(), namespace: this.application.ResourcePool, podName: this.podName, - containerName: this.application.Pods[0].Containers[0].name, + containerName: this.containerName, command: this.state.command, }; @@ -92,8 +92,10 @@ class KubernetesApplicationConsoleController { const podName = this.$transition$.params().pod; const applicationName = this.$transition$.params().name; const namespace = this.$transition$.params().namespace; + const containerName = this.$transition$.params().container; this.podName = podName; + this.containerName = containerName; try { this.application = await this.KubernetesApplicationService.get(namespace, applicationName); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index f30fa9a95..c6eb5f637 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -54,7 +54,15 @@
- +
@@ -128,7 +136,7 @@
- + add environment variable
@@ -146,6 +154,7 @@ ng-change="ctrl.onChangeEnvironmentName()" ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/" placeholder="foo" + ng-disabled="ctrl.formValues.Containers.length > 1" required />
@@ -171,10 +180,17 @@
value - +
-
+
@@ -194,7 +210,7 @@
- + add configuration
@@ -214,6 +230,7 @@ ng-model="config.SelectedConfiguration" ng-options="c as c.Name for c in ctrl.configurations" ng-change="ctrl.resetConfiguration(index)" + ng-disabled="ctrl.formValues.Containers.length > 1" >
@@ -222,14 +239,20 @@ type="button" ng-if="!config.Overriden" ng-click="ctrl.overrideConfiguration(index)" - ng-disabled="!config.SelectedConfiguration" + ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1" > Override - - +
@@ -266,6 +289,7 @@ ng-model="overridenKey.Path" placeholder="/etc/myapp/conf.d" name="overriden_key_path_{{ index }}_{{ keyIndex }}" + ng-disabled="ctrl.formValues.Containers.length > 1" required ng-change="ctrl.onChangeConfigurationPath()" /> @@ -316,7 +340,12 @@
- + add persisted folder
@@ -331,7 +360,7 @@ name="persisted_folder_path_{{ $index }}" ng-model="persistedFolder.ContainerPath" ng-change="ctrl.onChangePersistedFolderPath()" - ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)" + ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1" placeholder="/data" required /> @@ -341,7 +370,11 @@
@@ -396,7 +429,7 @@ class="form-control" ng-model="persistedFolder.StorageClass" ng-options="storageClass as storageClass.Name for storageClass in ctrl.storageClasses" - ng-disabled="ctrl.state.isEdit" + ng-disabled="ctrl.state.isEdit || ctrl.formValues.Containers.length > 1" >
@@ -409,7 +442,7 @@ ng-model="ctrl.formValues.PersistedFolders[$index].ExistingVolume" ng-options="vol as vol.PersistentVolumeClaim.Name for vol in ctrl.availableVolumes" ng-change="ctrl.onChangeExistingVolumeSelection()" - ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)" + ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1" required > @@ -417,7 +450,7 @@
-
+
@@ -607,7 +640,10 @@
-
+