diff --git a/butler/src/app/v2/api/deployments/repository/component.repository.ts b/butler/src/app/v2/api/deployments/repository/component.repository.ts index 87bfeb71a8..ea8beea7db 100644 --- a/butler/src/app/v2/api/deployments/repository/component.repository.ts +++ b/butler/src/app/v2/api/deployments/repository/component.repository.ts @@ -31,6 +31,17 @@ export class ComponentsRepositoryV2 extends Repository { .getMany() } + public async findHealthyActiveComponents(cdConfigurationId: string): Promise { + // WARNING: ALWAYS RETURN COMPONENT WITH ITS DEPLOYMENT + return this.createQueryBuilder('v2components') + .leftJoinAndSelect('v2components.deployment', 'deployment') + .where('deployment.current = true') + .andWhere('deployment.healthy = true') + .andWhere('deployment.cd_configuration_id = :cdConfigurationId', { cdConfigurationId }) + .orderBy('deployment.created_at', 'DESC') + .getMany() + } + public async findDefaultActiveComponents(defaultCircleId: string): Promise { // WARNING: ALWAYS RETURN COMPONENT WITH ITS DEPLOYMENT return this.createQueryBuilder('v2components') diff --git a/butler/src/app/v2/api/deployments/repository/deployment.repository.ts b/butler/src/app/v2/api/deployments/repository/deployment.repository.ts index a704d0f5bc..9381fb9fcb 100644 --- a/butler/src/app/v2/api/deployments/repository/deployment.repository.ts +++ b/butler/src/app/v2/api/deployments/repository/deployment.repository.ts @@ -34,16 +34,17 @@ export class DeploymentRepositoryV2 extends Repository { .where({ id: id }) .returning('id') .execute() - return this.findOneOrFail(updated.raw[0].id) + return await this.findOneOrFail(updated.raw[0].id) } - public async updateRouteStatus(id: string, status: boolean): Promise { + + public async updateRouteStatus(circleId: string, status: boolean): Promise { const updated = await this.createQueryBuilder('d') .update() .set({ routed: status }) - .where({ id: id }) + .where({ circleId: circleId, current: true }) .returning('id') .execute() - return this.findOneOrFail(updated.raw[0].id) + return await this.findOneOrFail(updated.raw[0].id) } public async findWithComponentsAndConfig(deploymentId: string): Promise { diff --git a/butler/src/app/v2/api/deployments/use-cases/create-undeployment.usecase.ts b/butler/src/app/v2/api/deployments/use-cases/create-undeployment.usecase.ts index 61892e3154..8e080c3390 100644 --- a/butler/src/app/v2/api/deployments/use-cases/create-undeployment.usecase.ts +++ b/butler/src/app/v2/api/deployments/use-cases/create-undeployment.usecase.ts @@ -54,7 +54,7 @@ export class CreateUndeploymentUseCase { private async createExecution(deployment: DeploymentEntity, incomingCircleId: string | null): Promise { this.consoleLoggerService.log('START:CREATE_UNDEPLOYMENT_EXECUTION', { deployment: deployment.id }) const execution = await this.executionRepository.save({ deployment, type: ExecutionTypeEnum.UNDEPLOYMENT, incomingCircleId }) - await this.deploymentsRepository.update({ id: deployment.id }, { current: false }) + await this.deploymentsRepository.update({ id: deployment.id }, { current: false, healthy: false, routed: false }) this.consoleLoggerService.log('FINISH:CREATE_UNDEPLOYMENT_EXECUTION', { execution: execution.id }) return execution } diff --git a/butler/src/app/v2/core/integrations/utils/istio-deployment-manifests.utils.ts b/butler/src/app/v2/core/integrations/utils/istio-deployment-manifests.utils.ts index bb5d5ada4a..3259c98f9f 100644 --- a/butler/src/app/v2/core/integrations/utils/istio-deployment-manifests.utils.ts +++ b/butler/src/app/v2/core/integrations/utils/istio-deployment-manifests.utils.ts @@ -14,22 +14,26 @@ * limitations under the License. */ -import { Http, K8sManifest, Subset } from '../interfaces/k8s-manifest.interface' +import { Http, Subset } from '../interfaces/k8s-manifest.interface' import { ISpinnakerConfigurationData } from '../../../api/configurations/interfaces' import { Component, Deployment } from '../../../api/deployments/interfaces' import { IstioManifestsUtils } from './istio-manifests.utilts' import { DeploymentUtils } from './deployment.utils' import { DeploymentComponent } from '../../../api/deployments/interfaces/deployment.interface' +import { DestinationRuleSpec, VirtualServiceSpec } from '../../../operator/params.interface' const IstioDeploymentManifestsUtils = { - getVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): K8sManifest => { + getVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): VirtualServiceSpec => { return { apiVersion: 'networking.istio.io/v1beta1', kind: 'VirtualService', metadata: { name: `${component.name}`, - namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}` + namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}`, + annotations: { + circles: JSON.stringify(activeByName.map(c => c.deployment.circleId)) + } }, spec: { gateways: component.gatewayName ? [component.gatewayName] : [], @@ -41,13 +45,16 @@ const IstioDeploymentManifestsUtils = { } }, - getDestinationRulesManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): K8sManifest => { + getDestinationRulesManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): DestinationRuleSpec => { return { apiVersion: 'networking.istio.io/v1beta1', kind: 'DestinationRule', metadata: { name: component.name, - namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}` + namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}`, + annotations: { + circles: JSON.stringify(activeByName.map(c => c.deployment.circleId)) + } }, spec: { host: component.name, diff --git a/butler/src/app/v2/core/integrations/utils/istio-undeployment-manifests.utils.ts b/butler/src/app/v2/core/integrations/utils/istio-undeployment-manifests.utils.ts index 14afa5ea2c..e4f978f2cc 100644 --- a/butler/src/app/v2/core/integrations/utils/istio-undeployment-manifests.utils.ts +++ b/butler/src/app/v2/core/integrations/utils/istio-undeployment-manifests.utils.ts @@ -14,22 +14,26 @@ * limitations under the License. */ -import { Http, K8sManifest, Subset } from '../interfaces/k8s-manifest.interface' -import { Component, Deployment } from '../../../api/deployments/interfaces' import { ISpinnakerConfigurationData } from '../../../api/configurations/interfaces' -import { IstioManifestsUtils } from './istio-manifests.utilts' -import { DeploymentUtils } from './deployment.utils' +import { Component, Deployment } from '../../../api/deployments/interfaces' import { DeploymentComponent } from '../../../api/deployments/interfaces/deployment.interface' +import { DestinationRuleSpec, VirtualServiceSpec } from '../../../operator/params.interface' +import { Http, Subset } from '../interfaces/k8s-manifest.interface' +import { DeploymentUtils } from './deployment.utils' +import { IstioManifestsUtils } from './istio-manifests.utilts' const IstioUndeploymentManifestsUtils = { - getVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): K8sManifest => { + getVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): VirtualServiceSpec => { return { apiVersion: 'networking.istio.io/v1beta1', kind: 'VirtualService', metadata: { name: `${component.name}`, - namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}` + namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}`, + annotations: { + circles: JSON.stringify(activeByName.map(c => c.deployment.circleId)) + } }, spec: { gateways: component.gatewayName ? [component.gatewayName] : [], @@ -39,13 +43,17 @@ const IstioUndeploymentManifestsUtils = { } }, - getEmptyVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent): K8sManifest => { + getEmptyVirtualServiceManifest: (deployment: Deployment, component: DeploymentComponent): VirtualServiceSpec => { return { apiVersion: 'networking.istio.io/v1beta1', kind: 'VirtualService', metadata: { name: component.name, - namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}` + namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}`, + annotations: { + circles: JSON.stringify([]) + } + }, spec: { gateways: component.gatewayName ? [component.gatewayName] : [], @@ -74,17 +82,21 @@ const IstioUndeploymentManifestsUtils = { } }, - getDestinationRulesManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): K8sManifest => { + getDestinationRulesManifest: (deployment: Deployment, component: DeploymentComponent, activeByName: Component[]): DestinationRuleSpec => { + const istioSubsets = IstioUndeploymentManifestsUtils.getActiveComponentsSubsets(deployment.circleId, activeByName) return { apiVersion: 'networking.istio.io/v1beta1', kind: 'DestinationRule', metadata: { name: component.name, - namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}` + namespace: `${(deployment.cdConfiguration.configurationData as ISpinnakerConfigurationData).namespace}`, + annotations: { + circles: JSON.stringify(istioSubsets.map(s => s.labels.circleId)) + } }, spec: { host: component.name, - subsets: IstioUndeploymentManifestsUtils.getActiveComponentsSubsets(deployment.circleId, activeByName) + subsets: istioSubsets } } }, diff --git a/butler/src/app/v2/operator/deployments.hook.controller.ts b/butler/src/app/v2/operator/deployments.hook.controller.ts index 201685b96a..4627854f0a 100644 --- a/butler/src/app/v2/operator/deployments.hook.controller.ts +++ b/butler/src/app/v2/operator/deployments.hook.controller.ts @@ -7,7 +7,7 @@ import { KubernetesManifest } from '../core/integrations/interfaces/k8s-manifest import { K8sClient } from '../core/integrations/k8s/client' import { ConsoleLoggerService } from '../core/logs/console/console-logger.service' import { HookParams } from './params.interface' -import { Reconcile } from './reconcile' +import { ReconcileDeployment } from './use-cases/reconcile-deployments.usecase' @Controller('/') export class DeploymentsHookController { @@ -17,25 +17,25 @@ export class DeploymentsHookController { private readonly deploymentRepository: DeploymentRepositoryV2, private readonly componentRepository: ComponentsRepositoryV2, private readonly configurationRepository: CdConfigurationsRepository, - private readonly consoleLoggerService: ConsoleLoggerService + private readonly consoleLoggerService: ConsoleLoggerService, + private readonly reconcileUseCase: ReconcileDeployment ) { } @Post('/v2/operator/deployment/hook/reconcile') @HttpCode(200) @UsePipes(new ValidationPipe({ transform: true })) public async reconcile(@Body() params: HookParams) : Promise<{status?: unknown, children: KubernetesManifest[], resyncAfterSeconds?: number}> { - const reconcile = new Reconcile() const deployment = await this.deploymentRepository.findWithComponentsAndConfig(params.parent.spec.deploymentId) const decryptedConfig = await this.configurationRepository.findDecrypted(deployment.cdConfiguration.id) const rawSpecs = deployment.components.flatMap(c => c.manifests) - const specs = reconcile.addMetadata(rawSpecs, deployment) + const specs = this.reconcileUseCase.addMetadata(rawSpecs, deployment) if (isEmpty(params.children['Deployment.apps/v1'])) { return { children: specs, resyncAfterSeconds: 5 } } - const currentDeploymentSpecs = reconcile.specsByDeployment(params, deployment.id) + const currentDeploymentSpecs = this.reconcileUseCase.specsByDeployment(params, deployment.id) - const allReady = reconcile.checkConditions(currentDeploymentSpecs) + const allReady = this.reconcileUseCase.checkConditions(currentDeploymentSpecs) if (allReady === false) { const previousDeploymentId = deployment.previousDeploymentId @@ -44,7 +44,7 @@ export class DeploymentsHookController { return { children: specs, resyncAfterSeconds: 5 } } const previousDeployment = await this.deploymentRepository.findWithComponentsAndConfig(previousDeploymentId) - const currentAndPrevious = reconcile.concatWithPrevious(previousDeployment, specs) + const currentAndPrevious = this.reconcileUseCase.concatWithPrevious(previousDeployment, specs) return { children: currentAndPrevious, resyncAfterSeconds: 5 } } diff --git a/butler/src/app/v2/operator/params.interface.ts b/butler/src/app/v2/operator/params.interface.ts index f0be13925c..e627a6c55d 100644 --- a/butler/src/app/v2/operator/params.interface.ts +++ b/butler/src/app/v2/operator/params.interface.ts @@ -1,3 +1,5 @@ +import { Http } from '../core/integrations/interfaces/k8s-manifest.interface' + export interface RouteHookParams { controller?: Record parent: { @@ -20,8 +22,8 @@ export interface RouteHookParams { } export interface RouteChildren { - 'VirtualService.networking.istio.io/v1beta1': VirtualServiceSpec, - 'DestinationRule.networking.istio.io/v1beta1': DestinationRuleSpec + 'VirtualService.networking.istio.io/v1beta1': ChildVirtualServiceSpec, + 'DestinationRule.networking.istio.io/v1beta1': ChildDestinationRuleSpec } export interface HookParams { @@ -87,53 +89,42 @@ export interface DeploymentSpec { } } +export interface ChildVirtualServiceSpec { + [key: string]: VirtualServiceSpec +} + +export interface ChildDestinationRuleSpec { + [key: string]: DestinationRuleSpec +} + export interface VirtualServiceSpec { - [key: string]: { apiVersion: string - kind: string - metadata?: SpecMetadata + kind: 'VirtualService' + metadata: { + name: string + namespace: string + annotations: { + circles: string, + } + } spec: { gateways: string[] hosts: string[] - http: { - match?: { - headers: { - cookie?: { - regex: string - } - 'x-circle-id'?: { - exact: string - } - } - }[], - route: { - destination: { - host: string - subset: string - } - headers: { - request: { - set: { - 'x-circle-source': string - } - }, - response: { - set: { - 'x-circle-source': string - } - } - } - }[] - }[] + http: Http[] } } -} export interface DestinationRuleSpec { - [key: string]: { apiVersion: string - kind: string - metadata?: SpecMetadata + kind: 'DestinationRule' + metadata: { + name: string + namespace: string + annotations: { + circles: string, + } + teste?: string + } spec: { host: string subsets: { @@ -146,7 +137,6 @@ export interface DestinationRuleSpec { }[] } } -} export interface ServiceSpec { [key: string]: { diff --git a/butler/src/app/v2/operator/partial-params.interface.ts b/butler/src/app/v2/operator/partial-params.interface.ts new file mode 100644 index 0000000000..00ecf0faf2 --- /dev/null +++ b/butler/src/app/v2/operator/partial-params.interface.ts @@ -0,0 +1,36 @@ +import { VirtualServiceSpec, DestinationRuleSpec } from './params.interface' + +type PartialVirtualServiceSpec = Pick +type PartialDestinationRuleSpec = Pick +export type SpecsUnion = PartialVirtualServiceSpec | PartialDestinationRuleSpec + + +export interface PartialRouteHookParams { + parent: { + spec: { + circles: { + components: { + name: string + tag: string + }[] + default: boolean + id: string + }[] + } + } + children: PartialRouteChildren +} + +interface PartialRouteChildren { + 'VirtualService.networking.istio.io/v1beta1': PartialChildVirtualServiceSpec, + 'DestinationRule.networking.istio.io/v1beta1': PartialChildDestinationRuleSpec +} + + +interface PartialChildVirtualServiceSpec { + [key: string]: PartialVirtualServiceSpec +} + +interface PartialChildDestinationRuleSpec { + [key: string]: PartialDestinationRuleSpec +} diff --git a/butler/src/app/v2/operator/use-cases/create-routes-manifests.usecase.ts b/butler/src/app/v2/operator/use-cases/create-routes-manifests.usecase.ts index 84c2d23763..aa06f89dc3 100644 --- a/butler/src/app/v2/operator/use-cases/create-routes-manifests.usecase.ts +++ b/butler/src/app/v2/operator/use-cases/create-routes-manifests.usecase.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { KubernetesObject } from '@kubernetes/client-node/dist/types' import { Injectable } from '@nestjs/common' -import { isEmpty, isEqual } from 'lodash' +import { groupBy, isEmpty } from 'lodash' import { CdConfigurationsRepository } from '../../api/configurations/repository' import { DeploymentEntityV2 } from '../../api/deployments/entity/deployment.entity' import { Component } from '../../api/deployments/interfaces' @@ -26,7 +25,8 @@ import { KubernetesManifest } from '../../core/integrations/interfaces/k8s-manif import { DeploymentUtils } from '../../core/integrations/utils/deployment.utils' import { IstioDeploymentManifestsUtils } from '../../core/integrations/utils/istio-deployment-manifests.utils' import { ConsoleLoggerService } from '../../core/logs/console' -import { RouteChildren, RouteHookParams } from '../params.interface' +import { DestinationRuleSpec, RouteHookParams, VirtualServiceSpec } from '../params.interface' +import { PartialRouteHookParams, SpecsUnion } from '../partial-params.interface' @Injectable() export class CreateRoutesManifestsUseCase { @@ -40,28 +40,85 @@ export class CreateRoutesManifestsUseCase { public async execute(hookParams: RouteHookParams): Promise<{status?: unknown, children: KubernetesManifest[], resyncAfterSeconds?: number}> { this.consoleLoggerService.log('START:EXECUTE_RECONCILE_ROUTE_MANIFESTS_USECASE') - const specs = Promise.all(hookParams.parent.spec.circles.flatMap(async c => { + let specs : (VirtualServiceSpec | DestinationRuleSpec)[]= [] + for (const c of hookParams.parent.spec.circles) { const deployment = await this.retriveDeploymentFor(c.id) - const activeComponents = await this.componentsRepository.findActiveComponents(deployment.cdConfiguration.id) + const activeComponents = await this.componentsRepository.findHealthyActiveComponents(deployment.cdConfiguration.id) const proxySpecs = this.createProxySpecsFor(deployment, activeComponents) - this.consoleLoggerService.log('FINISH:EXECUTE_RECONCILE_ROUTE_MANIFESTS_USECASE') - return proxySpecs - })).then(s => { + specs = specs.concat(proxySpecs) + } + const healthStatus = this.getRoutesStatus(hookParams, specs) + await this.updateRouteStatus(healthStatus) + return { children: specs, resyncAfterSeconds: 5 } + } - console.log({ - observed: JSON.stringify(hookParams.children), - desired: JSON.stringify(s.flat()) + public getRoutesStatus(observed: PartialRouteHookParams, desired: SpecsUnion[]): {circle: string, component: string, status: boolean, kind: string}[] { + if (desired.length === 0) { + return [] + } + return desired.flatMap(spec => { + const circles : string[] = JSON.parse(spec.metadata.annotations.circles) + return circles.flatMap(desiredCircleId => { + return this.handleSpecStatus(observed, spec, desiredCircleId) }) + }) + } + + public async updateRouteStatus(componentStatus: { circle: string, component: string, status: boolean, kind: string }[]): Promise { + const components = groupBy(componentStatus, 'circle') + const results = await Promise.all(Object.entries(components).flatMap(async c => { + const circleId = c[0] + const status = c[1] + if (status.every(s => s.status === true)) { + return await this.deploymentRepository.updateRouteStatus(circleId, true) + } else { + return await this.deploymentRepository.updateRouteStatus(circleId, false) + } + })) + + return results + } - return { children: s.flat() } + private handleSpecStatus(observed: PartialRouteHookParams, spec: SpecsUnion, circleId: string): { circle: string, component: string, status: boolean, kind: string } { + const baseResponse = { + circle: circleId, + component: spec.metadata.name, + kind: spec.kind, + status: false + } + + if (this.checkEmptySpecs(observed) === true) { + baseResponse.status = false + return baseResponse + } + + if (this.checkComponentExistsOnObserved(observed, spec, circleId)) { + baseResponse.status = true + return baseResponse + } else { + baseResponse.status = false + return baseResponse } - ) - return specs } - public checkRoutes(observed: RouteChildren, desired: KubernetesManifest[]): boolean { - const o = Object.values(observed).flat().filter( (o: KubernetesManifest) => !isEmpty(o)) - return isEqual(o, desired) + private checkEmptySpecs(observed: PartialRouteHookParams): boolean { + const emptyDestinationRules = isEmpty(observed.children['DestinationRule.networking.istio.io/v1beta1']) + const emptyVirtualServices = isEmpty(observed.children['VirtualService.networking.istio.io/v1beta1']) + if (emptyDestinationRules || emptyVirtualServices) { + return true + } + return false + } + + private checkComponentExistsOnObserved(observed: PartialRouteHookParams, spec: SpecsUnion, circleId: string): boolean { + const destionRulesCircles : string[] = JSON.parse(observed.children['DestinationRule.networking.istio.io/v1beta1'][spec.metadata.name].metadata.annotations.circles) + const desiredDestinationRulePresent = destionRulesCircles.includes(circleId) + const virtualServiceCircles : string [] = JSON.parse(observed.children['VirtualService.networking.istio.io/v1beta1'][spec.metadata.name].metadata.annotations.circles) + const desiredVirtualServicePresent = virtualServiceCircles.includes(circleId) + if (desiredDestinationRulePresent && desiredVirtualServicePresent) { + return true + } + return false } private async retriveDeploymentFor(id: string): Promise { @@ -74,8 +131,8 @@ export class CreateRoutesManifestsUseCase { return deployment } - private createProxySpecsFor(deployment: DeploymentEntityV2, activeComponents: Component[]): KubernetesManifest[] { - const proxySpecs: KubernetesManifest[] = [] + private createProxySpecsFor(deployment: DeploymentEntityV2, activeComponents: Component[]): (VirtualServiceSpec | DestinationRuleSpec)[] { + const proxySpecs: (VirtualServiceSpec | DestinationRuleSpec)[] = [] deployment.components.forEach(component => { const manifests = this.createIstioProxiesManifestsFor(deployment, component, activeComponents) manifests.forEach(m => proxySpecs.push(m)) @@ -86,7 +143,7 @@ export class CreateRoutesManifestsUseCase { private createIstioProxiesManifestsFor(deployment: DeploymentEntityV2, newComponent: Component, activeComponents: Component[] - ): [KubernetesManifest, KubernetesManifest] { + ): (VirtualServiceSpec | DestinationRuleSpec)[] { const activeByName: Component[] = DeploymentUtils.getActiveComponentsByName(activeComponents, newComponent.name) const destinationRules = IstioDeploymentManifestsUtils.getDestinationRulesManifest(deployment, newComponent, activeByName) const virtualService = IstioDeploymentManifestsUtils.getVirtualServiceManifest(deployment, newComponent, activeByName) diff --git a/butler/src/app/v2/operator/reconcile.ts b/butler/src/app/v2/operator/use-cases/reconcile-deployments.usecase.ts similarity index 86% rename from butler/src/app/v2/operator/reconcile.ts rename to butler/src/app/v2/operator/use-cases/reconcile-deployments.usecase.ts index db3b2ec1b2..b4b7cd3e63 100644 --- a/butler/src/app/v2/operator/reconcile.ts +++ b/butler/src/app/v2/operator/use-cases/reconcile-deployments.usecase.ts @@ -1,10 +1,11 @@ +import { Injectable } from '@nestjs/common' import { uniqWith } from 'lodash' -import { DeploymentEntityV2 } from '../api/deployments/entity/deployment.entity' -import { KubernetesManifest } from '../core/integrations/interfaces/k8s-manifest.interface' -import { HookParams, SpecMetadata, SpecStatus } from './params.interface' +import { DeploymentEntityV2 } from '../../api/deployments/entity/deployment.entity' +import { KubernetesManifest } from '../../core/integrations/interfaces/k8s-manifest.interface' +import { HookParams, SpecMetadata, SpecStatus } from '../params.interface' - -export class Reconcile { +@Injectable() +export class ReconcileDeployment { public concatWithPrevious(previousDeployment: DeploymentEntityV2, specs: KubernetesManifest[]) : KubernetesManifest[] { const rawSpecs = previousDeployment.components.flatMap(c => c.manifests) const previousSpecs = this.addMetadata(rawSpecs, previousDeployment) diff --git a/butler/src/tests/v2/fixtures/manifests.fixture.ts b/butler/src/tests/v2/fixtures/manifests.fixture.ts index 7bf85f1a38..387687eeab 100644 --- a/butler/src/tests/v2/fixtures/manifests.fixture.ts +++ b/butler/src/tests/v2/fixtures/manifests.fixture.ts @@ -57,6 +57,9 @@ export const routesManifests: KubernetesManifest[] = [ metadata: { name: 'hello-kubernetes', namespace: 'namespace', + annotations: { + circles: '["b46fd548-0082-4021-ba80-a50703c44a3b"]' + } }, spec: { host: 'hello-kubernetes', @@ -78,6 +81,9 @@ export const routesManifests: KubernetesManifest[] = [ metadata: { name: 'hello-kubernetes', namespace: 'namespace', + annotations: { + circles: '["b46fd548-0082-4021-ba80-a50703c44a3b"]' + } }, spec: { gateways: [ diff --git a/butler/src/tests/v2/integration/operator/create-routes-manifests-usecase.integration-spec.ts b/butler/src/tests/v2/integration/operator/create-routes-manifests-usecase.integration-spec.ts new file mode 100644 index 0000000000..9181fa7261 --- /dev/null +++ b/butler/src/tests/v2/integration/operator/create-routes-manifests-usecase.integration-spec.ts @@ -0,0 +1,184 @@ +import { INestApplication } from '@nestjs/common' +import { Test } from '@nestjs/testing' +import { EntityManager } from 'typeorm' +import { AppModule } from '../../../../app/app.module' +import { DeploymentRepositoryV2 } from '../../../../app/v2/api/deployments/repository/deployment.repository' +import { CreateRoutesManifestsUseCase } from '../../../../app/v2/operator/use-cases/create-routes-manifests.usecase' +import { cdConfigurationFixture, deploymentFixture } from '../../fixtures/deployment-entity.fixture' +import { FixtureUtilsService } from '../fixture-utils.service' +import { TestSetupUtils } from '../test-setup-utils' + +describe('Routes manifest use case', () => { + let fixtureUtilsService: FixtureUtilsService + let app: INestApplication + let deploymentRepository: DeploymentRepositoryV2 + let routeUseCase: CreateRoutesManifestsUseCase + let manager: EntityManager + + beforeAll(async() => { + const module = Test.createTestingModule({ + imports: [ + await AppModule.forRootAsync() + ], + providers: [ + FixtureUtilsService, + DeploymentRepositoryV2 + ] + }) + + app = await TestSetupUtils.createApplication(module) + TestSetupUtils.seApplicationConstants() + fixtureUtilsService = app.get(FixtureUtilsService) + deploymentRepository = app.get(DeploymentRepositoryV2) + routeUseCase = app.get(CreateRoutesManifestsUseCase) + manager = fixtureUtilsService.connection.manager + }) + + afterAll(async() => { + await fixtureUtilsService.clearDatabase() + await app.close() + }) + + beforeEach(async() => { + await fixtureUtilsService.clearDatabase() + }) + + it('Updates the healthy column to true when both VirtualService and DestinationRule for a component is true', async() => { + const configuration = await manager.save(cdConfigurationFixture) + deploymentFixture.cdConfiguration = configuration + deploymentFixture.circleId = 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' + deploymentFixture.current = true + await deploymentRepository.save(deploymentFixture) + + const params = [ + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'VirtualService', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: true + } + ] + + const result = await routeUseCase.updateRouteStatus(params) + expect(result.map(r => r.routed)).toEqual([true]) + }) + + it('Updates the healthy column to false when at least one manifest status is false', async() => { + const configuration = await manager.save(cdConfigurationFixture) + deploymentFixture.cdConfiguration = configuration + deploymentFixture.circleId = 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' + deploymentFixture.current = true + await deploymentRepository.save(deploymentFixture) + + const params = [ + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'VirtualService', + status: false + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: true + } + ] + + const result = await routeUseCase.updateRouteStatus(params) + expect(result.map(r => r.routed)).toEqual([false]) + }) + + it('Updates the healthy column for multiple circles independently', async() => { + const configuration = await manager.save(cdConfigurationFixture) + const firstCircleId = 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' + const secondCircleId = 'ed2a1669-34b8-4af2-b42c-acbad2ec6b60' + const firstDeployment = deploymentFixture + const secondDeployment = deploymentFixture + firstDeployment.cdConfiguration = configuration + firstDeployment.circleId = firstCircleId + await deploymentRepository.save(firstDeployment) + + secondDeployment.cdConfiguration = configuration + secondDeployment.circleId = secondCircleId + secondDeployment.id = 'a7d08a07-f29d-452e-a667-7a39820f3262' + await deploymentRepository.save(secondDeployment) + + const params = [ + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'VirtualService', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: true + }, + { + circle: 'ed2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: false + }, + { + circle: 'ed2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: true + } + ] + + const result = await routeUseCase.updateRouteStatus(params) + expect(result.map(r => ({ circle: r.circleId, routed: r.routed }))).toEqual( + [ + { circle: firstCircleId, routed: true }, + { circle: secondCircleId, routed: false } + ] + ) + }) +}) diff --git a/butler/src/tests/v2/unit/operator/create-routes-manifests-usecase.spec.ts b/butler/src/tests/v2/unit/operator/create-routes-manifests-usecase.spec.ts index 0103160de0..1541942b40 100644 --- a/butler/src/tests/v2/unit/operator/create-routes-manifests-usecase.spec.ts +++ b/butler/src/tests/v2/unit/operator/create-routes-manifests-usecase.spec.ts @@ -17,13 +17,13 @@ import 'jest' import { CdConfigurationsRepository } from '../../../../app/v2/api/configurations/repository' import { ComponentsRepositoryV2 } from '../../../../app/v2/api/deployments/repository' -import { ConsoleLoggerService } from '../../../../app/v2/core/logs/console' import { DeploymentRepositoryV2 } from '../../../../app/v2/api/deployments/repository/deployment.repository' +import { ConsoleLoggerService } from '../../../../app/v2/core/logs/console' +import { RouteHookParams } from '../../../../app/v2/operator/params.interface' +import { PartialRouteHookParams, SpecsUnion } from '../../../../app/v2/operator/partial-params.interface' import { CreateRoutesManifestsUseCase } from '../../../../app/v2/operator/use-cases/create-routes-manifests.usecase' import { cdConfigurationFixture, deployComponentsFixture, deploymentFixture } from '../../fixtures/deployment-entity.fixture' import { routesManifests } from '../../fixtures/manifests.fixture' -import { RouteHookParams } from '../../../../app/v2/operator/params.interface' -import { KubernetesManifest } from '../../../../app/v2/core/integrations/interfaces/k8s-manifest.interface' describe('Hook Routes Manifest Creation', () => { @@ -69,6 +69,8 @@ describe('Hook Routes Manifest Creation', () => { }) it('generate route manifest correctly', async() => { + jest.spyOn(deploymentRepository, 'updateRouteStatus').mockImplementation(async() => deploymentFixture) + jest.spyOn(componentsRepository, 'findHealthyActiveComponents').mockImplementation(async() => deployComponentsFixture) const routeUseCase = new CreateRoutesManifestsUseCase( deploymentRepository, componentsRepository, @@ -78,7 +80,7 @@ describe('Hook Routes Manifest Creation', () => { const manifests = await routeUseCase.execute(hookParams) - expect(manifests).toEqual({ children: routesManifests }) + expect(manifests).toEqual({ children: routesManifests, resyncAfterSeconds: 5 }) }) it('throws exception when manifests generation fail', async() => { @@ -98,17 +100,39 @@ describe('Hook Routes Manifest Creation', () => { describe('Compare observed routes state with desired routes state', () => { - it('should return true when observed and desired states are empty', () => { + it('should return empty array when observed and desired states are empty', () => { const deploymentRepository = new DeploymentRepositoryV2() const componentsRepository = new ComponentsRepositoryV2() const cdConfigurationsRepository = new CdConfigurationsRepository() const consoleLoggerService = new ConsoleLoggerService() - jest.spyOn(deploymentRepository, 'findOneOrFail').mockImplementation(async() => deploymentFixture) - jest.spyOn(cdConfigurationsRepository, 'findDecrypted').mockImplementation(async() => cdConfigurationFixture) - jest.spyOn(componentsRepository, 'findActiveComponents').mockImplementation(async() => deployComponentsFixture) - const observed = { 'DestinationRule.networking.istio.io/v1beta1':{}, 'VirtualService.networking.istio.io/v1beta1':{} } - const desired : KubernetesManifest[] = [] + const observed : PartialRouteHookParams = { + parent: { + spec: { + circles: [ + { + components: [ + { + name: 'jilo', + tag: 'latest' + }, + { + name: 'abobora', + tag: 'latest' + } + ], + default: false, + id: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' + } + ] + } + }, + children: { + 'DestinationRule.networking.istio.io/v1beta1': {}, + 'VirtualService.networking.istio.io/v1beta1': {} + } + } + const desired : SpecsUnion[] = [] const routeUseCase = new CreateRoutesManifestsUseCase( deploymentRepository, componentsRepository, @@ -116,408 +140,238 @@ describe('Compare observed routes state with desired routes state', () => { consoleLoggerService ) - expect(routeUseCase.checkRoutes(observed, desired)).toEqual(true) + expect(routeUseCase.getRoutesStatus(observed, desired)).toEqual([]) }) - it.skip('should return true when observed and desired states are empty', () => { + it('should return the objects with status true for all desired components', () => { const deploymentRepository = new DeploymentRepositoryV2() const componentsRepository = new ComponentsRepositoryV2() const cdConfigurationsRepository = new CdConfigurationsRepository() const consoleLoggerService = new ConsoleLoggerService() - jest.spyOn(deploymentRepository, 'findOneOrFail').mockImplementation(async() => deploymentFixture) - jest.spyOn(cdConfigurationsRepository, 'findDecrypted').mockImplementation(async() => cdConfigurationFixture) - jest.spyOn(componentsRepository, 'findActiveComponents').mockImplementation(async() => deployComponentsFixture) - - const observed = { - 'DestinationRule.networking.istio.io/v1beta1': { - 'abobora': { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'DestinationRule', - 'spec': { - 'host': 'abobora', - 'subsets': [ - { - 'labels': { - 'circleId': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', - 'component': 'abobora', - 'tag': 'latest' - }, - 'name': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - ] - } - }, - 'jilo': { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'DestinationRule', - 'spec': { - 'host': 'jilo', - 'subsets': [ - { - 'labels': { - 'circleId': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', - 'component': 'jilo', - 'tag': 'latest' + const observed : PartialRouteHookParams = { + parent: { + spec: { + circles: [ + { + components: [ + { + name: 'jilo', + tag: 'latest' }, - 'name': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - ] - } + { + name: 'abobora', + tag: 'latest' + } + ], + default: false, + id: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' + } + ] } }, - 'VirtualService.networking.istio.io/v1beta1': { - 'abobora': { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'VirtualService', - 'spec': { - 'gateways': [], - 'hosts': [ - 'abobora' - ], - 'http': [ - { - 'match': [ - { - 'headers': { - 'cookie': { - 'regex': '.*x-circle-id=ad2a1669-34b8-4af2-b42c-acbad2ec6b60.*' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'abobora', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] - }, - { - 'match': [ - { - 'headers': { - 'x-circle-id': { - 'exact': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'abobora', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] + children: { + 'DestinationRule.networking.istio.io/v1beta1': { + abobora: { + kind: 'DestinationRule', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' } - ] + } + }, + jilo: { + kind: 'DestinationRule', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } + } } }, - 'jilo': { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'VirtualService', - 'spec': { - 'gateways': [], - 'hosts': [ - 'jilo' - ], - 'http': [ - { - 'match': [ - { - 'headers': { - 'cookie': { - 'regex': '.*x-circle-id=ad2a1669-34b8-4af2-b42c-acbad2ec6b60.*' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'jilo', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] - }, - { - 'match': [ - { - 'headers': { - 'x-circle-id': { - 'exact': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'jilo', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] + 'VirtualService.networking.istio.io/v1beta1': { + abobora: { + kind: 'VirtualService', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } + } + }, + jilo: { + kind: 'VirtualService', + metadata: { + name: 'jilo', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' } - ] + } } } } } - const desired = [ + + const desired : SpecsUnion[] = [ { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'DestinationRule', - 'metadata': { - 'name': 'jilo', - 'namespace': 'default' - }, - 'spec': { - 'host': 'jilo', - 'subsets': [ - { - 'labels': { - 'component': 'jilo', - 'tag': 'latest', - 'circleId': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'name': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - ] + kind: 'DestinationRule', + metadata: { + name: 'jilo', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } } }, { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'VirtualService', - 'metadata': { - 'name': 'jilo', - 'namespace': 'default' + kind: 'VirtualService', + metadata: { + name: 'jilo', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } }, - 'spec': { - 'gateways': [], - 'hosts': [ - 'jilo' - ], - 'http': [ + }, + { + kind: 'DestinationRule', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } + } + }, + { + kind: 'VirtualService', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } + } + } + ] + + const routeUseCase = new CreateRoutesManifestsUseCase( + deploymentRepository, + componentsRepository, + cdConfigurationsRepository, + consoleLoggerService + ) + + expect(routeUseCase.getRoutesStatus(observed, desired)).toEqual([ + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'VirtualService', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: true + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: true + } + ]) + + }) + it('should return the objects with status false for all desired components when the observed state is does not have the routes', () => { + const deploymentRepository = new DeploymentRepositoryV2() + const componentsRepository = new ComponentsRepositoryV2() + const cdConfigurationsRepository = new CdConfigurationsRepository() + const consoleLoggerService = new ConsoleLoggerService() + + + const observed : PartialRouteHookParams = { + parent: { + spec: { + circles: [ { - 'match': [ - { - 'headers': { - 'cookie': { - 'regex': '.*x-circle-id=ad2a1669-34b8-4af2-b42c-acbad2ec6b60.*' - } - } - } - ], - 'route': [ + components: [ { - 'destination': { - 'host': 'jilo', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] - }, - { - 'match': [ + name: 'jilo', + tag: 'latest' + }, { - 'headers': { - 'x-circle-id': { - 'exact': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } + name: 'abobora', + tag: 'latest' } ], - 'route': [ - { - 'destination': { - 'host': 'jilo', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] + default: false, + id: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' } ] } }, + children: { + 'DestinationRule.networking.istio.io/v1beta1': {}, + 'VirtualService.networking.istio.io/v1beta1': {} + } + } + + const desired : SpecsUnion[] = [ { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'DestinationRule', - 'metadata': { - 'name': 'abobora', - 'namespace': 'default' - }, - 'spec': { - 'host': 'abobora', - 'subsets': [ - { - 'labels': { - 'component': 'abobora', - 'tag': 'latest', - 'circleId': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'name': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - ] + kind: 'DestinationRule', + metadata: { + name: 'jilo', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } } }, { - 'apiVersion': 'networking.istio.io/v1beta1', - 'kind': 'VirtualService', - 'metadata': { - 'name': 'abobora', - 'namespace': 'default' + kind: 'VirtualService', + metadata: { + name: 'jilo', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } + } + }, + { + kind: 'DestinationRule', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } }, - 'spec': { - 'gateways': [], - 'hosts': [ - 'abobora' - ], - 'http': [ - { - 'match': [ - { - 'headers': { - 'cookie': { - 'regex': '.*x-circle-id=ad2a1669-34b8-4af2-b42c-acbad2ec6b60.*' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'abobora', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] - }, - { - 'match': [ - { - 'headers': { - 'x-circle-id': { - 'exact': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - ], - 'route': [ - { - 'destination': { - 'host': 'abobora', - 'subset': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - }, - 'headers': { - 'request': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - }, - 'response': { - 'set': { - 'x-circle-source': 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60' - } - } - } - } - ] - } - ] + }, + { + kind: 'VirtualService', + metadata: { + name: 'abobora', + namespace: 'default', + annotations: { + circles: '["ad2a1669-34b8-4af2-b42c-acbad2ec6b60"]' + } } } ] - const routeUseCase = new CreateRoutesManifestsUseCase( deploymentRepository, componentsRepository, @@ -525,7 +379,31 @@ describe('Compare observed routes state with desired routes state', () => { consoleLoggerService ) - expect(routeUseCase.checkRoutes(observed, desired)).toEqual(true) - + expect(routeUseCase.getRoutesStatus(observed, desired)).toEqual([ + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'DestinationRule', + status: false + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'jilo', + kind: 'VirtualService', + status: false + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'DestinationRule', + status: false + }, + { + circle: 'ad2a1669-34b8-4af2-b42c-acbad2ec6b60', + component: 'abobora', + kind: 'VirtualService', + status: false + } + ]) }) }) diff --git a/butler/src/tests/v2/unit/operator/reconcile.spec.ts b/butler/src/tests/v2/unit/operator/reconcile.spec.ts index 74e3347502..0a566e1922 100644 --- a/butler/src/tests/v2/unit/operator/reconcile.spec.ts +++ b/butler/src/tests/v2/unit/operator/reconcile.spec.ts @@ -5,20 +5,20 @@ import { ComponentEntityV2 } from '../../../../app/v2/api/deployments/entity/com import { DeploymentEntityV2 } from '../../../../app/v2/api/deployments/entity/deployment.entity' import { GitProvidersEnum } from '../../../../app/v2/core/configuration/interfaces/git-providers.type' import { ClusterProviderEnum } from '../../../../app/v2/core/integrations/octopipe/interfaces/octopipe-payload.interface' -import { Reconcile } from '../../../../app/v2/operator/reconcile' +import { ReconcileDeployment } from '../../../../app/v2/operator/use-cases/reconcile-deployments.usecase' import { reconcileFixtures, reconcileFixturesParams } from './params' describe('Deployment on existing circle', () => { it('returns empty array for the first reconcile loop on same circle that already had deployments', () => { const params = reconcileFixturesParams.paramsWithPreviousDeployment const currentDeployment = reconcileFixtures.currentDeploymentId - const reconcile = new Reconcile() + const reconcile = new ReconcileDeployment() expect(reconcile.specsByDeployment(params, currentDeployment)).toEqual([]) }) it('returns list of previous deployment specs', () => { const params = reconcileFixturesParams.paramsWithPreviousDeployment const previousDeployment = reconcileFixtures.previousDeploymentId - const reconcile = new Reconcile() + const reconcile = new ReconcileDeployment() const ids = reconcile.specsByDeployment(params, previousDeployment).map(s => s.metadata.labels.deployment_id) expect(ids).toEqual([previousDeployment, previousDeployment]) }) @@ -27,7 +27,7 @@ describe('Deployment on existing circle', () => { const params = reconcileFixturesParams.paramsWithPreviousDeployment const previousDeployment = reconcileFixtures.previousDeploymentId const currentDeployment = reconcileFixtures.currentDeploymentId - const reconcile = new Reconcile() + const reconcile = new ReconcileDeployment() const currentSpecs = reconcile.specsByDeployment(params, currentDeployment) const previousSpecs = reconcile.specsByDeployment(params, previousDeployment) expect(reconcile.checkConditions(currentSpecs)).toEqual(false) @@ -35,7 +35,7 @@ describe('Deployment on existing circle', () => { }) it('concatenates deployments and services from previous and current deployment', () => { - const reconcile = new Reconcile() + const reconcile = new ReconcileDeployment() const cdConfig = new CdConfigurationEntity( CdTypeEnum.OCTOPIPE, { provider: ClusterProviderEnum.DEFAULT, gitProvider: GitProvidersEnum.GITHUB, gitToken: 'my-token', namespace: 'my-namespace' }, diff --git a/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-circle-dr.ts b/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-circle-dr.ts index c5f0652a74..e9d66686d9 100644 --- a/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-circle-dr.ts +++ b/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-circle-dr.ts @@ -3,7 +3,10 @@ export const noRepeatedCircleDr = { kind: 'DestinationRule', metadata: { name: 'A', - namespace: 'sandbox' + namespace: 'sandbox', + annotations: { + circles: '["default-circle-id","normal-circle-id"]' + } }, spec: { host: 'A', diff --git a/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-default-circle-dr.ts b/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-default-circle-dr.ts index 98f9efa837..fc834ec558 100644 --- a/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-default-circle-dr.ts +++ b/butler/src/tests/v2/unit/utils/fixtures/deployment/no-repeated-default-circle-dr.ts @@ -3,7 +3,10 @@ export const noRepeatedDefaultCircleDr = { kind: 'DestinationRule', metadata: { name: 'A', - namespace: 'sandbox' + namespace: 'sandbox', + annotations: { + circles: '["default-circle-id","normal-circle-id"]' + } }, spec: { host: 'A', diff --git a/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-circle-dr.ts b/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-circle-dr.ts index beb216b14a..a336552fbc 100644 --- a/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-circle-dr.ts +++ b/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-circle-dr.ts @@ -3,7 +3,10 @@ export const noRepeatedUndeploymentCircleDr = { kind: 'DestinationRule', metadata: { name: 'A', - namespace: 'sandbox' + namespace: 'sandbox', + annotations: { + circles: '["default-circle-id"]' + } }, spec: { host: 'A', diff --git a/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-default-circle-dr.ts b/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-default-circle-dr.ts index d831d105df..58b674a16f 100644 --- a/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-default-circle-dr.ts +++ b/butler/src/tests/v2/unit/utils/fixtures/undeployment/no-repeated-undeployment-default-circle-dr.ts @@ -3,7 +3,10 @@ export const noRepeatedUndeploymentDefaultCircleDr = { kind: 'DestinationRule', metadata: { name: 'A', - namespace: 'sandbox' + namespace: 'sandbox', + annotations: { + circles: '["normal-circle-id"]' + } }, spec: { host: 'A',