Skip to content
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
6 changes: 3 additions & 3 deletions docs/schema/yaml/1.0.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions src/server/lib/buildEnvVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
25 changes: 22 additions & 3 deletions src/server/lib/envVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
12 changes: 7 additions & 5 deletions src/server/lib/jsonschema/schemas/1.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
53 changes: 53 additions & 0 deletions src/server/lib/tests/envVariables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
4 changes: 2 additions & 2 deletions src/server/lib/yamlSchemas/schema_1_0_0/schema_1_0_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading