mirror of https://github.com/portainer/portainer
				
				
				
			
		
			
				
	
	
		
			381 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			381 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
import _ from 'lodash-es';
 | 
						|
import filesizeParser from 'filesize-parser';
 | 
						|
 | 
						|
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models/appConstants';
 | 
						|
import {
 | 
						|
  KubernetesApplication,
 | 
						|
  KubernetesApplicationConfigurationVolume,
 | 
						|
  KubernetesApplicationPersistedFolder,
 | 
						|
  KubernetesApplicationPort,
 | 
						|
  KubernetesPortainerApplicationNameLabel,
 | 
						|
  KubernetesPortainerApplicationNote,
 | 
						|
  KubernetesPortainerApplicationOwnerLabel,
 | 
						|
  KubernetesPortainerApplicationStackNameLabel,
 | 
						|
  KubernetesPortainerApplicationStackIdLabel,
 | 
						|
  KubernetesPortainerApplicationKindLabel,
 | 
						|
} from 'Kubernetes/models/application/models';
 | 
						|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
 | 
						|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
 | 
						|
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
 | 
						|
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';
 | 
						|
import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper';
 | 
						|
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
 | 
						|
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
 | 
						|
 | 
						|
function _apiPortsToPublishedPorts(pList, pRefs) {
 | 
						|
  const ports = _.map(pList, (item) => {
 | 
						|
    const res = new KubernetesApplicationPort();
 | 
						|
    res.Port = item.port;
 | 
						|
    res.TargetPort = item.targetPort;
 | 
						|
    res.NodePort = item.nodePort;
 | 
						|
    res.Protocol = item.protocol;
 | 
						|
    return res;
 | 
						|
  });
 | 
						|
  _.forEach(ports, (port) => {
 | 
						|
    if (isNaN(port.TargetPort)) {
 | 
						|
      const targetPort = _.find(pRefs, { name: port.TargetPort });
 | 
						|
      if (targetPort) {
 | 
						|
        port.TargetPort = targetPort.containerPort;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
  return ports;
 | 
						|
}
 | 
						|
 | 
						|
class KubernetesApplicationConverter {
 | 
						|
  static applicationCommon(res, data, pods, service, ingresses) {
 | 
						|
    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.Metadata = data.metadata;
 | 
						|
    res.ApplicationType = data.kind;
 | 
						|
    res.Labels = data.metadata.labels || {};
 | 
						|
 | 
						|
    if (data.metadata.labels) {
 | 
						|
      const { labels } = data.metadata;
 | 
						|
      res.StackId = labels[KubernetesPortainerApplicationStackIdLabel] ? parseInt(labels[KubernetesPortainerApplicationStackIdLabel], 10) : null;
 | 
						|
      res.StackName = labels[KubernetesPortainerApplicationStackNameLabel] || '';
 | 
						|
      res.ApplicationKind = labels[KubernetesPortainerApplicationKindLabel] || '';
 | 
						|
      res.ApplicationOwner = labels[KubernetesPortainerApplicationOwnerLabel] || '';
 | 
						|
      res.ApplicationName = labels[KubernetesPortainerApplicationNameLabel] || res.Name;
 | 
						|
    }
 | 
						|
 | 
						|
    res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
 | 
						|
    res.ResourcePool = data.metadata.namespace;
 | 
						|
    if (containers.length) {
 | 
						|
      res.Image = containers[0].image;
 | 
						|
    }
 | 
						|
    if (data.spec.template && data.spec.template.spec && data.spec.template.spec.imagePullSecrets && data.spec.template.spec.imagePullSecrets.length) {
 | 
						|
      res.RegistryId = parseInt(data.spec.template.spec.imagePullSecrets[0].name.replace('registry-', ''), 10);
 | 
						|
    }
 | 
						|
    res.CreationDate = data.metadata.creationTimestamp;
 | 
						|
    res.Env = _.without(_.flatMap(_.map(containers, 'env')), undefined);
 | 
						|
    res.Pods = data.spec.selector ? KubernetesApplicationHelper.associatePodsAndApplication(pods, data.spec.selector) : [data];
 | 
						|
 | 
						|
    const limits = {
 | 
						|
      Cpu: 0,
 | 
						|
      Memory: 0,
 | 
						|
    };
 | 
						|
    res.Limits = _.reduce(
 | 
						|
      containers,
 | 
						|
      (acc, item) => {
 | 
						|
        if (item.resources.limits && item.resources.limits.cpu) {
 | 
						|
          acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.limits.cpu);
 | 
						|
        }
 | 
						|
        if (item.resources.limits && item.resources.limits.memory) {
 | 
						|
          acc.Memory += filesizeParser(item.resources.limits.memory, { base: 10 });
 | 
						|
        }
 | 
						|
        return acc;
 | 
						|
      },
 | 
						|
      limits
 | 
						|
    );
 | 
						|
 | 
						|
    const requests = {
 | 
						|
      Cpu: 0,
 | 
						|
      Memory: 0,
 | 
						|
    };
 | 
						|
    res.Requests = _.reduce(
 | 
						|
      containers,
 | 
						|
      (acc, item) => {
 | 
						|
        if (item.resources.requests && item.resources.requests.cpu) {
 | 
						|
          acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.requests.cpu);
 | 
						|
        }
 | 
						|
        if (item.resources.requests && item.resources.requests.memory) {
 | 
						|
          acc.Memory += filesizeParser(item.resources.requests.memory, { base: 10 });
 | 
						|
        }
 | 
						|
        return acc;
 | 
						|
      },
 | 
						|
      requests
 | 
						|
    );
 | 
						|
 | 
						|
    if (service) {
 | 
						|
      const serviceType = service.spec.type;
 | 
						|
      res.ServiceType = serviceType;
 | 
						|
      res.ServiceId = service.metadata.uid;
 | 
						|
      res.ServiceName = service.metadata.name;
 | 
						|
      res.ClusterIp = service.spec.clusterIP;
 | 
						|
      res.ExternalIp = service.spec.externalIP;
 | 
						|
 | 
						|
      if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
 | 
						|
        if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) {
 | 
						|
          res.LoadBalancerIPAddress = service.status.loadBalancer.ingress[0].ip || service.status.loadBalancer.ingress[0].hostname;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      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)));
 | 
						|
      res.PublishedPorts = ports;
 | 
						|
    }
 | 
						|
 | 
						|
    if (data.spec.template) {
 | 
						|
      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
 | 
						|
    // instead of referencing the PVC the "good way" using spec.template.spec.volumes array
 | 
						|
    // Basically it creates an "in-memory" reference for the PVC, as if it was saved in
 | 
						|
    // spec.template.spec.volumes and retrieved from here.
 | 
						|
    //
 | 
						|
    // FIX FOR SFS ONLY ; as far as we know it's not possible to do this with DEPLOYMENTS/DAEMONSETS
 | 
						|
    //
 | 
						|
    // This may lead to destructing behaviours when we will allow external apps to be edited.
 | 
						|
    // E.G. if we try to generate the formValues and patch the app, SFS reference will be created under
 | 
						|
    // spec.template.spec.volumes and not be referenced directly inside spec.template.spec.containers[0].volumeMounts
 | 
						|
    // As we preserve original SFS name and try to build around it, it SHOULD be fine, but we definitely need to test this
 | 
						|
    // before allowing external apps modification
 | 
						|
    if (data.spec.volumeClaimTemplates) {
 | 
						|
      const vcTemplates = _.map(data.spec.volumeClaimTemplates, (vc) => {
 | 
						|
        return {
 | 
						|
          name: vc.metadata.name,
 | 
						|
          persistentVolumeClaim: { claimName: vc.metadata.name },
 | 
						|
        };
 | 
						|
      });
 | 
						|
      const inexistingPVC = _.filter(vcTemplates, (vc) => {
 | 
						|
        return !_.find(res.Volumes, { persistentVolumeClaim: { claimName: vc.persistentVolumeClaim.claimName } });
 | 
						|
      });
 | 
						|
      res.Volumes = _.concat(res.Volumes, inexistingPVC);
 | 
						|
    }
 | 
						|
 | 
						|
    const persistedFolders = _.filter(res.Volumes, (volume) => volume.persistentVolumeClaim || volume.hostPath);
 | 
						|
 | 
						|
    res.PersistedFolders = _.map(persistedFolders, (volume) => {
 | 
						|
      const volumeMounts = _.uniq(_.flatMap(_.map(containers, 'volumeMounts')), 'name');
 | 
						|
      const matchingVolumeMount = _.find(volumeMounts, { name: volume.name });
 | 
						|
 | 
						|
      if (matchingVolumeMount) {
 | 
						|
        const persistedFolder = new KubernetesApplicationPersistedFolder();
 | 
						|
        persistedFolder.MountPath = matchingVolumeMount.mountPath;
 | 
						|
 | 
						|
        if (volume.persistentVolumeClaim) {
 | 
						|
          persistedFolder.persistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
 | 
						|
        } else {
 | 
						|
          persistedFolder.HostPath = volume.hostPath.path;
 | 
						|
        }
 | 
						|
 | 
						|
        return persistedFolder;
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    res.PersistedFolders = _.without(res.PersistedFolders, undefined);
 | 
						|
 | 
						|
    res.ConfigurationVolumes = _.reduce(
 | 
						|
      res.Volumes,
 | 
						|
      (acc, volume) => {
 | 
						|
        if (volume.configMap || volume.secret) {
 | 
						|
          const matchingVolumeMount = _.find(_.flatMap(_.map(containers, 'volumeMounts')), { name: volume.name });
 | 
						|
 | 
						|
          if (matchingVolumeMount) {
 | 
						|
            let items = [];
 | 
						|
            let configurationName = '';
 | 
						|
 | 
						|
            if (volume.configMap) {
 | 
						|
              items = volume.configMap.items;
 | 
						|
              configurationName = volume.configMap.name;
 | 
						|
            } else {
 | 
						|
              items = volume.secret.items;
 | 
						|
              configurationName = volume.secret.secretName;
 | 
						|
            }
 | 
						|
 | 
						|
            if (!items) {
 | 
						|
              const configurationVolume = new KubernetesApplicationConfigurationVolume();
 | 
						|
              configurationVolume.fileMountPath = matchingVolumeMount.mountPath;
 | 
						|
              configurationVolume.rootMountPath = matchingVolumeMount.mountPath;
 | 
						|
              configurationVolume.configurationName = configurationName;
 | 
						|
              configurationVolume.configurationType = volume.configMap ? KubernetesConfigurationKinds.CONFIGMAP : KubernetesConfigurationKinds.SECRET;
 | 
						|
 | 
						|
              acc.push(configurationVolume);
 | 
						|
            } else {
 | 
						|
              _.forEach(items, (item) => {
 | 
						|
                const configurationVolume = new KubernetesApplicationConfigurationVolume();
 | 
						|
                configurationVolume.fileMountPath = matchingVolumeMount.mountPath + '/' + item.path;
 | 
						|
                configurationVolume.rootMountPath = matchingVolumeMount.mountPath;
 | 
						|
                configurationVolume.configurationKey = item.key;
 | 
						|
                configurationVolume.configurationName = configurationName;
 | 
						|
                configurationVolume.configurationType = volume.configMap ? KubernetesConfigurationKinds.CONFIGMAP : KubernetesConfigurationKinds.SECRET;
 | 
						|
 | 
						|
                acc.push(configurationVolume);
 | 
						|
              });
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        return acc;
 | 
						|
      },
 | 
						|
      []
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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);
 | 
						|
    res.ApplicationType = KubernetesApplicationTypes.Deployment;
 | 
						|
    res.DeploymentType = KubernetesApplicationDeploymentTypes.Replicated;
 | 
						|
    res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Shared;
 | 
						|
    res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0;
 | 
						|
    res.TotalPodsCount = data.spec.replicas;
 | 
						|
    return res;
 | 
						|
  }
 | 
						|
 | 
						|
  static apiDaemonSetToApplication(data, pods, service, ingresses) {
 | 
						|
    const res = new KubernetesApplication();
 | 
						|
    KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
 | 
						|
    res.ApplicationType = KubernetesApplicationTypes.DaemonSet;
 | 
						|
    res.DeploymentType = KubernetesApplicationDeploymentTypes.Global;
 | 
						|
    res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Shared;
 | 
						|
    res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0;
 | 
						|
    res.TotalPodsCount = data.status.desiredNumberScheduled;
 | 
						|
    return res;
 | 
						|
  }
 | 
						|
 | 
						|
  static apiStatefulSetToapplication(data, pods, service, ingresses) {
 | 
						|
    const res = new KubernetesApplication();
 | 
						|
    KubernetesApplicationConverter.applicationCommon(res, data, pods, service, ingresses);
 | 
						|
    res.ApplicationType = KubernetesApplicationTypes.StatefulSet;
 | 
						|
    res.DeploymentType = KubernetesApplicationDeploymentTypes.Replicated;
 | 
						|
    res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.Isolated;
 | 
						|
    res.RunningPodsCount = data.status.readyReplicas || 0;
 | 
						|
    res.TotalPodsCount = data.spec.replicas;
 | 
						|
    res.HeadlessServiceName = data.spec.serviceName;
 | 
						|
    return res;
 | 
						|
  }
 | 
						|
 | 
						|
  static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels, ingresses) {
 | 
						|
    const res = new KubernetesApplicationFormValues();
 | 
						|
    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;
 | 
						|
    res.ApplicationOwner = app.ApplicationOwner;
 | 
						|
    res.ImageModel.Image = app.Image;
 | 
						|
    res.ImageModel.Registry.Id = app.RegistryId;
 | 
						|
    res.ReplicaCount = app.TotalPodsCount;
 | 
						|
    res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory);
 | 
						|
    res.CpuLimit = app.Limits.Cpu;
 | 
						|
    res.DeploymentType = app.DeploymentType;
 | 
						|
    res.DataAccessPolicy = app.DataAccessPolicy;
 | 
						|
    res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env);
 | 
						|
    res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
 | 
						|
    res.Secrets = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(
 | 
						|
      app.Env,
 | 
						|
      app.ConfigurationVolumes,
 | 
						|
      configurations,
 | 
						|
      KubernetesConfigurationKinds.SECRET
 | 
						|
    );
 | 
						|
    res.ConfigMaps = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(
 | 
						|
      app.Env,
 | 
						|
      app.ConfigurationVolumes,
 | 
						|
      configurations,
 | 
						|
      KubernetesConfigurationKinds.CONFIGMAP
 | 
						|
    );
 | 
						|
    res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount);
 | 
						|
    res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts, ingresses);
 | 
						|
    res.Containers = app.Containers;
 | 
						|
 | 
						|
    res.PublishingType = app.ServiceType;
 | 
						|
 | 
						|
    if (app.Pods && app.Pods.length) {
 | 
						|
      KubernetesApplicationHelper.generatePlacementsFormValuesFromAffinity(res, app.Pods[0].Affinity);
 | 
						|
    }
 | 
						|
 | 
						|
    return res;
 | 
						|
  }
 | 
						|
 | 
						|
  static applicationFormValuesToApplication(formValues) {
 | 
						|
    formValues.ApplicationOwner = KubernetesCommonHelper.ownerToLabel(formValues.ApplicationOwner);
 | 
						|
 | 
						|
    const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues);
 | 
						|
    const rwx = KubernetesApplicationHelper.hasRWX(claims);
 | 
						|
 | 
						|
    const deployment =
 | 
						|
      (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Replicated &&
 | 
						|
        (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Shared))) ||
 | 
						|
      formValues.ApplicationType === KubernetesApplicationTypes.Deployment;
 | 
						|
 | 
						|
    const statefulSet =
 | 
						|
      (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Replicated &&
 | 
						|
        claims.length > 0 &&
 | 
						|
        formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.Isolated) ||
 | 
						|
      formValues.ApplicationType === KubernetesApplicationTypes.StatefulSet;
 | 
						|
 | 
						|
    const daemonSet =
 | 
						|
      (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.Global &&
 | 
						|
        (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);
 | 
						|
    } else if (statefulSet) {
 | 
						|
      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');
 | 
						|
    }
 | 
						|
    app.ApplicationType = formValues.ApplicationType;
 | 
						|
 | 
						|
    let headlessService;
 | 
						|
    if (statefulSet) {
 | 
						|
      headlessService = KubernetesServiceConverter.applicationFormValuesToHeadlessService(formValues);
 | 
						|
    }
 | 
						|
 | 
						|
    let service = KubernetesServiceConverter.applicationFormValuesToService(formValues);
 | 
						|
    if (!service.Ports.length) {
 | 
						|
      service = undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    let services = KubernetesServiceConverter.applicationFormValuesToServices(formValues);
 | 
						|
 | 
						|
    return [app, headlessService, services, service, claims];
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export default KubernetesApplicationConverter;
 |