cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/features/applications/application.service.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { combineLatest, filter, first, map, pairwise, publishReplay, refCount, startWith, switchMap } from 'rxjs/operators';

import { AppMetadataTypes } from '../../../../cloud-foundry/src/actions/app-metadata.actions';
import {
  GetApplication,
  UpdateApplication,
  UpdateExistingApplication,
} from '../../../../cloud-foundry/src/actions/application.actions';
import { CFAppState } from '../../../../cloud-foundry/src/cf-app-state';
import {
  applicationEntityType,
  domainEntityType,
  organizationEntityType,
  routeEntityType,
  serviceBindingEntityType,
  spaceEntityType,
  stackEntityType,
} from '../../../../cloud-foundry/src/cf-entity-types';
import { APP_GUID, CF_GUID } from '../../../../core/src/shared/entity.tokens';
import { EntityService } from '../../../../store/src/entity-service';
import { ActionState, rootUpdatingKey } from '../../../../store/src/reducers/api-request-reducer/types';
import {
  getCurrentPageRequestInfo,
  PaginationObservables,
} from '../../../../store/src/reducers/pagination-reducer/pagination-reducer.types';
import { endpointEntitiesSelector } from '../../../../store/src/selectors/endpoint.selectors';
import { APIResource, EntityInfo } from '../../../../store/src/types/api.types';
import { PaginationEntityState } from '../../../../store/src/types/pagination.types';
import { IApp, IAppSummary, IDomain, IOrganization, ISpace, IStack } from '../../cf-api.types';
import { cfEntityCatalog } from '../../cf-entity-catalog';
import { createEntityRelationKey } from '../../entity-relations/entity-relations.types';
import { ApplicationStateData, ApplicationStateService } from '../../shared/services/application-state.service';
import { AppStat } from '../../store/types/app-metadata.types';
import {
  ApplicationEnvVarsHelper,
  EnvVarStratosProject,
} from './application/application-tabs-base/tabs/build-tab/application-env-vars.service';
import { getRoute, isTCPRoute } from './routes/routes.helper';

export function createGetApplicationAction(guid: string, endpointGuid: string) {
  return new GetApplication(
    guid,
    endpointGuid, [
    createEntityRelationKey(applicationEntityType, routeEntityType),
    createEntityRelationKey(applicationEntityType, spaceEntityType),
    createEntityRelationKey(applicationEntityType, stackEntityType),
    createEntityRelationKey(applicationEntityType, serviceBindingEntityType),
    createEntityRelationKey(routeEntityType, domainEntityType),
    createEntityRelationKey(spaceEntityType, organizationEntityType),
  ]
  );
}

export interface ApplicationData {
  fetching: boolean;
  app: APIResource<IApp>;
  stack: APIResource<IStack>;
  cf: any;
}

@Injectable()
export class ApplicationService {

  public entityService: EntityService<APIResource<IApp>>;
  private appSummaryEntityService: EntityService<IAppSummary>;

  constructor(
    @Inject(CF_GUID) public cfGuid: string,
    @Inject(APP_GUID) public appGuid: string,
    private store: Store<CFAppState>,
    private appStateService: ApplicationStateService,
    private appEnvVarsService: ApplicationEnvVarsHelper,
  ) {
    this.entityService = cfEntityCatalog.application.store.getEntityService(
      appGuid,
      cfGuid,
      {
        includeRelations: createGetApplicationAction(appGuid, cfGuid).includeRelations,
        populateMissing: true
      }
    );
    this.appSummaryEntityService = cfEntityCatalog.appSummary.store.getEntityService(
      appGuid,
      cfGuid
    );

    this.constructCoreObservables();
    this.constructAmalgamatedObservables();
    this.constructStatusObservables();
  }

  // NJ: This needs to be cleaned up. So much going on!
  /**
   * An observable based on the core application entity
   */
  isFetchingApp$: Observable<boolean>;
  isUpdatingApp$: Observable<boolean>;

  isDeletingApp$: Observable<boolean>;

  isFetchingEnvVars$: Observable<boolean>;
  isUpdatingEnvVars$: Observable<boolean>;
  isFetchingStats$: Observable<boolean>;

  app$: Observable<EntityInfo<APIResource<IApp>>>;
  waitForAppEntity$: Observable<EntityInfo<APIResource<IApp>>>;
  appSummary$: Observable<EntityInfo<IAppSummary>>;
  appStats$: Observable<AppStat[]>;
  private appStatsFetching$: Observable<PaginationEntityState>; // Use isFetchingStats$ which is properly gated
  appEnvVars: PaginationObservables<APIResource>;
  appOrg$: Observable<APIResource<IOrganization>>;
  appSpace$: Observable<APIResource<ISpace>>;

  application$: Observable<ApplicationData>;
  applicationStratProject$: Observable<EnvVarStratosProject>;
  applicationState$: Observable<ApplicationStateData>;
  applicationUrl$: Observable<string>;
  applicationRunning$: Observable<boolean>;
  orgDomains$: Observable<APIResource<IDomain>[]>;

  /**
   * Fetch the current state of the app (given it's instances) as an object ready
   */
  static getApplicationState(
    appStateService: ApplicationStateService,
    app: IApp,
    appGuid: string,
    cfGuid: string): Observable<ApplicationStateData> {
    return cfEntityCatalog.appStats.store.getPaginationMonitor(appGuid, cfGuid).currentPage$.pipe(
      map(appInstancesPages => {
        return appStateService.get(app, appInstancesPages);
      })
    ).pipe(publishReplay(1), refCount());
  }

  private constructCoreObservables() {
    // First set up all the base observables
    this.app$ = this.entityService.waitForEntity$;
    const moreWaiting$ = this.app$.pipe(
      filter(entityInfo => !!(entityInfo.entity && entityInfo.entity.entity && entityInfo.entity.entity.cfGuid)),
      map(entityInfo => entityInfo.entity.entity));
    this.appSpace$ = moreWaiting$.pipe(
      first(),
      switchMap(app => {
        return cfEntityCatalog.space.store.getWithOrganization.getEntityService(
          app.space_guid,
          app.cfGuid,
        ).waitForEntity$.pipe(
          map(entityInfo => entityInfo.entity)
        );
      }),
      publishReplay(1),
      refCount()
    );
    this.appOrg$ = moreWaiting$.pipe(
      first(),
      switchMap(() => this.appSpace$),
      map(space => space.entity.organization),
      filter(org => !!org)
    );

    this.isDeletingApp$ = this.entityService.isDeletingEntity$.pipe(publishReplay(1), refCount());

    this.waitForAppEntity$ = this.entityService.waitForEntity$.pipe(publishReplay(1), refCount());

    this.appSummary$ = this.waitForAppEntity$.pipe(
      switchMap(() => this.appSummaryEntityService.entityObs$),
      publishReplay(1),
      refCount()
    );

    this.appEnvVars = this.appEnvVarsService.createEnvVarsObs(this.appGuid, this.cfGuid);
  }

  public getApplicationEnvVarsMonitor() {
    return cfEntityCatalog.appEnvVar.store.getEntityMonitor(
      this.appGuid
    );
  }

  private constructAmalgamatedObservables() {
    // Assign/Amalgamate them to public properties (with mangling if required)
    const appStats = cfEntityCatalog.appStats.store.getPaginationService(this.appGuid, this.cfGuid);
    // This will fail to fetch the app stats if the current app is not running but we're
    // willing to do this to speed up the initial fetch for a running application.
    this.appStats$ = appStats.entities$;

    this.appStatsFetching$ = appStats.pagination$.pipe(publishReplay(1), refCount());

    this.application$ = this.waitForAppEntity$.pipe(
      combineLatest(this.store.select(endpointEntitiesSelector)),
      filter(([entityInfo]) => {
        return !!entityInfo && !!entityInfo.entity && !!entityInfo.entity.entity && !!entityInfo.entity.entity.cfGuid;
      }),
      map(([{ entity, entityRequestInfo }, endpoints]): ApplicationData => {
        return {
          fetching: entityRequestInfo.fetching,
          app: entity,
          stack: entity.entity.stack,
          cf: endpoints[entity.entity.cfGuid],
        };
      }), publishReplay(1), refCount());

    this.applicationState$ = this.waitForAppEntity$.pipe(
      combineLatest(this.appStats$.pipe(startWith(null))),
      map(([appInfo, appStatsArray]: [EntityInfo, AppStat[]]) => {
        return this.appStateService.get(appInfo.entity.entity, appStatsArray || null);
      }), publishReplay(1), refCount());

    this.applicationStratProject$ = this.appEnvVars.entities$.pipe(map(applicationEnvVars => {
      return this.appEnvVarsService.FetchStratosProject(applicationEnvVars[0].entity);
    }), publishReplay(1), refCount());

    this.applicationRunning$ = this.application$.pipe(
      map(app => app ? app.app.entity.state === 'STARTED' : false)
    );

    // In an ideal world we'd get domains inline with the application, however the inline path from app to org domains exceeds max cf depth
    // of 2 (app --> space --> org --> domains).
    this.orgDomains$ = this.appOrg$.pipe(
      switchMap(org =>
        cfEntityCatalog.domain.store.getOrganizationDomains.getPaginationService(org.metadata.guid, this.cfGuid).entities$
      ),
      publishReplay(1),
      refCount()
    );

  }

  private constructStatusObservables() {
    this.isFetchingApp$ = this.entityService.isFetchingEntity$;

    this.isUpdatingApp$ = this.entityService.entityObs$.pipe(map(a => {
      const updatingRoot = a.entityRequestInfo.updating[rootUpdatingKey] || { busy: false };
      const updatingSection = a.entityRequestInfo.updating[UpdateExistingApplication.updateKey] || { busy: false };
      return !!updatingRoot.busy || !!updatingSection.busy;
    }));

    this.isFetchingEnvVars$ = this.appEnvVars.pagination$.pipe(
      map(ev => getCurrentPageRequestInfo(ev).busy),
      startWith(false),
      publishReplay(1),
      refCount());

    this.isUpdatingEnvVars$ = this.appEnvVars.pagination$.pipe(map(
      ev => !!(getCurrentPageRequestInfo(ev).busy && ev.ids[ev.currentPage])
    ), startWith(false), publishReplay(1), refCount());

    this.isFetchingStats$ = this.appStatsFetching$.pipe(map(
      appStats => appStats ? getCurrentPageRequestInfo(appStats).busy : false
    ), startWith(false), publishReplay(1), refCount());

    this.applicationUrl$ = this.appSummaryEntityService.entityObs$.pipe(
      map(({ entity }) => entity),
      filter(app => !!app),
      map(applicationSummary => {
        const routes = applicationSummary.routes ? applicationSummary.routes : [];
        const nonTCPRoutes = routes.filter(p => p && !isTCPRoute(p.port));
        return nonTCPRoutes[0] || null;
      }),
      map(entRoute => !!entRoute && !!entRoute && !!entRoute.domain ?
        getRoute(entRoute.port, entRoute.host, entRoute.path, true, false, entRoute.domain.name) :
        null
      )
    );
  }

  isEntityComplete(value, requestInfo: { fetching: boolean, }): boolean {
    if (requestInfo) {
      return !requestInfo.fetching;
    } else {
      return !!value;
    }
  }

  /*
  * Update an application
  */
  updateApplication(
    updatedApplication: UpdateApplication,
    updateEntities?: AppMetadataTypes[],
    existingApplication?: IApp): Observable<ActionState> {
    return cfEntityCatalog.application.api.update<ActionState>(
      this.appGuid,
      this.cfGuid,
      { ...updatedApplication },
      existingApplication,
      updateEntities
    ).pipe(
      pairwise(),
      filter(([oldS, newS]) => oldS.busy && !newS.busy),
      map(([, newS]) => newS),
      first()
    );
  }
}