Merge pull request #4415 from portainer/feat/GH/4011-pods-as-applications

feat(k8s/applications): exposed naked pods as applications
pull/4495/head
Stéphane Busso 4 years ago committed by GitHub
commit 3d9c10adf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -57,7 +57,7 @@
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<th ng-if="!$ctrl.isPod">
<a ng-click="$ctrl.changeOrderBy('PodName')">
Pod
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PodName' && !$ctrl.state.reverseOrder"></i>
@ -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"
>
<td>{{ item.PodName }}</td>
<td ng-if="!$ctrl.isPod">{{ item.PodName }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Image }}</td>
<td

@ -8,5 +8,6 @@ angular.module('portainer.kubernetes').component('kubernetesContainersDatatable'
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
isPod: '<',
},
});

@ -151,11 +151,14 @@
>{{ item.Image }} <span ng-if="item.Containers.length > 1">+ {{ item.Containers.length - 1 }}</span></td
>
<td>{{ item.ApplicationType | kubernetesApplicationTypeText }}</td>
<td>
<td ng-if="item.ApplicationType !== $ctrl.KubernetesApplicationTypes.POD">
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></td
>
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code>
</td>
<td ng-if="item.ApplicationType === $ctrl.KubernetesApplicationTypes.POD">
{{ item.Pods[0].Status }}
</td>
<td>
<span ng-if="item.PublishedPorts.length">
<span>

@ -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();

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

@ -57,6 +57,8 @@ angular
return KubernetesApplicationTypeStrings.DAEMONSET;
case KubernetesApplicationTypes.STATEFULSET:
return KubernetesApplicationTypeStrings.STATEFULSET;
case KubernetesApplicationTypes.POD:
return KubernetesApplicationTypeStrings.POD;
default:
return '-';
}

@ -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) {

@ -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;
}

@ -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];

@ -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));
}
}

@ -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');
}

@ -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({

@ -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: '',
};
}

@ -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);
}

@ -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);

@ -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);

@ -43,16 +43,21 @@
</tr>
<tr>
<td>Status</td>
<td>
<td ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD">
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ ctrl.application.RunningPodsCount }}</code> / <code>{{ ctrl.application.TotalPodsCount }}</code>
</td>
<td ng-if="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD">
{{ ctrl.application.Pods[0].Status }}
</td>
</tr>
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
<td>
<div>Resource reservations</div>
<div class="text-muted small">per instance</div>
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small">
per instance
</div>
</td>
<td>
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
@ -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' }}"
>
</kubernetes-containers-datatable>
</div>

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

@ -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');
}
}

Loading…
Cancel
Save