From a7ae00efcf8c035a4ef82da4fdbb840f716be9e9 Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Tue, 30 Jun 2026 14:53:37 -0700 Subject: [PATCH] feat(lfc)!: support inline configuration service data in lifecycle.yaml Allow `configuration` services to declare key/value data inline in lifecycle.yaml. The data is materialized onto the per-build deployable.env via YamlService.getEnvironmentVariables (Option A), so it flows through the existing full-yaml env pipeline rather than the configurations table. Consuming services reference keys normally; external-secret refs ({{aws:...}}) resolve transitively through the consumer's own deploy pipeline. - schema_1_0_0: the configuration block now takes `data` (required, string map) and optional `branchName`; `defaultTag` is removed. - Configuration deploys are skipped in resolve() so their (possibly secret-bearing) data is never persisted as plaintext on the unused config deploy's env. - Classic (non-full-yaml) builds keep the configurations-table read; a YAML-defined config without a serviceId now warns instead of being silently dropped (no values logged). - Remove the dead, superseded server/models/config module (no prod importers; the live path uses server/models/yaml). BREAKING CHANGE: the inline `configuration` block in lifecycle.yaml no longer accepts `defaultTag` and now requires a `data` map of string values. Existing inline configuration blocks using the old shape will fail schema validation. Configs referenced by serviceId (classic mode) are unaffected. --- docs/schema/yaml/1.0.0.yaml | 6 +- src/server/lib/buildEnvVariables.ts | 8 + src/server/lib/envVariables.ts | 25 +- src/server/lib/jsonschema/schemas/1.0.0.json | 12 +- src/server/lib/tests/envVariables.test.ts | 53 ++ .../yamlSchemas/schema_1_0_0/schema_1_0_0.ts | 4 +- src/server/models/config/index.test.ts | 456 ------------------ src/server/models/config/index.ts | 138 ------ src/server/models/config/types.ts | 140 ------ src/server/models/config/utils.ts | 75 --- src/server/models/yaml/YamlService.ts | 10 +- .../models/yaml/tests/YamlService.test.ts | 17 +- 12 files changed, 116 insertions(+), 828 deletions(-) delete mode 100644 src/server/models/config/index.test.ts delete mode 100644 src/server/models/config/index.ts delete mode 100644 src/server/models/config/types.ts delete mode 100644 src/server/models/config/utils.ts diff --git a/docs/schema/yaml/1.0.0.yaml b/docs/schema/yaml/1.0.0.yaml index 315c6f32..2e7296dd 100644 --- a/docs/schema/yaml/1.0.0.yaml +++ b/docs/schema/yaml/1.0.0.yaml @@ -739,10 +739,10 @@ services: arguments: '' # @param services.configuration configuration: - # @param services.configuration.defaultTag (required) - defaultTag: '' - # @param services.configuration.branchName (required) + # @param services.configuration.branchName branchName: '' + # @param services.configuration.data (required) + data: # @param services.dev dev: # @param services.dev.image (required) diff --git a/src/server/lib/buildEnvVariables.ts b/src/server/lib/buildEnvVariables.ts index 973a852e..ff1fb3a3 100644 --- a/src/server/lib/buildEnvVariables.ts +++ b/src/server/lib/buildEnvVariables.ts @@ -140,6 +140,14 @@ export class BuildEnvironmentVariables extends EnvironmentVariables { const useDeafulttUUID = !Array.isArray(build?.enabledFeatures) || !build.enabledFeatures.includes(FeatureFlags.NO_DEFAULT_ENV_RESOLVE); const promises = deploys.map(async (deploy) => { + // Configuration deploys are never built or deployed; their data is consumed + // directly into availableEnv from deployable.env (see configurationServiceEnvironments). + // Skip patching deploy.env so we never persist the (possibly secret-ref-bearing) + // configuration data as plaintext on the unused configuration deploy. + if (build.enableFullYaml && deploy.deployable?.type === DeployTypes.CONFIGURATION) { + return; + } + await deploy .$query() .patch({ diff --git a/src/server/lib/envVariables.ts b/src/server/lib/envVariables.ts index 42bb38a1..682a6d0e 100644 --- a/src/server/lib/envVariables.ts +++ b/src/server/lib/envVariables.ts @@ -255,17 +255,36 @@ export abstract class EnvironmentVariables { if (!this?.db?.models) this.db = new Database(); const configurations = await Promise.all( - configurationDeploys.map((deploy) => { + configurationDeploys.map(async (deploy) => { + // Full-yaml: configuration data is declared inline in lifecycle.yaml and + // materialized onto the deployable's env at ingestion (see YamlService.getEnvironmentVariables). + // Source it directly from the deployable instead of the configurations table. + if (fullYamlSupport) { + return deploy.deployable?.env ?? null; + } + + // Classic: configuration data lives in the configurations table, keyed by the + // referenced serviceId + branchName. if (deploy.serviceId != null) { - return this.db.models.Configuration.query() + const configuration = await this.db.models.Configuration.query() .where('serviceId', deploy.serviceId) .where('key', deploy.branchName) .first(); + return configuration ? configuration.data : null; } + + // A YAML-defined configuration service has no serviceId, so in classic mode its + // data cannot be resolved and would be silently dropped. Warn so the misconfiguration + // is visible (configuration via YAML requires full-yaml support). + getLogger().warn( + { deployName: deploy.deployable?.name ?? deploy.service?.name }, + 'EnvVars: configuration service defined in YAML is ignored without full-yaml support' + ); + return null; }) ); - return _.compact(configurations.map((configuration) => (configuration ? configuration.data : null))); + return _.compact(configurations); } /** diff --git a/src/server/lib/jsonschema/schemas/1.0.0.json b/src/server/lib/jsonschema/schemas/1.0.0.json index b23d9ec1..59970005 100644 --- a/src/server/lib/jsonschema/schemas/1.0.0.json +++ b/src/server/lib/jsonschema/schemas/1.0.0.json @@ -1429,16 +1429,18 @@ "type": "object", "additionalProperties": false, "properties": { - "defaultTag": { - "type": "string" - }, "branchName": { "type": "string" + }, + "data": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ - "defaultTag", - "branchName" + "data" ] }, "dev": { diff --git a/src/server/lib/tests/envVariables.test.ts b/src/server/lib/tests/envVariables.test.ts index 21f56aa5..ad05b5fa 100644 --- a/src/server/lib/tests/envVariables.test.ts +++ b/src/server/lib/tests/envVariables.test.ts @@ -19,6 +19,8 @@ import { EnvironmentVariables } from '../envVariables'; import GlobalConfigService from 'server/services/globalConfig'; import { IServices } from 'server/services/types'; import * as models from 'server/models'; +import { DeployTypes } from 'shared/constants'; +import { Deploy } from 'server/models'; jest.mock('server/database'); jest.mock('redlock', () => { @@ -258,4 +260,55 @@ describe('EnvironmentVariables library', () => { const customRenderResult = JSON.parse(await envVariables.customRender(template, data, true, 'testns')); expect(customRenderResult).toEqual(result); }); + + describe('configurationServiceEnvironments', () => { + test('full-yaml: sources configuration data from the deployable env (not the configurations table)', async () => { + const configurationQuery = jest.spyOn(models.Configuration, 'query'); + const deploys = [ + { + deployable: { + type: DeployTypes.CONFIGURATION, + name: 'gdrx-auth-descope', + env: { DESCOPE_KEY: '{{aws:secret/path:key}}' }, + }, + }, + // A non-configuration deploy is ignored by this method. + { deployable: { type: DeployTypes.GITHUB, name: 'web', env: { FOO: 'bar' } } }, + ] as unknown as Deploy[]; + + const result = await envVariables.configurationServiceEnvironments(deploys, true); + + expect(result).toEqual([{ DESCOPE_KEY: '{{aws:secret/path:key}}' }]); + expect(configurationQuery).not.toHaveBeenCalled(); + }); + + test('classic: reads configuration data from the configurations table keyed by serviceId and branchName', async () => { + const first = jest.fn().mockResolvedValue({ data: { DESCOPE_KEY: 'classic-value' } }); + const whereInner = jest.fn().mockReturnValue({ first }); + const whereOuter = jest.fn().mockReturnValue({ where: whereInner }); + jest.spyOn(models.Configuration, 'query').mockReturnValue({ where: whereOuter } as any); + + const deploys = [ + { service: { type: DeployTypes.CONFIGURATION }, serviceId: 256, branchName: 'main' }, + ] as unknown as Deploy[]; + + const result = await envVariables.configurationServiceEnvironments(deploys, false); + + expect(result).toEqual([{ DESCOPE_KEY: 'classic-value' }]); + expect(whereOuter).toHaveBeenCalledWith('serviceId', 256); + expect(whereInner).toHaveBeenCalledWith('key', 'main'); + }); + + test('classic: a yaml-defined configuration (no serviceId) resolves to nothing instead of querying the table', async () => { + const configurationQuery = jest.spyOn(models.Configuration, 'query'); + const deploys = [ + { service: { type: DeployTypes.CONFIGURATION, name: 'gdrx-auth-descope' }, serviceId: null }, + ] as unknown as Deploy[]; + + const result = await envVariables.configurationServiceEnvironments(deploys, false); + + expect(result).toEqual([]); + expect(configurationQuery).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts b/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts index f1d394be..c7677c15 100644 --- a/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts +++ b/src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts @@ -292,10 +292,10 @@ const schema_1_0_0 = { type: 'object', additionalProperties: false, properties: { - defaultTag: { type: 'string' }, + data: { type: 'object', additionalProperties: { type: 'string' } }, branchName: { type: 'string' }, }, - required: ['defaultTag', 'branchName'], + required: ['data'], }, dev: { type: 'object', diff --git a/src/server/models/config/index.test.ts b/src/server/models/config/index.test.ts deleted file mode 100644 index 54922372..00000000 --- a/src/server/models/config/index.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import mockRedisClient from 'server/lib/__mocks__/redisClientMock'; -mockRedisClient(); - -import { YamlConfigParser } from 'server/lib/yamlConfigParser'; -import { YamlConfigValidator } from 'server/lib/yamlConfigValidator'; -import { DeployTypes, DEPLOY_TYPES_DICTIONARY as dtd } from 'shared/constants'; -import * as YamlService from 'server/models/config'; - -describe('Yaml Service', () => { - const lifecycleConfigContent: string = `--- - version: '1.0.0' - - environment: - webhooks: - - state: 'deployed' - name: 'e2e test' - pipelineId: '3084088f0a8080b' - trigger: 'webhook' - type: 'codefresh' - env: - branch: 'main' - - services: - - name: 'githubApp' - github: - repository: 'org/foobar' - branchName: 'main' - docker: - defaultTag: 'main' - app: - dockerfilePath: 'app1/app.Dockerfile' - env: - SOURCE: 'yaml' - TYPE: 'github' - init: - dockerfilePath: 'app1/init.Dockerfile' - env: - SOURCE: 'yaml' - TYPE: 'github-init' - - name: 'githubApp-with-after-build-pipeline-id' - github: - repository: 'org/foobar' - branchName: 'main' - docker: - defaultTag: 'main' - app: - afterBuildPipelineConfig: - afterBuildPipelineId: '8080a08b080ff' - detatchAfterBuildPipeline: true - description: 'after build pipeline' - dockerfilePath: 'app1/app.Dockerfile' - env: - SOURCE: 'yaml' - TYPE: 'github' - init: - dockerfilePath: 'app1/init.Dockerfile' - env: - SOURCE: 'yaml' - TYPE: 'github-init' - - name: 'codefreshApp' - codefresh: - repository: 'org/cwf' - branchName: 'main' - env: - SOURCE: 'yaml' - TYPE: 'codefresh' - TOKEN1: '8080a08b080ff' - - name: 'dockerApp' - docker: - defaultTag: 'latest' - dockerImage: 'postgres' - command: 'postgres' - arguments: '-c%%SPLIT%%max_connections= 3451%%SPLIT%%-c%%SPLIT%%shared_buffers=3GB' - env: - SOURCE: 'yaml' - TYPE: 'docker' - - name: 'externalHttpApp' - externalHttp: - defaultInternalHostname: 'externalHttpApp-dev-0' - defaultPublicUrl: 'externalHttpApp-dev-0.lifecycle.dev.example.com' - - name: 'auroraRestoreApp' - auroraRestore: - command: 'ls' - arguments: '-arg foobar' - - name: 'configurationApp' - configuration: - defaultTag: 'main' - branchName: 'main' -`; - - describe('getDeployType', () => { - test('GithubService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getDeployType(service)).toEqual(DeployTypes.GITHUB); - }); - - test('CodefreshService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'codefreshApp'); - - expect(YamlService.getDeployType(service)).toEqual(DeployTypes.CODEFRESH); - }); - - test('DockerService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'dockerApp'); - - expect(YamlService.getDeployType(service)).toEqual(DeployTypes.DOCKER); - }); - - test('ExternalHttpService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'externalHttpApp'); - - expect(YamlService.getDeployType(service)).toEqual(dtd.extneralHttp); - }); - - test('AuroraRestoreService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'auroraRestoreApp'); - - expect(YamlService.getDeployType(service)).toEqual(dtd.auroraRestore); - }); - - test('ConfigurationService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - const service = YamlService.getDeployingServicesByName(config, 'configurationApp'); - - expect(YamlService.getDeployType(service)).toEqual(DeployTypes.CONFIGURATION); - }); - }); - - describe('getEnvironmentVariables', () => { - test('GithubService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual({ - SOURCE: 'yaml', - TYPE: 'github', - }); - }); - - test('GithubService - Missing app env', () => { - const missingEnvVar: string = `--- - version: '1.0.0' - - environment: - webhooks: - - state: 'deployed' - name: 'e2e test' - pipelineId: '3084088f0a8080b' - trigger: 'webhook' - type: 'codefresh' - env: - branch: 'main' - - services: - - name: 'githubApp' - github: - repository: 'org/foobar' - branchName: 'main' - docker: - defaultTag: 'main' - app: - dockerfilePath: 'app1/app.Dockerfile' - - name: 'codefreshApp' - codefresh: - repository: 'org/cwf' - branchName: 'main' - env: - SOURCE: 'yaml' - TYPE: 'codefresh' - TOKEN1: '8080a08b080ff' - - name: 'dockerApp' - docker: - defaultTag: 'latest' - dockerImage: 'postgres' - command: 'postgres' - arguments: '-c%%SPLIT%%max_connections= 3451%%SPLIT%%-c%%SPLIT%%shared_buffers=3GB' - env: - SOURCE: 'yaml' - TYPE: 'docker' - - name: 'externalHttpApp' - externalHttp: - defaultInternalHostname: 'externalHttpApp-dev-0' - defaultPublicUrl: 'externalHttpApp-dev-0.lifecycle.dev.example.com' - - name: 'auroraRestoreApp' - auroraRestore: - command: 'ls' - arguments: '-arg foobar' - - name: 'configurationApp' - configuration: - defaultTag: 'main' - branchName: 'main' - `; - - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(missingEnvVar); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual(undefined); - }); - - test('CodefreshService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'codefreshApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual({ - SOURCE: 'yaml', - TYPE: 'codefresh', - TOKEN1: '8080a08b080ff', - }); - }); - - test('DockerService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'dockerApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual({ - SOURCE: 'yaml', - TYPE: 'docker', - }); - }); - - test('ExternalHttpService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'externalHttpApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual(undefined); - }); - - test('AuroraRestoreService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'auroraRestoreApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual(undefined); - }); - - test('ConfigurationService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'configurationApp'); - - expect(YamlService.getEnvironmentVariables(service)).toEqual(undefined); - }); - }); - - describe('getInitEnvironmentVariables', () => { - test('GithubService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual({ - SOURCE: 'yaml', - TYPE: 'github-init', - }); - }); - - test('GithubService - Missing app env', () => { - const missingEnvVar: string = `--- - version: '1.0.0' - - environment: - webhooks: - - state: 'deployed' - name: 'e2e test' - pipelineId: '3084088f0a8080b' - trigger: 'webhook' - type: 'codefresh' - env: - branch: 'main' - - services: - - name: 'githubApp' - github: - repository: 'org/foobar' - branchName: 'main' - docker: - defaultTag: 'main' - app: - dockerfilePath: 'app1/app.Dockerfile' - - name: 'codefreshApp' - codefresh: - repository: 'org/cwf' - branchName: 'main' - env: - SOURCE: 'yaml' - TYPE: 'codefresh' - TOKEN1: '8080a08b080ff' - - name: 'dockerApp' - docker: - defaultTag: 'latest' - dockerImage: 'postgres' - command: 'postgres' - arguments: '-c%%SPLIT%%max_connections= 3451%%SPLIT%%-c%%SPLIT%%shared_buffers=3GB' - env: - SOURCE: 'yaml' - TYPE: 'docker' - - name: 'externalHttpApp' - externalHttp: - defaultInternalHostname: 'externalHttpApp-dev-0' - defaultPublicUrl: 'externalHttpApp-dev-0.lifecycle.dev.example.com' - - name: 'auroraRestoreApp' - auroraRestore: - command: 'ls' - arguments: '-arg foobar' - - name: 'configurationApp' - configuration: - defaultTag: 'main' - branchName: 'main' - `; - - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(missingEnvVar); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('CodefreshService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'codefreshApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('DockerService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'dockerApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('ExternalHttpService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'externalHttpApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('AuroraRestoreService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'auroraRestoreApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('ConfigurationService', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - - const service = YamlService.getDeployingServicesByName(config, 'configurationApp'); - - expect(YamlService.getInitEnvironmentVariables(service)).toEqual(undefined); - }); - - test('isAfterBuildPipelineId should return value', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - const service = YamlService.getDeployingServicesByName(config, 'githubApp-with-after-build-pipeline-id'); - - expect(YamlService.getAfterBuildPipelineId(service)).toEqual('8080a08b080ff'); - }); - - test('isAfterBuildPipelineId should be undefined', () => { - const parser = new YamlConfigParser(); - - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getAfterBuildPipelineId(service)).toEqual(undefined); - }); - - test('getDetachAfterBuildPipeline should return value', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - const service = YamlService.getDeployingServicesByName(config, 'githubApp-with-after-build-pipeline-id'); - - expect(YamlService.getDetatchAfterBuildPipeline(service)).toEqual(true); - }); - - test('getDetachAfterBuildPipeline should be false', () => { - const parser = new YamlConfigParser(); - const config = parser.parseYamlConfigFromString(lifecycleConfigContent); - new YamlConfigValidator().validate_1_0_0(config); - const service = YamlService.getDeployingServicesByName(config, 'githubApp'); - - expect(YamlService.getDetatchAfterBuildPipeline(service)).toEqual(false); - }); - }); -}); diff --git a/src/server/models/config/index.ts b/src/server/models/config/index.ts deleted file mode 100644 index c562e24c..00000000 --- a/src/server/models/config/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DeployTypes, DEPLOY_TYPES_DICTIONARY as dtd } from 'shared/constants'; -import Repository from 'server/models/Repository'; -import { - isInObj, - getDeployType, - resolveRepository, - fetchLifecycleConfigByRepository, -} from 'server/models/config/utils'; -import { LifecycleConfig, Service } from 'server/models/config/types'; - -import { getLogger } from 'server/lib/logger'; - -export const isGithubServiceDockerConfig = (obj) => isInObj(obj, 'dockerfilePath'); -export const isDockerServiceConfig = (obj) => isInObj(obj, 'dockerImage'); - -export function getEnvironmentVariables(service: Service) { - const deployType = getDeployType(service); - if (![dtd.github, dtd.codefresh, dtd.docker].includes(deployType)) return; - if (deployType === dtd.github) return service.github?.docker?.app?.env || service.github.env || undefined; - return service[deployType]?.env; -} - -export const getInitEnvironmentVariables = (service: Service) => { - if (getDeployType(service) !== dtd.github) return; - return service?.github?.docker?.init?.env; -}; - -export const getAfterBuildPipelineId = (service: Service) => { - const deployType = getDeployType(service); - if (deployType !== dtd.github) return; - return service.github?.docker?.app?.afterBuildPipelineConfig?.afterBuildPipelineId; -}; - -export const getDetatchAfterBuildPipeline = (service: Service) => { - const deployType = getDeployType(service); - if (deployType !== dtd.github) return false; - return service.github?.docker?.app?.afterBuildPipelineConfig?.detatchAfterBuildPipeline || false; -}; - -export const getDockerImage = (service: Service) => { - const deployType = getDeployType(service); - if (deployType !== dtd.docker) return; - return service.docker?.dockerImage; -}; - -export function getRepositoryName(service: Service): string { - const deployType = getDeployType(service); - if (![dtd.github, dtd.codefresh].includes(deployType)) return; - return service[deployType]?.repository; -} - -export function getBranchName(service: Service) { - const deployType = getDeployType(service); - if (![dtd.github, dtd.codefresh].includes(deployType)) return; - return service[deployType]?.branchName; -} - -export function getDefaultTag(service: Service) { - const deployType = getDeployType(service); - if (![dtd.github, dtd.docker].includes(deployType)) return; - if (deployType === dtd.github) return service.github?.docker?.defaultTag; - return service.docker?.defaultTag; -} - -export const getAppDockerConfig = (service: Service) => { - const deployType = getDeployType(service); - if (![dtd.github, dtd.docker].includes(deployType)) return; - if (deployType === dtd.github) return service.github?.docker?.app; - return service.docker; -}; - -export const getInitDockerConfig = (service: Service) => { - if (getDeployType(service) !== DeployTypes.GITHUB) return; - return service?.github?.docker?.init; -}; - -export const getPort = (service: Service) => { - const deployType = getDeployType(service); - if (![dtd.github, dtd.docker].includes(deployType)) return; - const ports = deployType === dtd.github ? service.github?.docker?.app?.ports : service.docker?.ports; - return ports ? ports.toString() : '8080'; -}; - -export const getDeploymentConfig = (service: Service) => { - const deployType = getDeployType(service); - if (![dtd.github, dtd.docker, dtd.codefresh].includes(deployType)) return; - return service[deployType]?.deployment; -}; - -export const getDeployPipelineConfig = (service: Service) => { - const deployType = getDeployType(service); - if (deployType !== dtd.codefresh) return; - return service.codefresh?.deploy; -}; - -export const getDestroyPipelineConfig = (service: Service) => { - const deployType = getDeployType(service); - if (deployType !== dtd.codefresh) return; - return service.codefresh?.destroy; -}; - -export const fetchLifecycleConfig = async (repositoryName: string, branchName: string) => { - if (!repositoryName || !branchName) return; - try { - const repository: Repository = await resolveRepository(repositoryName); - if (!repository) throw new Error(`Unable to resolve repository ${repositoryName}`); - const config = await fetchLifecycleConfigByRepository(repository, branchName); - return config; - } catch (err) { - getLogger().error( - { error: err instanceof Error ? err.message : String(err) }, - `Failed to fetch config: repository=${repositoryName} branch=${branchName}` - ); - } -}; - -export const getDeployingServicesByName = (config: LifecycleConfig, serviceName: string) => { - if (config?.services?.length === 0 || !serviceName) return; - return config.services.find(({ name }) => serviceName.localeCompare(name) === 0); -}; - -export { getDeployType, fetchLifecycleConfigByRepository, resolveRepository }; diff --git a/src/server/models/config/types.ts b/src/server/models/config/types.ts deleted file mode 100644 index fe9594c6..00000000 --- a/src/server/models/config/types.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DeploymentConfig } from 'server/models/yaml/YamlService'; - -export interface DependencyService { - name?: string; - repository?: string; - branch?: string; - serviceId?: number; -} - -export interface ExternalHttpService { - defaultInternalHostname: string; - defaultPublicUrl: string; -} - -export interface AuroraRestoreService { - command: string; - arguments: string; -} - -export interface ConfigurationService { - defaultTag: string; - branchName: string; -} - -export interface AfterBuildPipelineConfig { - afterBuildPipelineId: string; - detatchAfterBuildPipeline?: boolean; - description?: string; -} - -export interface GithubServiceAppDockerConfig { - dockerfilePath: string; - command?: string; - arguments?: string; - env?: Record; - ports?: number[]; - afterBuildPipelineConfig?: AfterBuildPipelineConfig; -} - -export interface InitDockerConfig { - dockerfilePath: string; - command?: string; - arguments?: string; - env?: Record; -} - -export interface GithubService { - env?: Record; - repository?: string; - branchName?: string; - docker?: { - defaultTag?: string; - app?: GithubServiceAppDockerConfig; - init?: InitDockerConfig; - }; - deployment?: DeploymentConfig; -} - -export interface DockerService { - dockerImage: string; - defaultTag: string; - command?: string; - arguments?: string; - env?: Record; - ports?: number[]; - deployment?: DeploymentConfig; - dockerfilePath: string; -} - -export interface CodefreshConfig { - pipelineId: string; - trigger: string; -} - -export interface CodefreshService { - repository: string; - branchName: string; - env?: Record; - deploy?: CodefreshConfig; - destroy?: CodefreshConfig; - deployment?: DeploymentConfig; -} -export interface Service { - name: string; - defaultUUID?: string; - requires?: DependencyService[]; - github?: GithubService; - docker?: DockerService; - codefresh?: CodefreshService; - configuration?: ConfigurationService; - externalHttp?: ExternalHttpService; - auroraRestore?: AuroraRestoreService; -} - -export interface Webhook { - name?: string; - description?: string; - state: string; - type: string; - pipelineId: string; - trigger: string; - env: Record; -} - -export interface Environment { - defaultServices?: DependencyService[]; - optionalServices?: DependencyService[]; - webhooks?: Webhook[]; -} - -export interface LifecycleConfigVersion { - version: string; -} - -export interface LifecycleConfig { - version: string; - environment: Environment; - service?: { - github?: { - env?: Record; - }; - }; - services: Service[]; -} diff --git a/src/server/models/config/utils.ts b/src/server/models/config/utils.ts deleted file mode 100644 index bdebce94..00000000 --- a/src/server/models/config/utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright 2025 GoodRx, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ref, raw } from 'objection'; -import { DEPLOY_TYPES } from 'shared/constants'; -import { YamlConfigValidator } from 'server/lib/yamlConfigValidator'; -import { YamlConfigParser } from 'server/lib/yamlConfigParser'; -import Repository from 'server/models/Repository'; -import { Service } from 'server/models/yaml/types'; - -import { getLogger } from 'server/lib/logger'; - -export const isInObj = (obj, key) => (!obj ? false : key in obj); - -export const getDeployType = (service: Service): string => - Object.keys(service).find((key) => DEPLOY_TYPES.includes(key)); - -export const resolveRepository = async (repositoryFullName: string) => { - if (!repositoryFullName) return; - try { - const key = ref('repositories.fullName').castText(); - const name = repositoryFullName.toLowerCase(); - const repositories = await Repository.query() - .where(raw('LOWER(??)', [key]), '=', name) - .catch((error) => { - getLogger().error({ error }, `Repository: not found name=${repositoryFullName}`); - return null; - }); - if (!repositories || repositories?.length === 0) { - throw new Error(`Unable to find ${repositoryFullName} from Lifecycle Database`); - } - return repositories[0]; - } catch (err) { - getLogger().error({ error: err }, `Repository: resolution failed name=${repositoryFullName}`); - } -}; - -export const fetchLifecycleConfigByRepository = async (repository: Repository, branchName: string) => { - if (!repository || !branchName) return null; - const parser = new YamlConfigParser(); - try { - const name = repository?.fullName; - const isClassicModeOnly = repository?.defaultEnvironment?.classicModeOnly ?? false; - const config = !isClassicModeOnly ? await parser.parseYamlConfigFromBranch(name, branchName) : null; - if (!config) throw new Error(`Unable to fetch configuration from ${name}/${branchName}`); - const configVersion = config?.version; - if (!configVersion) throw new Error(`YAML Config version is missing for ${name}/${branchName}`); - const validator = new YamlConfigValidator(); - const isConfigValid = validator.validate(configVersion, config); - if (!isConfigValid) { - getLogger().error(`Config: validation failed repo=${name}/${branchName} version=${configVersion}`); - // TODO: This is a temporary fix to allow the UI to display the config - // throw new Error( - // `YAML Config validation failed for ${name}/${branchName} using version Lifecyle Yaml version=${configVersion}` - // ); - } - return config; - } catch (err) { - getLogger().error({ error: err }, `Config: fetch failed`); - return null; - } -}; diff --git a/src/server/models/yaml/YamlService.ts b/src/server/models/yaml/YamlService.ts index 0b54de98..365ed0da 100644 --- a/src/server/models/yaml/YamlService.ts +++ b/src/server/models/yaml/YamlService.ts @@ -174,8 +174,8 @@ export interface AuroraRestoreServiceConfig extends Service { } export interface ConfigurationService extends Service { readonly configuration: { - readonly defaultTag: string; - readonly branchName: string; + readonly data: Record; + readonly branchName?: string; }; } @@ -451,6 +451,12 @@ export function getEnvironmentVariables(service: Service): Record { arguments: '-arg foobar' - name: 'configurationApp' configuration: - defaultTag: 'main' + data: + SOURCE: 'yaml' + TYPE: 'configuration' branchName: 'main' `; @@ -649,7 +651,9 @@ services: arguments: '-arg foobar' - name: 'configurationApp' configuration: - defaultTag: 'main' + data: + SOURCE: 'yaml' + TYPE: 'configuration' branchName: 'main' `; @@ -716,7 +720,10 @@ services: const service: YamlService.Service = YamlService.getDeployingServicesByName(config, 'configurationApp'); - expect(YamlService.getEnvironmentVariables(service)).toEqual(undefined); + expect(YamlService.getEnvironmentVariables(service)).toEqual({ + SOURCE: 'yaml', + TYPE: 'configuration', + }); }); }); @@ -784,7 +791,9 @@ services: arguments: '-arg foobar' - name: 'configurationApp' configuration: - defaultTag: 'main' + data: + SOURCE: 'yaml' + TYPE: 'configuration' branchName: 'main' `;