18F/federalist

View on GitHub
api/utils/cfApiClient.js

Summary

Maintainability
F
6 days
Test Coverage
A
100%
const CloudFoundryAuthClient = require('./cfAuthClient');
const HttpClient = require('./httpClient');

function filterEntity(res, name, field = 'name') {
  const errMsg = `Not found: Entity @${field} = ${name}`;
  const filtered = res.resources.filter(item => item.entity[field] === name);
  if (filtered.length === 0) {
    const error = new Error(errMsg);
    error.name = name;
    throw error;
  }
  return filtered;
}

function findEntity(res, name, field) {
  return filterEntity(res, name, field)[0];
}

function findS3ServicePlan(res, name, s3ServicePlanId) {
  const filtered = filterEntity(res, name);
  // TODO: remove dependency on s3ServicePlanId and remove this block
  if (name === 'basic-public') {
    const servicePlan = filtered.find(f => f.entity.unique_id === s3ServicePlanId);
    if (!servicePlan) {
      const error = new Error(`Not found: @basic-public service plan = (${s3ServicePlanId})`);
      error.name = name;
      throw error;
    }
    return servicePlan;
  }
  return filtered[0];
}

function firstEntity(res, name) {
  if (res.resources.length === 0) {
    const error = new Error('Not found');
    error.name = name;
    throw error;
  }

  return res.resources[0];
}

function objToQueryParams(obj) {
  const qs = new URLSearchParams();
  Object
    .entries(obj)
    .forEach(([key, value]) => {
      qs.append(key, value);
    });
  return qs;
}

class CloudFoundryAPIClient {
  constructor({ apiUrl, authClient } = {}) {
    this.authClient = authClient ?? new CloudFoundryAuthClient();
    this.httpClient = new HttpClient(apiUrl ?? process.env.CLOUD_FOUNDRY_API_HOST);
  }

  cancelTask(taskGuid) {
    return this.authRequest('POST', `/v3/tasks/${taskGuid}/actions/cancel`);
  }

  async cancelBuildTask(buildId) {
    const buildTask = await this.fetchBuildTask(buildId);
    if (!buildTask) {
      throw new Error(`There are no tasks for build id: ${buildId}`);
    }
    return this.cancelTask(buildTask.guid);
  }

  fetchBuildTask(buildId) {
    return this.fetchTaskByName(`build-${buildId}`);
  }

  fetchTaskByName(name) {
    return this.fetchTasks({ names: name })
      .then(tasks => tasks.find(task => task.name === name));
  }

  fetchTasks(params) {
    const qs = objToQueryParams(params);

    return this.authRequest('GET', `/v3/tasks?${qs.toString()}`)
      .then(body => body.resources);
  }

  /**
   * @param {Object} params
   * @param {string} params.domains Comma-delimited list of domains
   * @param {string} params.name The CF service name
   * @param {string} params.origin The target origin of the domains
   * @param {string} params.path The target path of the domains
   * @param {string} params.cfCdnSpaceName The name of the cf space to put the domain
   * @param {string} params.cfDomainWithCdnPlanGuid The guuid of the external domain service plan
   * @returns
   */
  async createExternalDomain(params) {
    const {
      domains,
      name,
      origin,
      path,
      cfCdnSpaceName,
      cfDomainWithCdnPlanGuid,
    } = params;

    const spaceGuid = await this.authRequest('GET', `/v3/spaces?names=${cfCdnSpaceName}`)
      .then(res => res.resources[0].guid);

    const body = {
      type: 'managed',
      name,
      relationships: {
        space: {
          data: {
            guid: spaceGuid,
          },
        },
        service_plan: {
          data: {
            guid: cfDomainWithCdnPlanGuid,
          },
        },
      },
      parameters: {
        domains,
        origin,
        path,
      },
    };

    return this.authRequest('POST', '/v3/service_instances', body);
  }

  createRoute(name, domainGuid, spaceGuid) {
    const body = {
      domain_guid: domainGuid,
      space_guid: spaceGuid,
      host: name,
    };

    return this.accessToken().then(token => this.request(
      'POST',
      '/v2/routes',
      token,
      body
    ));
  }

  createS3ServiceInstance(name, serviceName, spaceGuid, s3ServicePlanId) {
    return this.fetchS3ServicePlanGUID(serviceName, s3ServicePlanId)
      .then((servicePlanGuid) => {
        const body = {
          name,
          service_plan_guid: servicePlanGuid,
          space_guid: spaceGuid,
        };

        return this.accessToken().then(token => this.request(
          'POST',
          '/v2/service_instances?accepts_incomplete=true',
          token,
          body
        ));
      });
  }

  createServiceKey(serviceInstanceName, serviceInstanceGuid, keyIdentifier = 'key') {
    const body = {
      name: `${serviceInstanceName}-${keyIdentifier}`,
      service_instance_guid: serviceInstanceGuid,
    };

    return this.accessToken().then(token => this.request(
      'POST',
      '/v2/service_keys',
      token,
      body
    ));
  }

  createSiteBucket(name, spaceGuid, s3ServicePlanId, keyIdentifier = 'key', serviceName = 'basic-vpc') {
    return this.createS3ServiceInstance(name, serviceName, spaceGuid, s3ServicePlanId)
      .then(res => this.createServiceKey(name, res.metadata.guid, keyIdentifier));
  }

  deleteRoute(host) {
    return this.accessToken()
      .then(token => this.request(
        'GET',
        `/v2/routes?q=host:${host}`,
        token
      ))
      .then(res => findEntity(res, host, 'host'))
      .then(entity => this.accessToken()
        .then(token => this.request(
          'DELETE',
          `/v2/routes/${entity.metadata.guid}?recursive=true&async=true`,
          token
        )));
  }

  deleteServiceInstance(name) {
    return this.fetchServiceInstance(name)
      .then(instance => this.accessToken().then(token => this.request(
        'DELETE',
        `/v2/service_instances/${instance.metadata.guid}?accepts_incomplete=true&recursive=true&async=true`,
        token
      ).then(() => ({
        metadata: {
          guid: instance.metadata.guid,
        },
      }))));
  }

  fetchServiceInstance(name) {
    return this.fetchServiceInstances(name)
      .then(res => findEntity(res, name));
  }

  fetchServiceInstanceCredentials(name) {
    return this.fetchServiceInstance(name)
      .then(instance => this.accessToken().then(token => this.request(
        'GET',
        `/v2/service_instances/${instance.metadata.guid}/service_keys`,
        token
      )))
      .then(keys => firstEntity(keys, `${name} Service Keys`))
      .then(key => key.entity.credentials);
  }

  fetchServiceInstances(name = null) {
    const query = name ? `?q=name:${name}` : '';
    const path = `/v2/service_instances${query}`;

    return this.accessToken().then(token => this.request(
      'GET',
      path,
      token
    ));
  }

  fetchServiceKey(name) {
    return this.fetchServiceKeys()
      .then(res => findEntity(res, name))
      .then(key => this.accessToken().then(token => this.request(
        'GET',
        `/v2/service_keys/${key.metadata.guid}`,
        token
      )));
  }

  fetchServiceKeys() {
    return this.accessToken().then(token => this.request(
      'GET',
      '/v2/service_keys',
      token
    ));
  }

  fetchS3ServicePlanGUID(serviceName, s3ServicePlanId) {
    return this.accessToken().then(token => this.request(
      'GET',
      '/v2/service_plans?results-per-page=100',
      token
    )).then(res => findS3ServicePlan(res, serviceName, s3ServicePlanId))
      .then(service => service.metadata.guid);
  }

  // Private methods
  accessToken() {
    return this.authClient.accessToken();
  }

  request(method, path, accessToken, json) {
    return this.httpClient.request({
      method,
      url: path,
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      data: json,
    })
      .then(response => response.data);
  }

  authRequest(method, path, json) {
    return this.accessToken().then(token => this.request(method, path, token, json));
  }
}

CloudFoundryAPIClient.filterEntity = filterEntity;
CloudFoundryAPIClient.findEntity = findEntity;
CloudFoundryAPIClient.findS3ServicePlan = findS3ServicePlan;
CloudFoundryAPIClient.firstEntity = firstEntity;
CloudFoundryAPIClient.objToQueryParams = objToQueryParams;

module.exports = CloudFoundryAPIClient;