portainer/portainer

View on GitHub
app/react/docker/services/ListView/ServicesDatatable/columns/schedulingMode/constraint-helper.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { Node } from 'docker-types/generated/1.41';

import { ServiceViewModel } from '@/docker/models/service';

class ConstraintModel {
  op: string;

  value: string;

  key: string;

  constructor(op: string, key: string, value: string) {
    this.op = op;
    this.value = value;
    this.key = key;
  }
}

const patterns = {
  id: {
    nodeId: 'node.id',
    nodeHostname: 'node.hostname',
    nodeRole: 'node.role',
    nodeLabels: 'node.labels.',
    engineLabels: 'engine.labels.',
  },
  op: {
    eq: '==',
    neq: '!=',
  },
} as const;

function matchesConstraint(
  value: string | undefined,
  constraint?: ConstraintModel
) {
  if (
    !constraint ||
    (constraint.op === patterns.op.eq && value === constraint.value) ||
    (constraint.op === patterns.op.neq && value !== constraint.value)
  ) {
    return true;
  }
  return false;
}

function matchesLabel(
  labels: Record<string, string> | undefined,
  constraint?: ConstraintModel
) {
  if (!constraint) {
    return true;
  }
  return Object.entries(labels || {}).some(
    ([key, value]) => key === constraint.key && value === constraint.value
  );
}

function extractValue(constraint: string, op: string) {
  return constraint.split(op).pop()?.trim() || '';
}

function extractCustomLabelKey(
  constraint: string,
  op: string,
  baseLabelKey: string
) {
  return constraint.split(op).shift()?.trim().replace(baseLabelKey, '') || '';
}

interface Constraint {
  nodeId?: ConstraintModel;
  nodeHostname?: ConstraintModel;
  nodeRole?: ConstraintModel;
  nodeLabels?: ConstraintModel;
  engineLabels?: ConstraintModel;
}

function transformConstraints(constraints: Array<string>) {
  const transform: Constraint = {};
  for (let i = 0; i < constraints.length; i++) {
    const constraint = constraints[i];

    let op = '';
    if (constraint.includes(patterns.op.eq)) {
      op = patterns.op.eq;
    } else if (constraint.includes(patterns.op.neq)) {
      op = patterns.op.neq;
    }

    const value = extractValue(constraint, op);
    let key = '';
    switch (true) {
      case constraint.includes(patterns.id.nodeId):
        transform.nodeId = new ConstraintModel(op, key, value);
        break;
      case constraint.includes(patterns.id.nodeHostname):
        transform.nodeHostname = new ConstraintModel(op, key, value);
        break;
      case constraint.includes(patterns.id.nodeRole):
        transform.nodeRole = new ConstraintModel(op, key, value);
        break;
      case constraint.includes(patterns.id.nodeLabels):
        key = extractCustomLabelKey(constraint, op, patterns.id.nodeLabels);
        transform.nodeLabels = new ConstraintModel(op, key, value);
        break;
      case constraint.includes(patterns.id.engineLabels):
        key = extractCustomLabelKey(constraint, op, patterns.id.engineLabels);
        transform.engineLabels = new ConstraintModel(op, key, value);
        break;
      default:
        break;
    }
  }
  return transform;
}

export function matchesServiceConstraints(
  service: ServiceViewModel,
  node: Node
) {
  if (service.Constraints === undefined || service.Constraints.length === 0) {
    return true;
  }
  const constraints = transformConstraints([...service.Constraints]);
  return (
    matchesConstraint(node.ID, constraints.nodeId) &&
    matchesConstraint(node.Description?.Hostname, constraints.nodeHostname) &&
    matchesConstraint(node.Spec?.Role, constraints.nodeRole) &&
    matchesLabel(node.Spec?.Labels, constraints.nodeLabels) &&
    matchesLabel(node.Description?.Engine?.Labels, constraints.engineLabels)
  );
}