Compare commits

..

7 Commits

Author SHA1 Message Date
alice groux
c48709fc3e fix(applications/ports-mapping): load balancer link expand only if the item length > 1 2020-11-24 15:45:21 +01:00
Stéphane Busso
3d9c10adf1 Merge pull request #4415 from portainer/feat/GH/4011-pods-as-applications
feat(k8s/applications): exposed naked pods as applications
2020-11-23 14:57:04 +13:00
Alice Groux
0d20988bef fix(rest): remove timeouts for all REST services (#4385) 2020-11-05 20:49:37 +13:00
Anthony Lapenna
1545a42f08 chore(github): update bug report template
Update documentation URLs
2020-11-03 06:16:18 +13:00
Mulia Nasution
a12f2ee893 Fix typo, change Matamo to Matomo (#4409) 2020-10-28 11:33:23 +13:00
xAt0mZ
174e28b850 feat(k8s/application): app details for pods 2020-10-26 19:48:38 +01:00
xAt0mZ
3da9751c82 feat(k8s/applications): add pod as new application type for apps list 2020-10-26 19:46:44 +01:00
52 changed files with 118 additions and 81 deletions

View File

@@ -16,7 +16,7 @@ already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->
**Bug description**
@@ -27,7 +27,7 @@ A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
**Steps to reproduce the issue:**

View File

@@ -52,7 +52,7 @@ For community support: You can find more information about Portainer's community
## Privacy
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matamo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.

View File

@@ -19,7 +19,6 @@ angular.module('portainer.docker').factory('Container', [
params: { all: 0, action: 'json', filters: '@filters' },
isArray: true,
interceptor: ContainersInterceptor,
timeout: 15000,
},
get: {
method: 'GET',
@@ -48,20 +47,17 @@ angular.module('portainer.docker').factory('Container', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},
stats: {
method: 'GET',
params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500,
ignoreLoadingBar: true,
},
top: {
method: 'GET',
params: { id: '@id', action: 'top' },
timeout: 4500,
ignoreLoadingBar: true,
},
start: {

View File

@@ -16,7 +16,7 @@ angular.module('portainer.docker').factory('Image', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor, timeout: 15000 },
query: { method: 'GET', params: { all: 0, action: 'json' }, isArray: true, interceptor: ImagesInterceptor },
get: { method: 'GET', params: { action: 'json' } },
search: { method: 'GET', params: { action: 'search' } },
history: { method: 'GET', params: { action: 'history' }, isArray: true },

View File

@@ -18,7 +18,6 @@ angular.module('portainer.docker').factory('Network', [
method: 'GET',
isArray: true,
interceptor: NetworksInterceptor,
timeout: 15000,
},
get: {
method: 'GET',

View File

@@ -35,7 +35,6 @@ angular.module('portainer.docker').factory('Service', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,10 +18,9 @@ angular.module('portainer.docker').factory('System', [
info: {
method: 'GET',
params: { action: 'info' },
timeout: 15000,
interceptor: InfoInterceptor,
},
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
version: { method: 'GET', params: { action: 'version' }, interceptor: VersionInterceptor },
events: {
method: 'GET',
params: { action: 'events', since: '@since', until: '@until' },

View File

@@ -17,7 +17,6 @@ angular.module('portainer.docker').factory('Task', [
logs: {
method: 'GET',
params: { id: '@id', action: 'logs' },
timeout: 4500,
ignoreLoadingBar: true,
transformResponse: logsHandler,
},

View File

@@ -18,7 +18,7 @@ angular.module('portainer.docker').factory('Volume', [
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000 },
query: { method: 'GET', interceptor: VolumesInterceptor },
get: { method: 'GET', params: { id: '@id' } },
create: {
method: 'POST',

View File

@@ -12,9 +12,9 @@ angular.module('portainer.integrations.storidge').factory('Storidge', [
{
rebootCluster: { method: 'POST', params: { resource: 'clusters', action: 'reboot' } },
shutdownCluster: { method: 'POST', params: { resource: 'clusters', action: 'shutdown' } },
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, ignoreLoadingBar: true, isArray: true },
getVersion: { method: 'GET', params: { resource: 'clusters', action: 'version' } },
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, timeout: 4500, ignoreLoadingBar: true },
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, ignoreLoadingBar: true },
queryNodes: { method: 'GET', params: { resource: 'nodes' } },
getNode: { method: 'GET', params: { resource: 'nodes', id: '@id' } },

View File

@@ -14,7 +14,6 @@ angular.module('portainer.kubernetes').factory('KubernetesComponentStatus', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScale
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -15,7 +15,6 @@ function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

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

View File

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

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNodes', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

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

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesConfigMaps', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesControllerRevisions',
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDaemonSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesDeployments', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -11,7 +11,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEndpoints', function K
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
}

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesEvents', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -7,7 +7,7 @@ angular.module('portainer.kubernetes').factory('KubernetesHealth', [
API_ENDPOINT_ENDPOINTS + '/:id/kubernetes/healthz',
{},
{
ping: { method: 'GET', timeout: 15000, params: { id: 'id' } },
ping: { method: 'GET', params: { id: 'id' } },
}
);
},

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesNamespaces', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPersistentVolumeClaims
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -18,7 +18,6 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesReplicaSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesSecrets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesServices', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -17,7 +17,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStatefulSets', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

@@ -16,7 +16,6 @@ angular.module('portainer.kubernetes').factory('KubernetesStorage', [
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ class KubernetesApplicationsController {
_.forEach(this.ports, (item) => {
item.Expanded = false;
item.Highlighted = false;
if (item.Name === application.Name) {
if (item.Name === application.Name && item.Ports.length > 1) {
item.Expanded = true;
item.Highlighted = true;
}

View File

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

View File

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

View File

@@ -339,8 +339,8 @@ class KubernetesNodeController {
this.availableEffects = _.values(KubernetesNodeTaintEffects);
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
this.state.viewReady = true;
}

View File

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