Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default class ComponentRegister extends BaseCommand {
throw new Error('Component Config must have a name');
}

validateInterpolation(component_spec);
validateInterpolation(component_spec, true);

const { component_name } = ComponentSlugUtils.parse(component_spec.name);

Expand All @@ -197,7 +197,8 @@ export default class ComponentRegister extends BaseCommand {
const dependency_manager = new LocalDependencyManager(this.app.api, selected_account.name);
dependency_manager.environment = 'production';

const graph = await dependency_manager.getGraph([instanceToInstance(component_spec)], undefined, { interpolate: false, validate: false });
const graph = await dependency_manager.getGraph([instanceToInstance(component_spec)], undefined, { interpolate: true, validate: true, relax_validation: true });

// Tmp fix to register host overrides
for (const node of graph.nodes.filter(n => n instanceof ServiceNode) as ServiceNode[]) {
for (const interface_config of Object.values(node.interfaces)) {
Expand Down
1 change: 1 addition & 0 deletions src/dependency-manager/graph/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface GraphOptions {
interpolate?: boolean;
validate?: boolean;
relax_validation?: boolean,
}
81 changes: 73 additions & 8 deletions src/dependency-manager/manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { instanceToPlain, plainToInstance, serialize } from 'class-transformer';
import { buildNodeRef, ComponentConfig } from './config/component-config';
import { ArchitectContext, ComponentContext, DatabaseContext } from './config/component-context';
import { ArchitectContext, ComponentContext, DatabaseContext, ServiceContext } from './config/component-context';
import { DeprecatedInterfacesSpec } from './deprecated-spec/interfaces';
import { DependencyGraph, DependencyGraphMutable } from './graph';
import { IngressEdge } from './graph/edge/ingress';
Expand All @@ -21,6 +21,8 @@ import { validateOrRejectSpec } from './spec/utils/spec-validator';
import { Dictionary, transformDictionary } from './utils/dictionary';
import { ArchitectError } from './utils/errors';
import { interpolateObjectLoose, interpolateObjectOrReject, replaceInterpolationBrackets } from './utils/interpolation';
import { IngressConfig, ServiceConfig, ServiceInterfaceConfig } from './config/service-config';
import { SecretDefinitionSpec, SecretSpecValue } from './spec/secret-spec';

export default abstract class DependencyManager {
account?: string;
Expand Down Expand Up @@ -395,6 +397,7 @@ export default abstract class DependencyManager {
options = {
interpolate: true,
validate: true,
relax_validation: false,
...options,
};

Expand All @@ -412,6 +415,14 @@ export default abstract class DependencyManager {

const evaluated_component_specs: ComponentSpec[] = [];
for (const raw_component_spec of component_specs) {
if (options.relax_validation && raw_component_spec.secrets) {
// Assign dummy value to unset secrets
for (const [key, secret] of Object.entries(raw_component_spec.secrets as Dictionary<SecretSpecValue | SecretDefinitionSpec>)) {
if (!secret) {
raw_component_spec.secrets[key] = { default: '-999' };
}
}
}
const { component_spec, context } = await this.getComponentSpecContext(graph, raw_component_spec, secrets, options);

if (options.interpolate) {
Expand Down Expand Up @@ -486,12 +497,67 @@ export default abstract class DependencyManager {
context.dependencies = {};
for (const dep_name of Object.keys(component_spec.dependencies || {})) {
const dependency_context = context_map[dep_name];
if (!dependency_context) continue;
if (!dependency_context && !options.relax_validation) {
continue;
}

if (options.relax_validation) {
// Mock dependency node for validation
const mock_service_ingress_config: IngressConfig = {
enabled: false,
subdomain: '',
path: '',
ip_whitelist: [],
sticky: '',
private: false,
consumers: [],
dns_zone: '',
host: '',
port: '',
protocol: '',
username: '',
password: '',
url: '',
};
const mock_service_interface_config: ServiceInterfaceConfig = {
host: '',
port: '',
protocol: '',
username: '',
password: '',
url: '',
sticky: '',
path: '',
ingress: mock_service_ingress_config,
};
const mock_dependency_node = {
__type: '',
config: {} as ServiceConfig,
ref: `${dep_name}--ø`,
component_ref: dep_name,
service_name: 'ø',
interfaces: { 'ø': mock_service_interface_config },
ingresses: { 'ø': mock_service_ingress_config },
ports: [],
is_external: false,
instance_id: '',
};
graph.addNode(mock_dependency_node);

context.dependencies[dep_name] = {
services: dependency_context.services || {},
outputs: dependency_context.outputs || {},
};
const service_context: ServiceContext = {
environment: {},
interfaces: { 'ø': mock_service_interface_config },
};
context.dependencies[dep_name] = {
services: { 'ø': service_context },
outputs: {},
};
} else {
context.dependencies[dep_name] = {
services: dependency_context.services || {},
outputs: dependency_context.outputs || {},
};
}
}
}

Expand All @@ -506,7 +572,7 @@ export default abstract class DependencyManager {
const context = context_map[component_spec.metadata.ref];

if (options.interpolate) {
component_spec = interpolateObject(component_spec, context, { keys: false, values: true, file: component_spec.metadata.file });
component_spec = interpolateObject(component_spec, context, { keys: false, values: true, file: component_spec.metadata.file, relax_validation: options.relax_validation });
component_spec.metadata.interpolated = true;
}

Expand All @@ -529,7 +595,6 @@ export default abstract class DependencyManager {
this.validateGraph(graph);
graph.validated = true;
}

return Object.freeze(graph);
}
}
4 changes: 4 additions & 0 deletions src/dependency-manager/spec/utils/interpolation.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export const EXPRESSION_REGEX = new RegExp(`\\\${{\\s*(.*?)\\s*}}`, 'g');
export const IF_EXPRESSION_REGEX = new RegExp(`^\\\${{\\s*if(.*?)\\s*}}$`);
export const DEPENDENCY_EXPRESSION_REGEX = new RegExp('^\\${{\\s*dependencies\\.[^}]*}}$');
export const DEPRECATED_DEPENDENCY_EXPRESSION_REGEX = new RegExp('\\$\\{\\{ dependencies\\.[^.]*\\.(ingresses|interfaces)\\.[^.]*\\.url }}');
export const ARCHITECT_EXPRESSION_REGEX = new RegExp('^\\${{\\s*architect\\.[^}]*}}');
export const ENVIRONMENT_EXPRESSION_REGEX = new RegExp('^\\${{\\s*environment\\.[^}]*}}');
22 changes: 20 additions & 2 deletions src/dependency-manager/spec/utils/spec-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { ComponentInstanceMetadata, ComponentSpec } from '../component-spec';
import { ServiceInterfaceSpec } from '../service-spec';
import { findDefinition, getArchitectJSONSchema } from './json-schema';
import { Slugs } from './slugs';
import { DependencyGraphMutable } from '../../graph';
import { ServiceNode } from '../../graph/node/service';

export type AjvError = ErrorObject[] | null | undefined;

Expand Down Expand Up @@ -354,15 +356,31 @@ export const validateOrRejectSpec = (parsed_yml: ParsedYaml, metadata?: Componen
return component_spec;
};

export const validateInterpolation = (component_spec: ComponentSpec): void => {
const checkInterpolationPath = (queue: any, child: any): boolean => {
let key = queue.shift();
const keys = Object.keys(child);
const star = keys.includes('*');
if (star || child[key]) {
key = star ? '*' : key;
if (queue.length > 0 && typeof child[key] === 'object') {
return checkInterpolationPath(queue, child[key]);
} else {
return true;
}
} else {
return keys.includes(key);
}
};

export const validateInterpolation = (component_spec: ComponentSpec, relax_validation?: boolean): void => {
const { errors } = interpolateObject(component_spec, {}, {
keys: true,
values: true,
file: component_spec.metadata.file,
relax_validation: relax_validation,
});

const filtered_errors = errors.filter(error => !error.message.startsWith(RequiredInterpolationRule.PREFIX));

if (filtered_errors.length > 0) {
throw new ValidationErrors(filtered_errors, component_spec.metadata.file);
}
Expand Down
67 changes: 63 additions & 4 deletions src/dependency-manager/utils/interpolation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { instanceToInstance } from 'class-transformer';
import deepmerge from 'deepmerge';
import { EXPRESSION_REGEX, IF_EXPRESSION_REGEX } from '../spec/utils/interpolation';
import { ARCHITECT_EXPRESSION_REGEX, DEPENDENCY_EXPRESSION_REGEX, DEPRECATED_DEPENDENCY_EXPRESSION_REGEX, ENVIRONMENT_EXPRESSION_REGEX, EXPRESSION_REGEX, IF_EXPRESSION_REGEX } from '../spec/utils/interpolation';
import { Dictionary } from './dictionary';
import { ValidationError, ValidationErrors } from './errors';
import { ArchitectParser } from './parser';
import { matches } from './regex';
import { CONTEXT_KEY_DELIMITER } from './rules';
import { ComponentSpec } from '../spec/component-spec';
import { ArchitectContext, DependencyContext, ServiceContext } from '../config/component-context';
import { IngressConfig, ServiceInterfaceConfig } from '../config/service-config';

export const replaceBrackets = (value: string): string => {
return value.replace(/\[/g, '.').replace(/["'\\\]|]/g, '');
Expand All @@ -23,8 +26,52 @@ export const replaceInterpolationBrackets = (value: string): string => {
return res;
};

export const buildContextMap = (context: any): any => {
/*
Create mock dependencies for dependencies.<dependency-name>.services.*.interfaces.*.<ingress-config-prop>
*/
const createMockDependencies = (component_spec: ComponentSpec) => {
const dependencies: Dictionary<DependencyContext> = {};
for (const dep_name of Object.keys(component_spec.dependencies || {})) {
const mock_service_interface_config: ServiceInterfaceConfig = {
host: '',
port: '',
protocol: '',
username: '',
password: '',
url: '',
sticky: '',
path: '',
ingress: { private: false } as IngressConfig,
};
const mock_service_context: ServiceContext = {
interfaces: { '*': mock_service_interface_config },
environment: {},
};

const dependency_context = {
name: dep_name,
dependencies: {},
secrets: {},
outputs: {},
databases: {},
services: { '*': mock_service_context },
tasks: {},
architect: {} as ArchitectContext,
};
dependencies[dep_name] = {
services: dependency_context.services || {},
outputs: dependency_context.outputs || {},
};
}
return dependencies;
};

export const buildContextMap = (context: any, use_mock_dependencies?: boolean): any => {
const context_map: Dictionary<any> = {};
if (use_mock_dependencies) {
context.dependencies = createMockDependencies(context);
}

const queue = [['', context]];
while (queue.length > 0) {
const [prefix, c] = queue.shift()!;
Expand All @@ -47,6 +94,7 @@ export interface InterpolateObjectOptions {
keys?: boolean;
values?: boolean;
file?: { path: string, contents: string };
relax_validation?: boolean;
}

const overwriteMerge = (destinationArray: any[], sourceArray: any[], options: deepmerge.Options) => sourceArray;
Expand All @@ -63,6 +111,7 @@ export const interpolateObject = <T>(obj: T, context: any, _options?: Interpolat
const options = {
keys: false,
values: true,
relax_validation: false,
..._options,
};

Expand Down Expand Up @@ -99,8 +148,18 @@ export const interpolateObject = <T>(obj: T, context: any, _options?: Interpolat
error.invalid_key = true;
}
} else if (options.values && typeof value === 'string') {
const parsed_value = parser.parseString(value, context_map);
el[key] = parsed_value;
let updated_value = value;
const special_case = DEPRECATED_DEPENDENCY_EXPRESSION_REGEX.test(value) || ENVIRONMENT_EXPRESSION_REGEX.test(value) || ARCHITECT_EXPRESSION_REGEX.test(value);
if (options.relax_validation && special_case) {
el[key] = updated_value;
} else if (options.relax_validation && DEPENDENCY_EXPRESSION_REGEX.test(value)) {
updated_value = value.replace(/(services\.)[^.]+(\.interfaces\.)([^.]+)/g, '$1ø$2ø');
const parsed_value = parser.parseString(updated_value, context_map);
el[key] = parsed_value;
} else {
const parsed_value = parser.parseString(value, context_map);
el[key] = parsed_value;
}
} else {
el[key] = value;
if (value instanceof Object) {
Expand Down
Loading