| /* |
| * 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, DSL} 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"; |
| import {isSensitiveFieldName, isSensitiveFieldPlaintextValueBlocked} from 'brooklyn-ui-utils/sensitive-field/sensitive-field'; |
| |
| 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 = DSL.ENTITY_SPEC; |
| |
| export const COMMON_HINTS = { |
| 'config-quick-fixes': [{ |
| key: '.*', |
| fix: 'explicit_config', |
| 'message-regex': /implicitly defined/i |
| }] |
| }; |
| |
| export function blueprintServiceProvider() { |
| return { |
| $get: ['$log', '$q', '$sce', 'paletteApi', 'iconGenerator', 'dslService', 'brBrandInfo', |
| function ($log, $q, $sce, paletteApi, iconGenerator, dslService, brBrandInfo) { |
| return new BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService, brBrandInfo); |
| }] |
| } |
| } |
| |
| function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService, brBrandInfo) { |
| let blueprint = new Entity(); |
| let entityRelationshipProviders = {}; |
| |
| // Add relationships provider based on Entity.config |
| addEntityRelationshipsProvider( 'config',{ |
| apply: (entity) => { |
| let set = Array.from(entity.config.keys()) |
| .reduce((set, key)=> { |
| let config = entity.config.get(key); |
| if (config instanceof Dsl) { |
| config.relationships.forEach((entity) => { |
| if (entity !== null) { |
| set.add({entity: entity, name: key}); |
| } |
| }); |
| } |
| if (config instanceof Array) { |
| config |
| .filter(item => item instanceof Dsl) |
| .reduce((set, config) => { |
| config.relationships.forEach((entity) => { |
| if (entity !== null) { |
| set.add({entity: entity, name: key}); |
| } |
| }); |
| return set; |
| }, set); |
| } |
| if (config instanceof Object) { |
| Object.keys(config) |
| .filter(objectKey => config[objectKey] instanceof Dsl) |
| .reduce((set, objectKey) => { |
| config[objectKey].relationships.forEach((entity) => { |
| if (entity !== null) { |
| // name is the name of a complex relationship, and it consists of a property name, not its members. That is why here, name is set to 'key' not 'objectKey'. |
| set.add({entity: entity, name: key}); |
| } |
| }); |
| return set; |
| }, set); |
| } |
| return set; |
| }, new Set()); |
| |
| return Array.from(set).map((relation) => { |
| return { |
| source: entity, |
| target: relation.entity, |
| label: relation.name |
| }; |
| }); |
| } |
| }); |
| |
| // Add relationships provider based on Entity spec |
| addEntityRelationshipsProvider('spec', { |
| apply: (entity) => { |
| return Array.from(entity.config.values()) |
| .filter(config => config && config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity) |
| .map(config => config[DSL_ENTITY_SPEC]) |
| .reduce((relationships, spec) => relationships.concat(getRelationships(spec)), []); |
| } |
| }); |
| |
| 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, |
| refreshConfigInherited: refreshConfigInherited, |
| addRelationshipsProvider: addEntityRelationshipsProvider, |
| isReservedKey: isReservedKey, |
| getIssues: getIssues, |
| hasIssues: hasIssues, |
| clearAllIssues: clearAllIssues, |
| getAllIssues: getAllIssues, |
| populateEntityFromApi: populateEntityFromApiSuccess, |
| populateLocationFromApi: populateLocationFromApiSuccess, |
| addConfigKeyDefinition: addConfigKeyDefinition, |
| addParameterDefinition: addParameterDefinition, |
| getRelationships: getRelationships, |
| populateId: populateId, |
| }; |
| |
| 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); |
| refreshBlueprintMetadata(); // needed to prevent graph child nodes from disappearing when the "Add to catalog" modal is opened |
| } |
| |
| 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, nonRecursive) { |
| 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)); |
| }); |
| |
| if (!nonRecursive) { |
| entity.children.forEach((child) => { |
| issues = issues.concat(getIssues(child)); |
| }); |
| } |
| |
| return issues; |
| } |
| |
| // typically followed by a call to refresh |
| function clearAllIssues(entity = blueprint) { |
| entity.resetIssues(); |
| entity.children.forEach(clearAllIssues); |
| } |
| |
| function getAllIssues(entity = blueprint) { |
| return collectAllIssues({}, entity); |
| } |
| |
| function collectAllIssues(result, entity) { |
| if (!result.entities) { |
| result.entities = {}; |
| result.byEntity = {}; |
| result.count = 0; |
| result.errors = { byEntity: {}, count: 0 } |
| result.warnings = { byEntity: {}, count: 0 } |
| } |
| if (result.entities[entity._id]) { |
| // already visited, some sort of reference; ignore |
| } else { |
| result.entities[entity._id] = entity; |
| |
| let issues = getIssues(entity, true); |
| if (issues.length) { |
| result.byEntity[entity._id] = issues; |
| result.count += issues.length; |
| |
| let errors = issues.filter(i => i.level.id == 'error'); |
| if (errors.length) { |
| result.errors.byEntity[entity._id] = errors; |
| result.errors.count += errors.length; |
| } |
| |
| let warnings = issues.filter(i => i.level.id != 'error'); |
| if (warnings.length) { |
| result.warnings.byEntity[entity._id] = warnings; |
| result.warnings.count += warnings.length; |
| } |
| } |
| |
| entity.children.forEach((child)=> collectAllIssues(result, child)); |
| } |
| return result; |
| } |
| |
| 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(() => { |
| entity.clearIssues({group: 'config'}); |
| return $q.all([ |
| refreshConfigConstraints(entity), |
| refreshConfigMemberspecsMetadata(entity), |
| refreshPoliciesMetadata(entity), |
| refreshEnrichersMetadata(entity), |
| refreshConfigInherited(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', []); |
| |
| entity.clearIssues({group: 'config'}); |
| entity.miscData.set('config', []); |
| entity.miscData.set('configMap', {}); |
| entity.miscData.set('parameters', []); |
| entity.miscData.set('parametersMap', {}); |
| |
| addUnlistedConfigKeysDefinitions(entity); |
| addUnlistedParameterDefinitions(entity); |
| |
| deferred.resolve(entity); |
| } |
| |
| return deferred.promise; |
| } |
| |
| function locationType(location) { |
| if (!location || typeof location === 'string') return location; |
| if (typeof location === 'object' && Object.keys(location).length==1) return Object.keys(location)[0]; |
| return null; |
| } |
| |
| function refreshLocationMetadata(entity) { |
| let deferred = $q.defer(); |
| |
| if (entity.hasLocation()) { |
| let type = locationType(entity.location); |
| if (type && type.startsWith) { |
| if (type.startsWith("jclouds:")) { |
| // types eg jclouds:aws-ec2 are low-level, not in the catalog |
| deferred.resolve(populateLocationFromApiSuccess(entity, { yamlHere: entity.location })); |
| } else { |
| paletteApi.getLocation(locationType(entity.location)).then((location) => { |
| let loc = Object.assign({}, location.catalog || location, {yamlHere: entity.location}); |
| deferred.resolve(populateLocationFromApiSuccess(entity, loc)); |
| }).catch(function () { |
| deferred.resolve(populateLocationFromApiError(entity)); |
| }); |
| } |
| } else { |
| deferred.resolve(entity); |
| } |
| } else { |
| deferred.resolve(entity); |
| } |
| |
| return deferred.promise; |
| } |
| |
| function refreshConfigConstraints(entity) { |
| function checkSensitiveFields(config) { |
| if (isSensitiveFieldPlaintextValueBlocked() && isSensitiveFieldName(config.name)) { |
| let v = entity.config.get(config.name); |
| if (!v) return; |
| let t = typeof v; |
| if (t === 'object') return; |
| let invalid = false; |
| if (t === 'string') { |
| if (t.length) { |
| if (t.startsWith("$brooklyn:")) { |
| invalid = false; |
| } else { |
| invalid = true; |
| } |
| } |
| } else if (t === 'number') { |
| invalid = true; |
| } |
| if (invalid) { |
| let message = `Plaintext values are not permitted for <samp>${config.name}</samp>. <br/>Use DSL with externalized configuration.`; |
| entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build()); |
| } |
| } |
| } |
| function checkConstraints(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); |
| } |
| const hasDefault = (typeof config.defaultValue) !== 'undefined'; |
| |
| 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': |
| let matchFullLine = '^' + args + '$'; |
| if (isSet() && !(new RegExp(matchFullLine).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()); |
| } |
| } |
| } |
| return $q((resolve) => { |
| if (entity.miscData.has('config')) { |
| entity.miscData.get('config') |
| .forEach(checkSensitiveFields); |
| |
| entity.miscData.get('config') |
| .filter(config => config.constraints && config.constraints.length > 0) |
| .forEach(checkConstraints); |
| } |
| // could do same as above to check parameters, but that doesn't make the parameters appear as config to set, |
| // so instead we merge parameters with config |
| 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")`; |
| // only accept for the former, where refresh will have replaced it with an Entity object |
| if (memberSpec instanceof Entity) { |
| 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 refreshConfigInherited(entity) { |
| return $q((resolve) => { |
| (Array.isArray(entity.miscData.get('config')) ? entity.miscData.get('config') : []) |
| .filter(definition => !entity.config.has(definition.name)) |
| .forEach(definition => { |
| if (entity.hasInheritedConfig(definition.name)) { |
| entity.addIssue(Issue.builder() |
| .group('config') |
| .ref(definition.name) |
| .level(ISSUE_LEVEL.WARN) |
| .message(`Implicitly defined (inherited from an ancestor)`) |
| .build()); |
| } |
| }); |
| resolve(); |
| }); |
| } |
| |
| 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("Cannot detect whether this is a DSL expression; assuming not", input, 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, phase: 'relationship'}); |
| result.issues.forEach(issue => { |
| entity.addIssue(Issue.builder().group('config').phase('relationship').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(entity, key) { |
| entity.addConfigKeyDefinition(key, false); |
| } |
| |
| function addParameterDefinition(entity, key) { |
| entity.addParameterDefinition(key); |
| } |
| |
| function addUnlistedConfigKeysDefinitions(entity) { |
| // copy config key definitions set on this entity into the miscData aggregated view |
| let allConfig = entity.miscDataOrDefault('configMap', {}); |
| entity.config.forEach((value, key) => { |
| entity.addConfigKeyDefinition(key, false, true, value); |
| }); |
| entity.addConfigKeyDefinition(null, false, false); |
| } |
| |
| function addUnlistedParameterDefinitions(entity) { |
| // copy parameter definitions set on this entity into the miscData aggregated view; |
| // see discussions in PR 112 about whether this is necessary and/or there is a better way; but note, this is much updated since |
| entity.parameters.forEach((param) => { |
| entity.addParameterDefinition(param, false, true); |
| }); |
| entity.addParameterDefinition(null, false, false); |
| } |
| |
| function populateEntityFromApiSuccess(entity, data) { |
| function mapped(list, field) { |
| let result = {}; |
| if (list) { |
| list.forEach(l => { |
| if (l && l[field]) { |
| result[l[field]] = l; |
| } |
| }); |
| } |
| return result; |
| } |
| entity.clearIssues({group: 'type'}); |
| |
| entity.type = data.symbolicName; |
| entity.icon = entity.metadata.get('iconUrl') |
| ? (data.iconUrl || '/v1/catalog/types/'+entity.type+'/'+(entity.version || 'latest')+'/icon')+'?iconUrl='+entity.metadata.get('iconUrl') |
| : 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('displayName', data.displayName); |
| entity.miscData.set('symbolicName', data.symbolicName); |
| entity.miscData.set('description', data.description); |
| |
| entity.miscData.set('config', data.config || []); |
| entity.miscData.set('configMap', mapped(data.config, 'name')); |
| entity.miscData.set('parameters', data.parameters || []); |
| entity.miscData.set('parametersMap', mapped(data.parameters, 'name')); |
| |
| entity.miscData.set('sensors', data.sensors || []); |
| entity.miscData.set('traits', data.supertypes || []); |
| entity.miscData.set('tags', data.tags || []); |
| entity.miscData.set('loadedVersion', data.version); |
| |
| data.tags.forEach( (t) => { |
| mergeAppendingLists(COMMON_HINTS, t['ui-composer-hints']); |
| }); |
| entity.miscData.set('ui-composer-hints', COMMON_HINTS); |
| 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 populateLocationFromApiCommon(entity, data) { |
| entity.clearIssues({group: 'location'}); |
| entity.location = data.yamlHere || data.id; |
| |
| let name = data.name || data.displayName; |
| if (!name && data.yamlHere) { |
| name = typeof data.yamlHere === 'object' ? Object.keys(data.yamlHere)[0] : data.yamlHere; |
| } |
| if (!name) name = data.symbolicName; |
| entity.miscData.set('locationName', name); |
| |
| // use icon on item, but if none then generate using *yaml* to distinguish when someone has changed it |
| // (especially for things like jclouds:aws-ec2 -- the config is more interesting than the type name) |
| entity.miscData.set('locationIcon', data==null ? null : data.iconUrl || iconGenerator(data.yamlHere ? JSON.stringify(data.yamlHere) : data.symbolicName)); |
| return entity; |
| } |
| |
| function populateLocationFromApiSuccess(entity, data) { |
| populateLocationFromApiCommon(entity, data); |
| } |
| |
| function populateLocationFromApiError(entity) { |
| populateLocationFromApiCommon(entity, { yamlHere: entity.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('locationIcon', typeNotFoundIcon); |
| return entity; |
| } |
| |
| /** |
| * Adds {Entity} relationships provider to discover relationships between entities that are specific to provider. |
| * |
| * @param {String} providerName The relationships provider name. |
| * @param {Object} entityRelationshipsProvider The {Entity} relationships provider. The provider must implement the |
| * method `apply({Entity})` which takes {Entity} as an argument and returns array of relationships found in the |
| * format [{source: {Entity}, target: {Entity}}], or an empty array []. |
| */ |
| function addEntityRelationshipsProvider(providerName, entityRelationshipsProvider) { |
| if (typeof entityRelationshipsProvider.apply !== 'function' || !providerName) { |
| console.error(`Provider ${entityRelationshipsProvider} with name ${providerName} is not an Entity relationships provider.`); |
| } |
| entityRelationshipProviders[providerName] = entityRelationshipsProvider; |
| } |
| |
| /** |
| * 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 relationships = []; |
| |
| // Aggregate relationships discovered by Entity relationships providers. |
| for (let provider of Object.values(entityRelationshipProviders)) { |
| relationships = relationships.concat(provider.apply(entity)); |
| } |
| |
| // Iterate over children and reduct. |
| return entity.children.reduce((relationships, child) => { |
| return relationships.concat(getRelationships(child)) |
| }, relationships); |
| } |
| |
| function populateId(entity) { |
| if (entity.id) return; |
| |
| let defaultSalterFn = (candidateId, index) => candidateId+"-"+index; |
| let uniqueSuffixFn = (candidateId, root, salterFn) => { |
| let matches = {}; |
| root.visitWithDescendants(e => { |
| if (e.id && e.id.startsWith(candidateId)) { |
| matches[e.id] = true; |
| } |
| }); |
| if (!matches[candidateId]) return candidateId; |
| let i=2; |
| while (true) { |
| let newCandidateId = (salterFn || defaultSalterFn)(candidateId, i); |
| if (!matches[newCandidateId]) return newCandidateId; |
| i++; |
| } |
| }; |
| |
| entity.id = (brBrandInfo.blueprintComposerIdGenerator || blueprintComposerIdGenerator)(entity, uniqueSuffixFn); |
| } |
| |
| function blueprintComposerIdGenerator(entity, uniqueSuffixFn) { |
| let candidate = entity.hasName() |
| ? entity.name.replace(/\W/g, '-').toLowerCase() |
| : entity.type ? entity.type.replace(/\W/g, '-').toLowerCase() |
| : !entity.parent ? "root" |
| : entity._id; |
| return uniqueSuffixFn( |
| candidate, |
| entity.getApplication() /* unique throughout blueprint */, |
| null /* use default salter */ ); |
| } |
| |
| } |