Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(xo-web/new-vm): ability to escape cloud config template variables #4501

Merged
merged 20 commits into from Sep 19, 2019
3 changes: 3 additions & 0 deletions @xen-orchestra/template/.babelrc.js
@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)
62 changes: 62 additions & 0 deletions @xen-orchestra/template/README.md
@@ -0,0 +1,62 @@
# @xen-orchestra/template [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)

## Install

Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/template):

```
> npm install --save @xen-orchestra/template
```

## Usage

Create a string replacer based on a pattern and a list of rules.

```js
const myReplacer = compileTemplate('{name}_COPY_\{name}_{id}_%\%', {
'{name}': vm => vm.name_label,
'{id}': vm => vm.id,
'%': (_, i) => i
})

const newString = myReplacer({
name_label: 'foo',
id: 42,
}, 32)

newString === 'foo_COPY_{name}_42_32%' // true
```

## Development

```
# Install dependencies
> yarn

# Run the tests
> yarn test

# Continuously compile
> yarn dev

# Continuously run the tests
> yarn dev-test

# Build for production (automatically called by npm install)
> yarn build
```

## Contributions

Contributions are *very* welcomed, either on the documentation or on
the code.

You may:

- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.

## License

ISC © [Vates SAS](https://vates.fr)
46 changes: 46 additions & 0 deletions @xen-orchestra/template/package.json
@@ -0,0 +1,46 @@
{
"name": "@xen-orchestra/template",
"version": "0.0.0",
"license": "ISC",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/template",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/template",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
},
"dependencies": {
"lodash": "^4.17.15"
}
}
19 changes: 19 additions & 0 deletions @xen-orchestra/template/src/index.js
@@ -0,0 +1,19 @@
import escapeRegExp from 'lodash/escapeRegExp'

const compareLengthDesc = (a, b) => b.length - a.length

export function compileTemplate(pattern, rules) {
const matches = Object.keys(rules)
.sort(compareLengthDesc)
.map(escapeRegExp)
.join('|')
const regExp = new RegExp(`\\\\(?:\\\\|${matches})|${matches}`, 'g')
return (...params) =>
pattern.replace(regExp, match => {
if (match[0] === '\\') {
return match.slice(1)
}
const rule = rules[match]
return typeof rule === 'function' ? rule(...params) : rule
})
}
14 changes: 14 additions & 0 deletions @xen-orchestra/template/src/index.spec.js
@@ -0,0 +1,14 @@
/* eslint-env jest */
import { compileTemplate } from '.'

it("correctly replaces the template's variables", () => {
const replacer = compileTemplate(
'{property}_\\{property}_\\\\{property}_{constant}_%_FOO',
{
'{property}': obj => obj.name,
'{constant}': 1235,
'%': (_, i) => i,
}
)
expect(replacer({ name: 'bar' }, 5)).toBe('bar_{property}_\\bar_1235_5_FOO')
})
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Expand Up @@ -23,6 +23,7 @@
- [Network] Fix inability to set a network name [#4514](https://github.com/vatesfr/xen-orchestra/issues/4514) (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
- [Backup NG] Fix race conditions that could lead to disabled jobs still running (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))

### Released packages

Expand All @@ -31,6 +32,7 @@
>
> Rule of thumb: add packages on top.

- @xen-orchestra/template v0.0.0
- @xen-orchestra/cron v1.0.4
- xo-server-sdn-controller v0.3.0
- xo-server v5.50.0
Expand Down
1 change: 1 addition & 0 deletions packages/xo-web/package.json
Expand Up @@ -34,6 +34,7 @@
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/template": "^0.0.0",
"ansi_up": "^4.0.3",
"asap": "^2.0.6",
"babel-core": "^6.26.0",
Expand Down
15 changes: 10 additions & 5 deletions packages/xo-web/src/common/cloud-config.js
Expand Up @@ -14,11 +14,16 @@ const AVAILABLE_TEMPLATE_VARS = {
const showAvailableTemplateVars = () =>
alert(
_('availableTemplateVarsTitle'),
<ul>
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
<li key={key}>{_.keyValue(key, _(value))}</li>
))}
</ul>
<div>
<ul>
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
<li key={key}>{_.keyValue(key, _(value))}</li>
))}
</ul>
<div className='text-info'>
<Icon icon='info' /> {_('templateEscape')}
</div>
</div>
)

const showNetworkConfigInfo = () =>
Expand Down
1 change: 1 addition & 0 deletions packages/xo-web/src/common/intl/messages.js
Expand Up @@ -1324,6 +1324,7 @@ const messages = {
availableTemplateVarsTitle: 'Available template variables',
templateNameInfo: 'the VM\'s name. It must not contain "_"',
templateIndexInfo: "the VM's index, it will take 0 in case of single VM",
templateEscape: 'Tip: escape any variable with a preceding backslash (\\)',
coreOsDefaultTemplateError:
'Error on getting the default coreOS cloud template',
newVmBootAfterCreate: 'Boot VM after creation',
Expand Down
31 changes: 0 additions & 31 deletions packages/xo-web/src/common/utils.js
Expand Up @@ -6,20 +6,16 @@ import { connect } from 'react-redux'
import { FormattedDate } from 'react-intl'
import {
clone,
escapeRegExp,
every,
forEach,
isArray,
isEmpty,
isFunction,
isPlainObject,
isString,
join,
keys,
map,
mapValues,
pick,
replace,
sample,
some,
} from 'lodash'
Expand Down Expand Up @@ -355,33 +351,6 @@ export const resolveResourceSet = resourceSet => {
export const resolveResourceSets = resourceSets =>
map(resourceSets, resolveResourceSet)

// -------------------------------------------------------------------

// Creates a string replacer based on a pattern and a list of rules
//
// ```js
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
// '{name}': vm => vm.name_label,
// '{id}': vm => vm.id,
// '%': (_, i) => i
// })
//
// const newString = myReplacer({
// name_label: 'foo',
// id: 42,
// }, 32)
//
// newString === 'foo_COPY_foo_42_32'
// ```
export function buildTemplate(pattern, rules) {
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
return (...params) =>
replace(pattern, regExp, match => {
const rule = rules[match]
return isFunction(rule) ? rule(...params) : rule
})
}

// ===================================================================

export const streamToString = getStream
Expand Down
5 changes: 3 additions & 2 deletions packages/xo-web/src/common/xo/copy-vms-modal/index.js
@@ -1,14 +1,15 @@
import _, { messages } from 'intl'
import map from 'lodash/map'
import React from 'react'
import { compileTemplate } from '@xen-orchestra/template'
import { injectIntl } from 'react-intl'

import BaseComponent from 'base-component'
import SingleLineRow from 'single-line-row'
import Upgrade from 'xoa-upgrade'
import { Col } from 'grid'
import { SelectSr } from 'select-objects'
import { buildTemplate, connectStore } from 'utils'
import { connectStore } from 'utils'

import SelectCompression from '../../select-compression'
import ZstdChecker from '../../zstd-checker'
Expand All @@ -35,7 +36,7 @@ class CopyVmsModalBody extends BaseComponent {
const names = namePattern
? map(
resolvedVms,
buildTemplate(namePattern, {
compileTemplate(namePattern, {
'{name}': vm => vm.name_label,
'{id}': vm => vm.id,
})
Expand Down
11 changes: 6 additions & 5 deletions packages/xo-web/src/common/xo/snapshot-vm-modal/index.js
@@ -1,10 +1,11 @@
import _ from 'intl'
import React from 'react'
import BaseComponent from 'base-component'
import { forEach } from 'lodash'
import { createGetObjectsOfType } from 'selectors'
import { buildTemplate, connectStore } from 'utils'
import { compileTemplate } from '@xen-orchestra/template'
import { connectStore } from 'utils'
import { Container, Col, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { forEach } from 'lodash'

const RULES = {
'{date}': () => new Date().toISOString(),
Expand All @@ -30,8 +31,8 @@ export default class SnapshotVmModalBody extends BaseComponent {
return { names: {}, descriptions: {}, saveMemory }
}

const generateName = buildTemplate(namePattern, RULES)
const generateDescription = buildTemplate(descriptionPattern, RULES)
const generateName = compileTemplate(namePattern, RULES)
const generateDescription = compileTemplate(descriptionPattern, RULES)
const names = {}
const descriptions = {}

Expand Down
10 changes: 5 additions & 5 deletions packages/xo-web/src/xo-app/new-vm/index.js
Expand Up @@ -15,16 +15,17 @@ import store from 'store'
import Tags from 'tags'
import Tooltip from 'tooltip'
import Wizard, { Section } from 'wizard'
import { compileTemplate } from '@xen-orchestra/template'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those changes are unrelated, leave them, but please avoid in the future.

import {
AvailableTemplateVars,
CAN_CLOUD_INIT,
DEFAULT_CLOUD_CONFIG_TEMPLATE,
DEFAULT_NETWORK_CONFIG_TEMPLATE,
NetworkConfigInfo,
} from 'cloud-config'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import {
Input as DebounceInput,
Textarea as DebounceTextarea,
Expand Down Expand Up @@ -78,7 +79,6 @@ import {
import { SizeInput, Toggle } from 'form'
import {
addSubscriptions,
buildTemplate,
connectStore,
formatSize,
getCoresPerSocketPossibilities,
Expand Down Expand Up @@ -744,7 +744,7 @@ export default class NewVm extends BaseComponent {
)

_buildTemplate = pattern =>
buildTemplate(pattern, {
compileTemplate(pattern, {
'{name}': state => state.name_label || '',
'%': (_, i) => i,
})
Expand Down