-
Notifications
You must be signed in to change notification settings - Fork 722
Expand file tree
/
Copy pathConfigLoader.ts
More file actions
190 lines (160 loc) · 6.36 KB
/
ConfigLoader.ts
File metadata and controls
190 lines (160 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { getParameter, getParameters } from '@aws-github-runner/aws-ssm-util';
import { RunnerMatcherConfig } from './sqs';
import { logger } from '@aws-github-runner/aws-powertools-util';
/**
* Base class for loading configuration from environment variables and SSM parameters.
*
* @remarks
* To avoid usages or checking values can be undefined we assume that configuration is
* set to
* - empty string if the property is not relevant
* - empty list if the property is not relevant
*/
abstract class BaseConfig {
static instance: BaseConfig | null = null;
configLoadingErrors: string[] = [];
static async load<T extends BaseConfig>(): Promise<T> {
if (!this.instance) {
this.instance = new (this as unknown as { new (): T })();
await this.instance.loadConfig();
if (this.instance.configLoadingErrors.length > 0) {
logger.debug('Failed to load config', {
config: this.instance.logOjbect,
errors: this.instance.configLoadingErrors,
});
throw new Error(`Failed to load config: ${this.instance.configLoadingErrors.join(', ')}`);
}
logger.debug('Config loaded', { config: this.instance.logOjbect() });
} else {
logger.debug('Config already loaded', { config: this.instance.logOjbect() });
}
return this.instance as T;
}
static reset(): void {
this.instance = null;
}
abstract loadConfig(): Promise<void>;
protected loadEnvVar<T>(envVar: string, propertyName: keyof this, defaultValue?: T): void {
logger.debug(`Loading env var for ${String(propertyName)}`, { envVar });
if (!(envVar == undefined || envVar === 'null')) {
this.loadProperty(propertyName, envVar);
} else if (defaultValue !== undefined) {
this[propertyName] = defaultValue as unknown as this[keyof this];
} else {
const errorMessage = `Environment variable for ${String(propertyName)} is not set and no default value provided.`;
this.configLoadingErrors.push(errorMessage);
}
}
protected async loadParameter(paramPath: string, propertyName: keyof this): Promise<void> {
logger.debug(`Loading parameter for ${String(propertyName)} from path ${paramPath}`);
await getParameter(paramPath)
.then((value) => {
this.loadProperty(propertyName, value);
})
.catch((error) => {
const errorMessage = `Failed to load parameter for ${String(propertyName)} from path ${paramPath}: ${(error as Error).message}`;
this.configLoadingErrors.push(errorMessage);
});
}
private loadProperty(propertyName: keyof this, value: string) {
try {
this[propertyName] = JSON.parse(value) as unknown as this[keyof this];
} catch {
this[propertyName] = value as unknown as this[keyof this];
}
}
// create a log object without secrets
protected logOjbect(): this {
const config = { ...this };
for (const key in config) {
if (key.toLowerCase().includes('secret') && config[key]) {
config[key as keyof this] = '***' as unknown as this[keyof this];
}
}
return config;
}
}
abstract class MatcherAwareConfig extends BaseConfig {
matcherConfig: RunnerMatcherConfig[] = [];
protected async loadMatcherConfig(paramPathsEnv: string) {
if (!paramPathsEnv || paramPathsEnv === 'undefined' || paramPathsEnv === 'null' || !paramPathsEnv.includes(':')) {
// Single path or invalid string → load directly
await this.loadParameter(paramPathsEnv, 'matcherConfig');
return;
}
const paths = paramPathsEnv
.split(':')
.map((p) => p.trim())
.filter(Boolean);
// Batch fetch all matcher config paths in a single SSM API call
try {
const params = await getParameters(paths);
let combinedString = '';
for (const path of paths) {
const value = params.get(path);
if (value) {
combinedString += value;
} else {
this.configLoadingErrors.push(
`Failed to load parameter for matcherConfig from path ${path}: Parameter not found`,
);
}
}
if (combinedString) {
this.matcherConfig = JSON.parse(combinedString);
}
} catch (error) {
this.configLoadingErrors.push(`Failed to load/parse combined matcher config: ${(error as Error).message}`);
}
}
}
export class ConfigWebhook extends MatcherAwareConfig {
repositoryAllowList: string[] = [];
webhookSecret: string = '';
workflowJobEventSecondaryQueue: string = '';
async loadConfig(): Promise<void> {
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
await Promise.all([
this.loadMatcherConfig(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH),
this.loadParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET, 'webhookSecret'),
]);
validateWebhookSecret(this);
validateRunnerMatcherConfig(this);
}
}
export class ConfigWebhookEventBridge extends BaseConfig {
eventBusName: string | undefined;
allowedEvents: string[] = [];
webhookSecret: string = '';
async loadConfig(): Promise<void> {
this.loadEnvVar(process.env.ACCEPT_EVENTS, 'allowedEvents', []);
this.loadEnvVar(process.env.EVENT_BUS_NAME, 'eventBusName');
await this.loadParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET, 'webhookSecret');
validateEventBusName(this);
validateWebhookSecret(this);
}
}
export class ConfigDispatcher extends MatcherAwareConfig {
repositoryAllowList: string[] = [];
workflowJobEventSecondaryQueue: string = ''; // Deprecated
async loadConfig(): Promise<void> {
this.loadEnvVar(process.env.REPOSITORY_ALLOW_LIST, 'repositoryAllowList', []);
await this.loadMatcherConfig(process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH);
validateRunnerMatcherConfig(this);
}
}
function validateEventBusName(config: ConfigWebhookEventBridge): void {
if (!config.eventBusName) {
config.configLoadingErrors.push('Environment variable for eventBusName is not set and no default value provided.');
}
}
function validateWebhookSecret(config: ConfigWebhookEventBridge | ConfigWebhook): void {
if (!config.webhookSecret) {
config.configLoadingErrors.push('Environment variable for webhookSecret is not set and no default value provided.');
}
}
function validateRunnerMatcherConfig(config: ConfigDispatcher | ConfigWebhook): void {
if (config.matcherConfig.length === 0) {
config.configLoadingErrors.push('Matcher config is empty');
}
}