-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
Copy pathagent.ts
174 lines (150 loc) · 6.1 KB
/
agent.ts
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
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Handlebars from 'handlebars';
import { safeLoad, safeDump } from 'js-yaml';
import type { Logger } from '@kbn/core/server';
import type { PackagePolicyConfigRecord } from '../../../../common/types';
import { toCompiledSecretRef } from '../../secrets';
import { PackageInvalidArchiveError } from '../../../errors';
import { appContextService } from '../..';
const handlebars = Handlebars.create();
export function compileTemplate(variables: PackagePolicyConfigRecord, templateStr: string) {
const logger = appContextService.getLogger();
const { vars, yamlValues } = buildTemplateVariables(logger, variables);
let compiledTemplate: string;
try {
const template = handlebars.compile(templateStr, { noEscape: true });
compiledTemplate = template(vars);
} catch (err) {
throw new PackageInvalidArchiveError(`Error while compiling agent template: ${err.message}`);
}
compiledTemplate = replaceRootLevelYamlVariables(yamlValues, compiledTemplate);
const yamlFromCompiledTemplate = safeLoad(compiledTemplate, {});
// Hack to keep empty string ('') values around in the end yaml because
// `safeLoad` replaces empty strings with null
const patchedYamlFromCompiledTemplate = Object.entries(yamlFromCompiledTemplate).reduce(
(acc, [key, value]) => {
if (value === null && typeof vars[key] === 'string' && vars[key].trim() === '') {
acc[key] = '';
} else {
acc[key] = value;
}
return acc;
},
{} as { [k: string]: any }
);
return replaceVariablesInYaml(yamlValues, patchedYamlFromCompiledTemplate);
}
function isValidKey(key: string) {
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
}
function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) {
if (Object.keys(yamlVariables).length === 0 || !yaml) {
return yaml;
}
Object.entries(yaml).forEach(([key, value]: [string, any]) => {
if (typeof value === 'object') {
yaml[key] = replaceVariablesInYaml(yamlVariables, value);
}
if (typeof value === 'string' && value in yamlVariables) {
yaml[key] = yamlVariables[value];
}
});
return yaml;
}
function buildTemplateVariables(logger: Logger, variables: PackagePolicyConfigRecord) {
const yamlValues: { [k: string]: any } = {};
const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => {
// support variables with . like key.patterns
const keyParts = key.split('.');
const lastKeyPart = keyParts.pop();
logger.debug(`Building agent template variables`);
if (!lastKeyPart || !isValidKey(lastKeyPart)) {
throw new PackageInvalidArchiveError(
`Error while compiling agent template: Invalid key ${lastKeyPart}`
);
}
let varPart = acc;
for (const keyPart of keyParts) {
if (!isValidKey(keyPart)) {
throw new PackageInvalidArchiveError(
`Error while compiling agent template: Invalid key ${keyPart}`
);
}
if (!varPart[keyPart]) {
varPart[keyPart] = {};
}
varPart = varPart[keyPart];
}
if (recordEntry.type && recordEntry.type === 'yaml') {
const yamlKeyPlaceholder = `##${key}##`;
varPart[lastKeyPart] = recordEntry.value ? `"${yamlKeyPlaceholder}"` : null;
yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null;
} else if (recordEntry.value && recordEntry.value.isSecretRef) {
varPart[lastKeyPart] = toCompiledSecretRef(recordEntry.value.id);
} else {
varPart[lastKeyPart] = recordEntry.value;
}
return acc;
}, {} as { [k: string]: any });
return { vars, yamlValues };
}
function containsHelper(this: any, item: string, check: string | string[], options: any) {
if ((Array.isArray(check) || typeof check === 'string') && check.includes(item)) {
if (options && options.fn) {
return options.fn(this);
}
return true;
}
return '';
}
handlebars.registerHelper('contains', containsHelper);
// escapeStringHelper will wrap the provided string with single quotes.
// Single quoted strings in yaml need to escape single quotes by doubling them
// and to respect any incoming newline we also need to double them, otherwise
// they will be replaced with a space.
function escapeStringHelper(str: string) {
if (!str) return undefined;
return "'" + str.replace(/\'/g, "''").replace(/\n/g, '\n\n') + "'";
}
handlebars.registerHelper('escape_string', escapeStringHelper);
// toJsonHelper will convert any object to a Json string.
function toJsonHelper(value: any) {
if (typeof value === 'string') {
// if we get a string we assume is an already serialized json
return value;
}
return JSON.stringify(value);
}
handlebars.registerHelper('to_json', toJsonHelper);
// urlEncodeHelper returns a string encoded as a URI component.
function urlEncodeHelper(input: string) {
let encodedString = encodeURIComponent(input);
// encodeURIComponent does not encode the characters -.!~*'(), known as "unreserved marks",
// which do not have a reserved purpose but are allowed in a URI "as is". So, these have are
// explicitly encoded. The following creates the sequences %27 %28 %29 %2A. Since the valid
// encoding of "*" is %2A, it is necessary to call toUpperCase() to properly encode.
encodedString = encodedString.replace(
/[!'()*]/g,
(char) => '%' + char.charCodeAt(0).toString(16).toUpperCase()
);
return encodedString;
}
handlebars.registerHelper('url_encode', urlEncodeHelper);
function replaceRootLevelYamlVariables(yamlVariables: { [k: string]: any }, yamlTemplate: string) {
if (Object.keys(yamlVariables).length === 0 || !yamlTemplate) {
return yamlTemplate;
}
let patchedTemplate = yamlTemplate;
Object.entries(yamlVariables).forEach(([key, val]) => {
patchedTemplate = patchedTemplate.replace(
new RegExp(`^"${key}"`, 'gm'),
val ? safeDump(val) : ''
);
});
return patchedTemplate;
}