Murano/DSL/Blueprint
目录
YAML
YAML 是一种人类可读的数据序列化格式,它是 JSON 的超集。与 JSON 不同,YAML 旨在由人类读取和写入,并依赖于视觉缩进以表示数据结构的嵌套。这类似于 Python 如何使用缩进表示块结构,而不是像大多数 C 类语言那样使用花括号。此外,YAML 可以包含比 JSON 更多的数据类型。有关 YAML 的详细描述,请参阅 http://yaml.org/。
MuranoPL 被设计为可以用 YAML 表示,以便 MuranoPL 代码保持可读和结构化。因此,通常 MuranoPL 文件是 YAML 编码的文档。但是 MuranoPL 引擎本身并不直接处理 YAML 文档,由宿主机应用程序负责定位和反序列化特定类的定义。这使宿主机应用程序能够控制可以找到这些定义的位置(文件系统、数据库、远程存储库等),并可能使用其他序列化格式而不是 YAML。
MuranoPL 引擎依赖于宿主机反序列化代码,以自动检测源定义中的 YAQL 表达式,并将其作为 YaqlExpression 类的实例提供,而不是纯字符串。通常,YAQL 表达式可以通过 $(美元符号)和运算符的存在来区分,但在 YAML 中,开发人员始终可以使用 YAML 标签显式声明类型。所以
Some text - a string, $.something() - YAQL “$.something()” - string (because of quote marks) !!str $ - a string (because of YAML tag) !yaql "text" - YAQL (because of YAML tag)
YAQL
YAQL(Yet Another Query Language)是一种查询语言,它也是 Murano 项目的一部分。MuranoPL 大量使用 YAQL。YAQL 描述可以在这里找到:https://github.com/ativelkov/yaql
简单来说,YAQL 是一种表达式求值语言。2 + 2、foo() > bar()、true != false 都是有效的 YAQL 表达式。YAQL 中有趣的是,它没有内置函数列表。YAQL 可以访问的一切都是可定制的。YAQL 无法调用未显式注册为 YAQL 可访问的任何函数。运算符也是如此。因此,表达式 2 * foo(3, 4) 的结果完全取决于显式提供的“foo”和“operator_*”的实现。YAQL 使用美元符号 ($) 访问外部变量(也由宿主机应用程序显式提供)和函数参数。$variable 是获取变量“$variable”值的语法,$1、$2 等是函数参数的名称。“$”是当前对象(数据)的名称,表达式在其上求值,或者单个参数的名称。因此,表达式开头的 $ 和中间的 $ 可以指代不同的事物。
YAQL 具有许多开箱即用的函数,可以注册到 YAQL 上下文中。例如
$.where($.myObj.myScalar > 5 and $.myObj.myArray.len() > 0 and $.myObj.myArray.any($ = 4)).select($.myObj.myArray[0]) 可以在 $ = 对象数组上执行,并得到另一个数组,它是源数据的过滤和投影。这与 SQL 的工作方式非常相似,但使用更类似于 Python 的语法。
请注意,YAQL 中没有赋值运算符,并且“=”表示比较运算符,这与 Python 中的“==”含义相同。
由于 YAQL 无法访问底层操作系统资源并且 100% 可由宿主机控制,因此无需信任执行代码即可安全地执行 YAQL 表达式。此外,由于函数不是预定义的,因此在不同的上下文中可以访问不同的函数。因此,用于指定属性契约的 YAQL 表达式不一定在工作流定义中有效。
通用类结构
以下是类声明的常用模板。在下面的章节中,我将解释每个部分的意思。请注意,它是 YAML 格式。
Name: class name
Namespaces: namespaces specification
Extends: [list of parent classes]
Properties: properties declaration
Workflow:
methodName:
Arguments:
- list
- of
- arguments
Body:
- list
- of
- instructions
因此,MuranoPL 类是具有预定义键名称的 YAML 字典。除了 Name 之外的所有键都是可选的,可以省略(但如果存在则必须有效)
类名
类名是类的字母数字名称。按照惯例,所有类名都以大写字母开头,并以 PascalCasing 编写。
在 Murano 中,所有类名都是全局唯一的。这是通过使用命名空间来实现的。类名可以具有显式的命名空间规范(如 ns:MyName)或隐式的(如 MyName,如果 = 在名称规范中有效,则等于 =:MyName)
命名空间
命名空间声明指定了可以在类主体中使用的前缀,以缩短长类名。
Namespaces:
=: com.mirantis.murano.services.windows
srv: com.mirantis.murano.services
std: com.mirantis.murano
在上面的示例中,类名 srv:Something 将自动转换为“com.mirantis.murano.services.Something”。
“=”表示“当前命名空间”,因此在上面的示例中,“MyClass”将表示“com.mirantis.murano.services.windows.MyClass”。
如果类名在其名称中包含句点 (.),则假定它已经完全限定了命名空间,并且不会扩展。因此,ns.Myclass 将保持不变。
为了使类名全局唯一,建议将开发人员的域名作为命名空间的一部分(如示例中,类似于 Java)。
继承
MuranoPL 支持多重继承。如果存在,Extends 部分列出扩展的基类。如果列表包含单个条目,则可以将其写为标量字符串而不是数组。如果没有指定父类(或省略了键),则假定为“com.mirantis.murano.Object”,使其成为所有类层次结构的根类。
属性
属性是类属性,与方法一起构成公共类接口。通常(但不总是),属性是需要在环境设计器中输入的工作流调用之前需要的值和对其他对象的引用。
属性具有以下声明格式
propertyName:
Contract: property contract
Usage: property usage
Default: property default
契约
契约是 YAQL 表达式,说明了属性的期望值类型以及施加在属性上的其他约束。
| $.int() | 整数值(可能为 null)。由数字组成的字符串值将被转换为整数 |
| $.int().notNull() | 必需的整数 |
| $.string() $.string().notNull() |
与字符串相同。如果提供的value不是字符串,它将被转换为字符串 |
| $.bool() $.bool().notNull() |
布尔值是 true 和 false。0 转换为 false,其他整数转换为 true |
| $.class(ns:ClassName) $.class(ns:ClassName).notNull() |
该值必须是对指定类名称的实例的引用 |
| $.class(ns:ClassName, ns:DefaultClassName) | 如果没有提供实例,则创建 ns:DefaultClassName 类的实例 |
| $.class(ns:Name).check($.p = 12) | 该值必须是 ns:Name 类型,并且具有等于 12 的属性“p” |
| [$.int()] [$.int().notNull()] |
整数数组。与其他类型类似 |
| [$.int().check($ > 0)] | 正整数数组(因此不为 null) |
| [$.int(), $.string()] | 数组至少包含两个元素,第一个是 int,其余的是字符串 |
| [$.int(), 2] [$.int(), 2, 5] |
至少包含 2 个项目的 int 数组 最多 5 个项目 |
| { A: $.int(), B: [$.string()] } | 字典,其中“A”键为 int 类型,“B”为字符串数组 |
| $ [] {} |
任何标量或数据结构 任何数组 任何字典 |
| { $.string().notNull(): $.int().notNull() } | 字典 string -> int |
A: StringMap
$.string().notNull(): $
|
字典,其中“A”键必须等于“StringMap”,其他键可以是任何标量或数据结构 |
用法
用法说明了属性的用途。这暗示了谁以及如何访问它。可用的用法如下
| In | 输入属性。此类属性的值从用户处获取,并且不能在 MuranoPL 工作流中修改。这是 Usage 键的默认值 |
| Out | 该值是通过执行 MuranoPL 工作流获得的,并且不能被用户修改 |
| InOut | 用户和工作流都可以编辑该值 |
| Const | 与 In 相同,但工作流执行后,该属性不能再由用户或工作流更改 |
| Runtime | 属性仅在工作流内部可见。它既不从输入读取,也不序列化到工作流输出 |
Usage 属性是可选的,可以省略(这表示 In)。
如果工作流尝试写入未声明上述类型之一的属性,则认为它是私有的,仅对该类可访问(并且不会序列化到输出,因此在下次部署时会丢失)。尝试读取未初始化的属性会导致抛出异常。
默认
Default 是一个值,如果输入对象模型中未提及属性值(但如果提供为 null),则将使用该值。如果指定了 Default,则它必须符合声明的属性契约。如果未指定 Default,则 null 是默认值。
对于引用其他类的属性,Default 可以修改引用值的默认值。例如
p:
Contract: $.class(MyClass)
Default: {a: 12}
将覆盖为该属性创建的 MyClass 实例的 MyClass 的“a”属性的默认值。
工作流程
工作流是描述表示的 MuranoPL 类的实体部署方式的方法。
在典型场景中,输入数据模型中的根对象是 com.mirantis.murano.Environment 类型,并具有“deploy”方法。调用此方法会导致一系列基础设施活动(通常通过修改 Heat 堆栈和 VM 代理命令),从而导致执行部署脚本。工作流的作用是将输入对象模型(或先前执行的操作的结果)映射到这些活动的参数,并以正确的顺序启动这些活动。
方法具有输入参数并且可以返回一个值给调用者。在类的 Workflow 部分中定义的方法使用以下模板
methodName:
Arguments:
- list
- of
- arguments
Body:
- list
- of
- instructions
参数是可选的,并且(如果指定)使用与类属性相同的语法声明,除了 Type 属性对方法参数没有意义。例如,参数也具有契约和可选的默认值。
方法体是按顺序执行的一系列指令。在工作流主体中可以找到 3 种类型的指令:表达式、赋值和块结构。
表达式
表达式是 YAQL 表达式,它们为了副作用而执行。可以使用 $obj.methodName(arguments) 语法调用所有可访问的对象方法。
| $.methodName() $this.methodName() |
调用此(自身)对象上的“methodName”方法 |
| $.property.methodName() $this.property.methodName() |
调用“property”属性中的对象上的方法 |
| $.method(1, 2, 3) | 方法可以有参数 |
| $.method(1, 2, thirdParameter => 3) | 也支持命名参数 |
| list($.foo().bar($this.property), $p) | 可以构造复杂的表达式 |
赋值
赋值是具有 YAQL 表达式作为键和任意结构作为值的单键字典。这种结构被评估为赋值。
| $x: value | 将“value”分配给局部变量 $x |
| $.x: value $this.x: value |
将值分配给对象的属性 |
| $.x: $.y | 将属性“y”的值复制到属性“x” |
| $x: [$a, $b] | 将 $x 设置为两个值 $a 和 $b 的数组 |
$x:
SomeKey:
NestedKey: $variable
|
可以评估任何复杂程度的结构 |
| $.x[0]: value | 将值分配给属性 x 的第一个数组条目 |
| $.x.append(): value | 将值附加到属性 x 中的数组 |
| $.x.insert(1): value | 在位置 1 插入值 |
| $.x.key.subKey: value $.x[key][subKey]: value |
深度字典修改 |
块结构
块结构控制程序流程。块结构是所有键都是字符串的字典。可用的块结构如下
Return: value
|
返回方法的值 |
If: predicate()
Then:
- code
- block
Else:
- code
- block
|
predicate() 是必须评估为 true 或 false 的 YAQL 表达式 Else 部分是可选的。 可以作为标量而不是数组编写单行代码块。 |
While: predicate()
Do:
- code
- block
While loop.
|
predicate() 必须评估为 true 或 false |
For: variableName
In: collection
Do:
- code
- block
|
集合必须是 YAQL 表达式,返回可迭代集合或可评估数组,如赋值指令(例如 [1, 2, $x]) 在代码块内,循环变量可以作为 $variableName 访问 |
Repeat: $count
Do:
- code
- block
|
重复指定次数的代码块 |
Break:
|
中断循环 |
Match:
case1:
- code
- block
case2:
- code
- block
Value: $valueExpression()
Default:
- code
- block
|
将 $valueExpression() 的结果与一组可能的值(cases)进行匹配 执行第一个匹配 case 的代码块。如果没有 case 匹配并且存在 Default 键(它是可选的),则执行 Default 代码块。 case 值是常量值(不是表达式) |
Switch:
$predicate1() :
- code
- block
$predicate2() :
- code
- block
Default:
- code
- block
|
所有评估其 predicate 为 true 的代码块都会执行,但 predicate 评估的顺序是不固定的。 Default 键是可选的。如果没有 predicate 评估为 true,则执行 Default 代码块。 |
Parallel:
- code
- block
Limit: 5
|
在单独的绿色线程中并行执行代码块中的所有指令。 Limit 是可选的,表示并发绿色线程的最大数量。 |
对象模型
对象模型是对象及其属性的 JSON 序列化表示。用户在环境构建器(仪表板)中执行的所有操作都反映在对象模型中。对象模型在用户决定部署构建的环境时发送到 App Catalog 引擎。在引擎端,MuranoPL 对象从接收到的对象模型和预定义方法构造和初始化,并在根对象上执行。
对象序列化为 JSON 使用以下模板
{
"?": {
"id": "globally unique object ID (UUID)",
"type": "fully namespace-qualified class name",
"optional designer-related entries can be placed here": {
"key": "value"
}
},
"classProperty1": "propertyValue",
"classProperty2": 123,
"classProperty3": ["value1", "value2"],
"reference1": {
"?": {
"id": "object id",
"type": "object type"
},
"property": "value"
},
"reference2": "referenced object id"
}
对象可以识别为包含“?”条目的字典。所有系统字段都隐藏在该条目中。
有两种方法可以指定引用。第一种方法(“reference1”在示例中)允许内联定义对象。当引用的对象实例创建时,外部对象成为其父级(所有者),负责该对象。对象本身可能需要其父级(直接或间接)是指定类型(例如,所有应用程序都需要在父链中某个地方有 Environment)。
引用对象的第二种方法是通过指定其他对象 id。该对象必须在对象树中的其他地方定义。通过评估属性契约来区分对象引用和具有相同值的字符串。前者将具有 $.class(Name) 而后者将具有 $.string() 契约。