blob: 38687daf54f0d69d87da665d718f3130e6c76ce2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 angular from 'angular';
import {Entity, EntityFamily} from "../util/model/entity.model";
import {Issue, ISSUE_LEVEL} from '../util/model/issue.model';
import {Dsl} from "../util/model/dsl.model";
import jsYaml from "js-yaml";
import typeNotFoundIcon from "../../img/icon-not-found.svg";
const MODULE_NAME = 'brooklyn.composer.service.blueprint-service';
const TAG = 'SERVICE :: BLUEPRINT :: ';
angular.module(MODULE_NAME, [])
.provider('blueprintService', blueprintServiceProvider);
export default MODULE_NAME;
export const RESERVED_KEYS = ['name', 'location', 'locations', 'type', 'services', 'brooklyn.config', 'brooklyn.children', 'brooklyn.enrichers', 'brooklyn.policies'];
export const DSL_ENTITY_SPEC = '$brooklyn:entitySpec';
export function blueprintServiceProvider() {
return {
$get: ['$log', '$q', '$sce', 'paletteApi', 'iconGenerator', 'dslService',
function ($log, $q, $sce, paletteApi, iconGenerator, dslService) {
return new BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService);
}]
}
}
function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService) {
let blueprint = new Entity();
return {
setFromJson: setBlueprintFromJson,
setFromYaml: setBlueprintFromYaml,
get: getBlueprint,
getAsJson: getBlueprintAsJson,
getAsYaml: getBlueprintAsYaml,
reset: resetBlueprint,
find: findEntity,
findAny: findAnyEntity,
getEntityMetadata: getEntityMetadata,
entityHasMetadata: entityHasMetadata,
refreshBlueprintMetadata: refreshBlueprintMetadata,
refreshEntityMetadata: refreshEntityMetadata,
refreshConfigConstraints: refreshConfigConstraints,
refreshRelationships: refreshRelationships,
refreshAllRelationships: refreshAllRelationships,
isReservedKey: isReservedKey,
getIssues: getIssues,
hasIssues: hasIssues,
populateEntityFromApi: populateEntityFromApiSuccess,
populateLocationFromApi: populateLocationFromApiSuccess,
addConfigKeyDefinition: addConfigKeyDefinition,
addParameterDefinition: addParameterDefinition,
getRelationships: getRelationships,
};
function setBlueprintFromJson(jsonBlueprint) {
if (jsonBlueprint) {
blueprint.setEntityFromJson(jsonBlueprint);
$log.debug(TAG + 'Blueprint set from JS', blueprint);
} else {
resetBlueprint();
}
}
function setBlueprintFromYaml(yamlBlueprint, reset = false) {
let newBlueprint = jsYaml.safeLoad(yamlBlueprint);
if (reset) {
resetBlueprint();
}
setBlueprintFromJson(newBlueprint);
$log.debug(TAG + 'Blueprint set from YAML', blueprint);
// TODO refresh the blueprint now? see comments in yaml.state.js and on refreshBlueprint
}
function getBlueprint() {
return blueprint;
}
function getBlueprintAsJson() {
return blueprint.getData();
}
function getBlueprintAsYaml() {
try {
let currentModel = blueprint.getData();
return (Object.keys(currentModel).length === 0) ? '' : jsYaml.safeDump(currentModel, { lineWidth: 127 });
} catch (err) {
$log.error("Error converting brooklyn blueprint (rethrowing)", err);
throw 'Could not convert the Brooklyn blueprint into YAML format';
}
}
function resetBlueprint() {
blueprint.reset();
}
function findEntity(id) {
if (!id) {
return null;
}
return lookup(blueprint, id);
}
function findAnyEntity(id) {
if (!id) {
return null;
}
return lookup(blueprint, id, true);
}
function getEntityMetadata(entity) {
let metadata = new Map();
if (entity instanceof Entity) {
entity.metadata.forEach((value, key)=> {
if (RESERVED_KEYS.indexOf(key) === -1) {
metadata.set(key, value);
}
});
}
return metadata;
}
function entityHasMetadata(entity) {
return this.getEntityMetadata(entity).size > 0;
}
function isReservedKey(key) {
return RESERVED_KEYS.indexOf(key) > -1;
}
function getIssues(entity = blueprint) {
let issues = [];
if (entity.hasIssues()) {
issues = issues.concat(entity.issues.map((issue)=> {
let newIssue = Issue.builder().message(issue.message).group(issue.group).ref(issue.ref).level(issue.level).build();
newIssue.entity = entity;
return newIssue;
}));
}
entity.policies.forEach((policy)=> {
issues = issues.concat(getIssues(policy));
});
entity.enrichers.forEach((enricher)=> {
issues = issues.concat(getIssues(enricher));
});
entity.children.forEach((child)=> {
issues = issues.concat(getIssues(child));
});
return issues;
}
function hasIssues() {
return this.getIssues().length > 0;
}
function lookup(entity, id, any = false) {
if (entity._id === id) {
return entity;
}
if (entity.childrenAsMap.has(id)) {
return entity.childrenAsMap.get(id);
}
if (entity.policies.has(id)) {
return entity.policies.get(id);
}
if (entity.enrichers.has(id)) {
return entity.enrichers.get(id);
}
let memberSpecs = Object.values(entity.getClusterMemberspecEntities()).filter((memberSpec)=>(memberSpec._id === id));
if (memberSpecs.length === 1) {
return memberSpecs[0];
}
for (let child of entity.childrenAsMap.values()) {
let ret = lookup(child, id, any);
if (ret !== null) {
return ret;
}
}
return null;
}
function refreshBlueprintMetadata(entity = blueprint, family = 'ENTITY') {
// TODO ideally we'd have a cache for all types
return refreshEntityMetadata(entity, family).then(()=> {
return $q.all(entity.children.reduce((result, child) => {
result.push(refreshBlueprintMetadata(child));
return result;
}, []));
}).then(() => {
return entity;
});
}
function refreshEntityMetadata(entity, family) {
entity.miscData.set('loading', true);
return $q.all([refreshTypeMetadata(entity, family), refreshLocationMetadata(entity)]).then(()=> {
entity.miscData.set('loading', false);
return refreshRelationships(entity);
}).then(() => {
return $q.all([
refreshConfigConstraints(entity),
refreshConfigMemberspecsMetadata(entity),
refreshPoliciesMetadata(entity),
refreshEnrichersMetadata(entity),
]);
}).then(()=> {
return entity;
});
}
function refreshTypeMetadata(entity, family) {
let deferred = $q.defer();
if (entity.hasType()) {
entity.family = family.id;
let promise = entity.miscData.has('bundle')
? paletteApi.getBundleType(entity.miscData.get('bundle').symbolicName, entity.miscData.get('bundle').version, entity.type, entity.version, entity.config)
: paletteApi.getType(entity.type, entity.version, entity.config);
promise.then((data)=> {
deferred.resolve(populateEntityFromApiSuccess(entity, data));
}).catch(function (error) {
deferred.resolve(populateEntityFromApiError(entity, error));
});
} else if (entity.parent) {
entity.clearIssues({group: 'type'}).addIssue(Issue.builder().group('type').message('Entity needs a type').level(ISSUE_LEVEL.WARN).build());
entity.miscData.set('sensors', []);
entity.miscData.set('traits', []);
deferred.resolve(entity);
addUnlistedConfigKeysDefinitions(entity);
addUnlistedParameterDefinitions(entity);
} else {
entity.miscData.set('sensors', []);
entity.miscData.set('traits', []);
deferred.resolve(entity);
addUnlistedConfigKeysDefinitions(entity);
addUnlistedParameterDefinitions(entity);
}
return deferred.promise;
}
function refreshLocationMetadata(entity) {
let deferred = $q.defer();
if (entity.hasLocation()) {
paletteApi.getLocation(entity.location).then((location)=> {
deferred.resolve(populateLocationFromApiSuccess(entity, location.catalog || location));
}).catch(function () {
deferred.resolve(populateLocationFromApiError(entity));
});
} else {
deferred.resolve(entity);
}
return deferred.promise;
}
function refreshConfigConstraints(entity) {
return $q((resolve) => {
if (entity.miscData.has('config')) {
entity.miscData.get('config')
.filter(config => config.constraints && config.constraints.length > 0)
.forEach(config => {
for (let constraintO of config.constraints) {
let message = null;
let key = null, args = null;
if (constraintO instanceof String || typeof constraintO=='string') {
key = constraintO;
} else if (Object.keys(constraintO).length==1) {
key = Object.keys(constraintO)[0];
args = constraintO[key];
} else {
$log.warn("Unknown constraint object", typeof constraintO, constraintO, config);
key = constraintO;
}
let val = (k) => entity.config.get(k || config.name);
let isSet = (k) => entity.config.has(k || config.name) && angular.isDefined(val(k));
let isAnySet = (k) => {
if (!k || !Array.isArray(k)) return false;
return k.some(isSet);
}
let hasDefault = () => angular.isDefined(config.defaultValue);
switch (key) {
case 'Predicates.notNull()':
case 'Predicates.notNull':
if (!isSet() && !hasDefault()) {
message = `<samp>${config.name}</samp> is required`;
}
break;
case 'required':
if (!isSet() && !hasDefault() && val()!='') {
message = `<samp>${config.name}</samp> is required`;
}
break;
case 'regex':
if (isSet() && !(new RegExp(args).test(val))) {
message = `<samp>${config.name}</samp> does not match the required format: <samp>${args}</samp>`;
}
break;
case 'forbiddenIf':
if (isSet() && isSet(args)) {
message = `<samp>${config.name}</samp> and <samp>${args}</samp> cannot both be set`;
}
break;
case 'forbiddenUnless':
if (isSet() && !isSet(args)) {
message = `<samp>${config.name}</samp> can only be set when <samp>${args}</samp> is set`;
}
break;
case 'requiredIf':
if (!isSet() && isSet(args)) {
message = `<samp>${config.name}</samp> must be set if <samp>${args}</samp> is set`;
}
break;
case 'requiredUnless':
if (!isSet() && !isSet(args)) {
message = `<samp>${config.name}</samp> or <samp>${args}</samp> is required`;
}
break;
case 'requiredUnlessAnyOf':
if (!isSet() && !isAnySet(args)) {
message = `<samp>${config.name}</samp> or one of <samp>${args}</samp> is required`;
}
break;
case 'forbiddenUnlessAnyOf':
if (isSet() && !isAnySet(args)) {
message = `<samp>${config.name}</samp> cannot be set if any of <samp>${args}</samp> are set`;
}
break;
default:
$log.warn("Unknown constraint predicate", constraintO, config);
}
if (message !== null) {
entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build());
}
}
});
}
resolve();
});
}
function refreshConfigMemberspecsMetadata(entity) {
let promiseArray = [];
Object.values(entity.getClusterMemberspecEntities()).forEach((memberSpec)=> {
// memberSpec can be `undefined` if the member spec is not a `$brooklyn:entitySpec`, e.g. it is `$brooklyn:config("spec")`.
// there may be a better way but this seems to handle it.
if (memberSpec) promiseArray.push(refreshBlueprintMetadata(memberSpec, 'SPEC'));
});
return $q.all(promiseArray);
}
function refreshPoliciesMetadata(entity) {
return $q.all(entity.getPoliciesAsArray().reduce((result, policy)=> {
policy.miscData.set('loading', true);
policy.family = 'POLICY';
let deferred = $q.defer();
paletteApi.getType(policy.type, policy.version).then((data)=> {
deferred.resolve(populateEntityFromApiSuccess(policy, data));
}).catch(function (error) {
deferred.resolve(populateEntityFromApiError(policy, error));
}).finally(()=> {
policy.miscData.set('loading', false);
});
result.push(deferred);
return result;
}, []));
}
function refreshEnrichersMetadata(entity) {
return $q.all(entity.getEnrichersAsArray().reduce((result, enricher)=> {
enricher.miscData.set('loading', true);
enricher.family = 'ENRICHER';
let deferred = $q.defer();
paletteApi.getType(enricher.type, enricher.version).then((data)=> {
deferred.resolve(populateEntityFromApiSuccess(enricher, data));
}).catch(function (error) {
deferred.resolve(populateEntityFromApiError(enricher, error));
}).finally(()=> {
enricher.miscData.set('loading', false);
});
result.push(deferred);
return result;
}, []));
}
function parseInput(input, entity) {
return $q((resolve, reject) => {
try {
let parsed = dslService.parse(input, entity, getBlueprint());
if (parsed.kind && parsed.kind.family === 'constant') {
reject('constants not interpreted as DSL when parsed', input);
} else {
resolve(parsed);
}
} catch (ex) {
$log.debug(ex);
reject(ex, input);
}
});
}
function refreshRelationships(entity) {
return $q.all(Array.from(entity.config.keys()).reduce((promises, key) => {
let value = entity.config.get(key);
// Return promises that returns an object like { key: "my-config-key-key", issues: [] }
if (value instanceof Dsl) {
promises.push(
parseInput(value.toString(), entity).then(dsl => {
entity.config.set(key, dsl);
return {
key: key,
issues: dsl.getAllIssues()
};
}).catch(() => $q.resolve({
key: key,
issues: []
}))
);
} else if (value instanceof Array) {
promises.push(
$q.all(value.reduce((issues, itemValue, itemIndex) => {
return issues.concat(
parseInput(itemValue, entity).then(dsl => {
value[itemIndex] = dsl;
return dsl.getAllIssues();
}).catch(() => [])
);
}, [])).then(issues => {
return {
key: key,
issues: issues.reduce((acc, issue) => acc.concat(issue), [])
}
})
);
} else if (value instanceof Object) {
promises.push(
$q.all(Object.keys(value).reduce((issues, itemKey) => {
return issues.concat(
parseInput(value[itemKey], entity).then(dsl => {
value[itemKey] = dsl;
return dsl.getAllIssues();
}).catch(() => [])
);
}, [])).then(issues => {
return {
key: key,
issues: issues.reduce((acc, issue) => acc.concat(issue), [])
}
})
);
} else {
promises.push(
parseInput(value, entity).then(dsl => {
entity.config.set(key, dsl);
return {
key: key,
issues: dsl.getAllIssues()
};
}).catch(() => $q.resolve({
key: key,
issues: []
}))
);
}
return promises;
}, [])).then(results => {
results.forEach(result => {
entity.clearIssues({ref: result.key});
result.issues.forEach(issue => {
entity.addIssue(Issue.builder().group('config').ref(result.key).message($sce.trustAsHtml(issue)).build());
});
})
});
}
function refreshAllRelationships(entity = blueprint) {
let promises = [];
promises.push(refreshRelationships(entity));
promises.concat(entity.children.map(child => refreshAllRelationships(child)));
return $q.all(promises);
}
function addConfigKeyDefinition(config, key) {
config.push({
"name": key,
"label": key,
"description": "",
"priority": 1,
"pinned": true,
"type": "java.lang.String",
"constraints": [],
});
}
function addParameterDefinition(params, key) {
params.push({
"name": key,
"type": "string",
});
}
function addUnlistedConfigKeysDefinitions(entity) {
// copy config key definitions set on this entity into the miscData aggregated view
let allConfig = entity.miscData.get('config') || [];
entity.config.forEach((value, key) => {
if (!allConfig.some((e) => e.name === key)) {
addConfigKeyDefinition(allConfig, key);
}
});
entity.miscData.set('config', allConfig);
}
function addUnlistedParameterDefinitions(entity) {
// copy parameter definitions set on this entity into the miscData aggregated view;
// TODO see discussions in PR 112 about whether this is necessary and/or there is a better way
let allParams = entity.miscData.get('parameters') || [];
entity.parameters.forEach((param) => {
if (!allParams.some((e) => e.name === param.name)) {
allParams.push(param);
}
});
entity.miscData.set('parameters', allParams);
}
function populateEntityFromApiSuccess(entity, data) {
entity.clearIssues({group: 'type'});
entity.type = data.symbolicName;
entity.icon = data.iconUrl || iconGenerator(data.symbolicName);
entity.miscData.set('important', !!data.iconUrl);
entity.miscData.set('bundle', {
symbolicName: data.containingBundle.split(':')[0],
version: data.containingBundle.split(':')[1]
});
entity.miscData.set('typeName', data.displayName || data.symbolicName);
entity.miscData.set('config', data.config || []);
entity.miscData.set('parameters', data.parameters || []);
entity.miscData.set('sensors', data.sensors || []);
entity.miscData.set('traits', data.supertypes || []);
entity.miscData.set('tags', data.tags || []);
var uiHints = {};
data.tags.forEach( (t) => {
mergeAppendingLists(uiHints, t['ui-composer-hints']);
});
entity.miscData.set('ui-composer-hints', uiHints);
entity.miscData.set('virtual', data.virtual || null);
addUnlistedConfigKeysDefinitions(entity);
addUnlistedParameterDefinitions(entity);
return entity;
}
function mergeAppendingLists(dst, src) {
for (let p in src) {
if (Array.isArray(dst[p]) || Array.isArray(src[p])) {
dst[p] = [].concat(dst[p] || [], src[p]);
} else {
dst[p] = Object.assign({}, dst[p], src[p]);
}
}
return dst;
}
function populateEntityFromApiError(entity, error) {
$log.warn("Error loading/populating type, data will be incomplete.", entity, error);
entity.clearIssues({group: 'type'});
entity.addIssue(Issue.builder().group('type').message($sce.trustAsHtml(`Type <samp>${entity.type + (entity.hasVersion ? ':' + entity.version : '')}</samp> does not exist`)).build());
entity.miscData.set('typeName', entity.type || '');
entity.miscData.set('config', []);
entity.miscData.set('parameters', []);
entity.miscData.set('sensors', []);
entity.miscData.set('traits', []);
entity.miscData.set('virtual', null);
entity.icon = typeNotFoundIcon;
addUnlistedConfigKeysDefinitions(entity);
addUnlistedParameterDefinitions(entity);
return entity;
}
function populateLocationFromApiSuccess(entity, data) {
entity.clearIssues({group: 'location'});
entity.location = data.symbolicName;
entity.miscData.set('locationName', data.name);
entity.miscData.set('locationIcon', data.iconUrl || iconGenerator(data.symbolicName));
return entity;
}
function populateLocationFromApiError(entity) {
entity.clearIssues({group: 'location'});
entity.addIssue(Issue.builder().level(ISSUE_LEVEL.WARN).group('location').message($sce.trustAsHtml(`Location <samp>${!(entity.location instanceof String) ? JSON.stringify(entity.location) : entity.location}</samp> does not exist in your local catalog. Deployment might fail.`)).build());
entity.miscData.set('locationName', entity.location);
entity.miscData.set('locationIcon', typeNotFoundIcon);
return entity;
}
/**
* Retrieves all the Entities referenced by an Entity
* @param {Entity} entity the Entity to resolve relative references from
* @return {Array} of objects that contains source and target entities
*/
function getRelationships(entity = blueprint) {
let set = Array.from(entity.config.values())
.reduce((set, config)=> {
if (config instanceof Dsl) {
config.relationships.forEach((entity) => {
if (entity !== null) {
set.add(entity);
}
});
}
if (config instanceof Array) {
config
.filter(conf => conf instanceof Dsl)
.reduce((set, config)=> {
config.relationships.forEach((entity)=> {
if (entity !== null) {
set.add(entity);
}
});
return set;
}, set);
}
if (config instanceof Object) {
Object.keys(config)
.filter(key => config[key] instanceof Dsl)
.reduce((set, key)=> {
config[key].relationships.forEach((entity)=> {
if (entity !== null) {
set.add(entity);
}
});
return set;
}, set);
}
return set;
}, new Set());
let relationships = Array.from(set).map((relation) => {
return {
source: entity,
target: relation
};
});
relationships = Array.from(entity.config.values())
.filter(config => config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity)
.map(config => config[DSL_ENTITY_SPEC])
.reduce((relationships, spec) => {
return relationships.concat(getRelationships(spec));
}, relationships);
return entity.children.reduce((relationships, child) => {
return relationships.concat(getRelationships(child))
}, relationships);
}
}