Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rocket health sensors #1546

Merged
merged 9 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@boostercloud/framework-core",
"comment": "Health sensors for Rockets",
"type": "minor"
}
],
"packageName": "@boostercloud/framework-core"
}
175 changes: 104 additions & 71 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
HealthIndicatorsResult,
UserEnvelope,
} from '@boostercloud/framework-types'
import { childrenHealthProviders, isEnabled, metadataFromId, rootHealthProviders } from './health-utils'
import { childHealthProviders, isEnabled, metadataFromId, rootHealthProviders } from './health-utils'
import { createInstance } from '@boostercloud/framework-common-helpers'
import { defaultBoosterHealthIndicators } from './health-indicators'
import { BoosterTokenVerifier } from '../../booster-token-verifier'
Expand Down Expand Up @@ -51,14 +51,14 @@ export class BoosterHealthService {
if (!indicatorResult) {
continue
}
const childrens = childrenHealthProviders(current, healthProviders)
const children = childHealthProviders(current, healthProviders)
const newResult: HealthIndicatorsResult = {
...indicatorResult,
name: current.healthIndicatorConfiguration.name,
id: current.healthIndicatorConfiguration.id,
}
if (childrens && childrens?.length > 0) {
newResult.components = await this.boosterHealthProviderResolver(childrens, healthProviders)
if (children && children?.length > 0) {
newResult.components = await this.boosterHealthProviderResolver(children, healthProviders)
}
result.push(newResult)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { BoosterDatabaseHealthIndicator } from './booster-database-health-indica
import { BoosterDatabaseEventsHealthIndicator } from './booster-database-events-health-indicator'
import { BoosterFunctionHealthIndicator } from './booster-function-health-indicator'
import { BoosterDatabaseReadModelsHealthIndicator } from './booster-database-read-models-health-indicator'
import { RocketsHealthIndicator } from './rockets-health-indicator'

function buildMetadata(
config: BoosterConfig,
Expand Down Expand Up @@ -59,11 +60,18 @@ export function defaultBoosterHealthIndicators(config: BoosterConfig): Record<st
'Booster Database ReadModels',
BoosterDatabaseReadModelsHealthIndicator
)
const rocketFunctions = buildMetadata(
config,
BOOSTER_HEALTH_INDICATORS_IDS.ROCKETS,
'Rockets',
RocketsHealthIndicator
)
return {
[root.healthIndicatorConfiguration.id]: root,
[boosterFunction.healthIndicatorConfiguration.id]: boosterFunction,
[boosterDatabase.healthIndicatorConfiguration.id]: boosterDatabase,
[databaseEvents.healthIndicatorConfiguration.id]: databaseEvents,
[databaseReadModels.healthIndicatorConfiguration.id]: databaseReadModels,
[rocketFunctions.healthIndicatorConfiguration.id]: rocketFunctions,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
BOOSTER_HEALTH_INDICATORS_IDS,
BoosterConfig,
HealthIndicatorMetadata,
HealthIndicatorsResult,
HealthStatus,
} from '@boostercloud/framework-types'

export class RocketsHealthIndicator {
public async health(
config: BoosterConfig,
healthIndicatorMetadata: HealthIndicatorMetadata
): Promise<HealthIndicatorsResult> {
const results = await config.provider.sensor.areRocketFunctionsUp(config)
if (Object.keys(results).length === 0) {
return {
name: 'Rockets',
id: BOOSTER_HEALTH_INDICATORS_IDS.ROCKETS,
status: HealthStatus.UNKNOWN,
details: {
reason: 'No Rockets found',
},
}
}
return {
name: 'Rockets',
id: BOOSTER_HEALTH_INDICATORS_IDS.ROCKETS,
status: this.getOverAllHealthStatus(results),
components: Object.entries(results).map(([rocketFunctionApp, status]) => {
return {
name: rocketFunctionApp,
id: rocketFunctionApp, // @TODO: put the rocket's id instead of its name
status: status ? HealthStatus.UP : HealthStatus.DOWN,
}
}),
}
}

private getOverAllHealthStatus(results: { [key: string]: boolean }): HealthStatus {
const statusValues = Object.values(results)

if (statusValues.every((status) => status)) {
return HealthStatus.UP
}

if (statusValues.every((status) => !status)) {
return HealthStatus.DOWN
}

return HealthStatus.PARTIALLY_UP
}
}
2 changes: 1 addition & 1 deletion packages/framework-core/src/sensor/health/health-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function rootHealthProviders(
)
}

export function childrenHealthProviders(
export function childHealthProviders(
healthIndicatorMetadata: HealthIndicatorMetadata,
healthProviders: Record<string, HealthIndicatorMetadata>
): Array<HealthIndicatorMetadata> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ function defaultSensor(token?: string, url?: string) {
rawRequestToHealthEnvelope: fake(() => {
return { token: token, componentPath: url }
}),
areRocketFunctionsUp: fake(() => ''),
}
}

Expand Down Expand Up @@ -289,24 +290,12 @@ function expectDatabaseEventsWithDetails(databaseEvents: any, status: string, de
}

function expectDatabaseReadModels(databaseReadModels: any, status: string): void {
expectDefaultResult(
databaseReadModels,
status,
'booster/database/readmodels',
'Booster Database ReadModels',
0
)
expectDefaultResult(databaseReadModels, status, 'booster/database/readmodels', 'Booster Database ReadModels', 0)
expect(databaseReadModels.details).to.be.undefined
}

function expectDatabaseReadModelsWithDetails(databaseReadModels: any, status: string, details: any): void {
expectDefaultResult(
databaseReadModels,
status,
'booster/database/readmodels',
'Booster Database ReadModels',
0
)
expectDefaultResult(databaseReadModels, status, 'booster/database/readmodels', 'Booster Database ReadModels', 0)
expect(databaseReadModels.details).to.be.deep.eq(details)
}

Expand Down
12 changes: 6 additions & 6 deletions packages/framework-core/test/sensor/health/health-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HealthIndicatorMetadata } from '@boostercloud/framework-types'
import 'mocha'
import {
childrenHealthProviders,
childHealthProviders,
isEnabled,
metadataFromId,
parentId,
Expand Down Expand Up @@ -165,13 +165,13 @@ describe('Health utils', () => {
})

it('childrenHealthProviders', () => {
expect(childrenHealthProviders(root, healthProviders)).to.be.deep.equal([rootChildren1, rootChildren2])
expect(childrenHealthProviders(rootChildren1, healthProviders)).to.be.deep.equal([
expect(childHealthProviders(root, healthProviders)).to.be.deep.equal([rootChildren1, rootChildren2])
expect(childHealthProviders(rootChildren1, healthProviders)).to.be.deep.equal([
rootChildren1Children1,
rootChildren1Children2,
])
expect(childrenHealthProviders(rootChildren1Children1, healthProviders)).to.be.deep.equal([])
expect(childrenHealthProviders(rootChildren1Children2, healthProviders)).to.be.deep.equal([])
expect(childrenHealthProviders(rootChildren2, healthProviders)).to.be.deep.equal([])
expect(childHealthProviders(rootChildren1Children1, healthProviders)).to.be.deep.equal([])
expect(childHealthProviders(rootChildren1Children2, healthProviders)).to.be.deep.equal([])
expect(childHealthProviders(rootChildren2, healthProviders)).to.be.deep.equal([])
})
})
2 changes: 2 additions & 0 deletions packages/framework-provider-aws/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export const Provider = (rockets?: RocketDescriptor[]): ProviderLibrary => {
rawRequestToHealthEnvelope: (rawRequest: unknown): HealthEnvelope => {
throw new Error('Not implemented')
},
areRocketFunctionsUp: async (config: BoosterConfig): Promise<{ [key: string]: boolean }> =>
notImplementedResult(),
},
// ProviderInfrastructureGetter
infrastructure: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export class ApplicationBuilder {
const azureStack = await this.synthApplication(app, webPubSubBaseFile)
const rocketBuilder = new RocketBuilder(this.config, azureStack.applicationStack, this.rockets)
await rocketBuilder.synthRocket()
// add rocket-related env vars to main function app settings
azureStack.addAppSettingsToFunctionApp(this.rockets)
app.synth()

azureStack.applicationStack.functionDefinitions = FunctionZip.buildAzureFunctions(this.config)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
import { Construct } from 'constructs'
import { TerraformStack } from 'cdktf'
import { Fn, TerraformStack } from 'cdktf'
import { ApplicationSynth } from './synth/application-synth'
import { ApplicationSynthStack } from './types/application-synth-stack'
import { InfrastructureRocket } from './rockets/infrastructure-rocket'
import { environmentVarNames } from '@boostercloud/framework-provider-azure'

export class AzureStack extends TerraformStack {
readonly applicationStack: ApplicationSynthStack
readonly defaultApplicationSettings: { [key: string]: string }

constructor(scope: Construct, name: string, zipFile?: string) {
super(scope, name)

const applicationSynth = new ApplicationSynth(this)
this.applicationStack = applicationSynth.synth(zipFile)
this.defaultApplicationSettings = applicationSynth.buildDefaultAppSettings(
this.applicationStack,
this.applicationStack.storageAccount!,
'func'
)
}

public addAppSettingsToFunctionApp(rockets?: InfrastructureRocket[]): void {
if (!this.applicationStack.functionApp) {
throw new Error('Function app not defined')
}

const functionAppNames = rockets
? rockets
.map((rocket: InfrastructureRocket) =>
rocket.getFunctionAppName ? rocket.getFunctionAppName(this.applicationStack) : ''
)
.join(',')
: ''

this.applicationStack.functionApp.appSettings = Fn.merge([
this.defaultApplicationSettings,
{ [environmentVarNames.rocketFunctionAppNames]: functionAppNames },
])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ import { TerraformWebPubsubHub } from './terraform-web-pubsub-hub'
import { TerraformWebPubSubExtensionKey } from './terraform-web-pub-sub-extension-key'
import { TerraformEventHubNamespace } from './terraform-event-hub-namespace'
import { TerraformEventHub } from './terraform-event-hub'
import { windowsFunctionApp } from '@cdktf/provider-azurerm'
import { storageAccount, windowsFunctionApp } from '@cdktf/provider-azurerm'
import { TerraformNetworkSecurityGroup } from './gateway/terraform-network-security-group'
import { TerraformVirtualNetwork } from './gateway/terraform-virtual-network'
import { TerraformPublicIp } from './gateway/terraform-public-ip'
import { TerraformPublicIpData } from './gateway/terraform-public-ip-data'
import { TerraformSubnet } from './gateway/terraform-subnet'
import { TerraformSubnetSecurity } from './gateway/terraform-subnet-security'
import { BASIC_SERVICE_PLAN } from '../constants'
import { TerraformFunctionAppSettings } from './terraform-function-app-settings'

export class ApplicationSynth {
readonly config: BoosterConfig
Expand Down Expand Up @@ -121,7 +122,6 @@ export class ApplicationSynth {
): windowsFunctionApp.WindowsFunctionApp {
return TerraformFunctionApp.build(
stack,
this.config,
stack.applicationServicePlan!,
stack.storageAccount!,
'func',
Expand All @@ -139,11 +139,12 @@ export class ApplicationSynth {
stack.eventConsumerStorageAccount = TerraformStorageAccount.build(stack, 'sc')
stack.eventConsumerFunctionApp = TerraformFunctionApp.build(
stack,
this.config,
stack.eventConsumerServicePlan,
stack.eventConsumerStorageAccount,
'fhub',
stack.streamFunctionAppName
stack.streamFunctionAppName,
undefined,
this.buildDefaultAppSettings(stack, stack.eventConsumerStorageAccount, 'fhub')
)
if (!stack.containers) {
stack.containers = []
Expand All @@ -164,4 +165,12 @@ export class ApplicationSynth {
stack.webPubSubHub = TerraformWebPubsubHub.build(stack)
}
}

public buildDefaultAppSettings(
stack: ApplicationSynthStack,
storageAccount: storageAccount.StorageAccount,
suffixName: string
) {
return TerraformFunctionAppSettings.build(stack, this.config, storageAccount!, suffixName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { environmentVarNames } from '@boostercloud/framework-provider-azure'
import { ApplicationSynthStack } from '../types/application-synth-stack'
import { toTerraformName } from '../helper/utils'
import { BoosterConfig } from '@boostercloud/framework-types'
import { storageAccount } from '@cdktf/provider-azurerm'

export class TerraformFunctionAppSettings {
static build(
{ appPrefix, cosmosdbDatabase, domainNameLabel, eventHubNamespace, eventHub, webPubSub }: ApplicationSynthStack,
config: BoosterConfig,
storageAccount: storageAccount.StorageAccount,
suffixName: string
): { [key: string]: string } {
if (!cosmosdbDatabase) {
throw new Error('Undefined cosmosdbDatabase resource')
}
const id = toTerraformName(appPrefix, suffixName)
const eventHubConnectionString =
eventHubNamespace?.defaultPrimaryConnectionString && eventHub?.name
? `${eventHubNamespace.defaultPrimaryConnectionString};EntityPath=${eventHub.name}`
: ''
const region = (process.env['REGION'] ?? '').toLowerCase().replace(/ /g, '')
return {
WEBSITE_RUN_FROM_PACKAGE: '1',
WEBSITE_CONTENTSHARE: id,
...config.env,
WebPubSubConnectionString: webPubSub?.primaryConnectionString || '',
BOOSTER_ENV: config.environmentName,
[environmentVarNames.restAPIURL]: `http://${domainNameLabel}.${region}.cloudapp.azure.com/${config.environmentName}`,
[environmentVarNames.eventHubConnectionString]: eventHubConnectionString,
[environmentVarNames.eventHubName]: config.resourceNames.streamTopic,
[environmentVarNames.eventHubMaxRetries]:
config.eventStreamConfiguration.parameters?.maxRetries?.toString() || '5',
[environmentVarNames.eventHubMode]: config.eventStreamConfiguration.parameters?.mode || 'exponential',
COSMOSDB_CONNECTION_STRING: `AccountEndpoint=https://${cosmosdbDatabase.name}.documents.azure.com:443/;AccountKey=${cosmosdbDatabase.primaryKey};`,
WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: storageAccount.primaryConnectionString, // Terraform bug: https://github.com/hashicorp/terraform-provider-azurerm/issues/16650
BOOSTER_APP_NAME: process.env['BOOSTER_APP_NAME'] ?? '',
}
}
}
Loading
Loading