_ __ __
___ _ __ (_)/ _|/ _| _ _
/ __| '_ | | |_| |_ _| |_ _| |_
__ |_) | | _| _|_ _|_ _|
|___/ .__/|_|_| |_| |_| |_|
|_|
spiff++
es una bifurcación de spiff que proporciona una extensión compatible con spiff basada en la última versión que ofrece un amplio conjunto de funciones nuevas que aún no están disponibles en spiff. Todas las correcciones proporcionadas por el proyecto spiff original también se incorporarán a spiff++. Debido a que no habrá forma de regresar a la base fuente de spiff, se ha creado un nuevo repositorio independiente de spiff++ para continuar con el desarrollo de spiff++.spiff es una herramienta de línea de comandos y un sistema de plantillas YAML híbrido declarativo en el dominio. Mientras que los sistemas de plantillas normales procesan un archivo de plantilla sustituyendo las expresiones de la plantilla por valores tomados de fuentes de datos externas, en el dominio significa que el motor de plantillas conoce la sintaxis y la estructura de la plantilla procesada. Por lo tanto, puede tomar los valores de las expresiones de plantilla directamente del documento procesado, incluidas aquellas partes indicadas por las propias expresiones de plantilla.
Por ejemplo:
resource :
name : bosh deployment
version : 25
url : (( "http://resource.location/bosh?version=" version ))
description : (( "This document describes a " name " located at " url ))
En lugar de utilizar únicamente fuentes de valor externas, spiff proporciona un mecanismo de combinación para combinar una plantilla con cualquier número de apéndices de combinación para producir un documento final.
Es una herramienta de línea de comandos y un sistema de plantillas YAML declarativo, especialmente diseñado para generar manifiestos de implementación (por ejemplo, manifiestos BOSH, Kubernetes o Landscaper).
Además de la CLI, existe una biblioteca golang que permite el uso del procesamiento de plantillas spiff en cualquier programa GO (por ejemplo, Landscaper).
El motor de plantillas ofrece acceso al sistema de archivos basado en un sistema de archivos virtual configurable o al sistema de proceso para ejecutar comandos e incorporar la salida al procesamiento de la plantilla.
Contenido:
<<if:
<<switch:
<<type:
<<for:
<<merge:
Los binarios ejecutables de la versión oficial se pueden descargar a través de las versiones de Github para máquinas Darwin, Linux y PowerPC (y máquinas virtuales).
Algunas de las dependencias de spiff han cambiado desde la última versión oficial y spiff no se actualizará para mantenerse al día con estas dependencias. Esas dependencias se arreglan o se copian en la base del código local.
spiff merge template.yml [template2.yml ...]
Combine un montón de archivos de plantilla en un manifiesto e imprímalo.
Consulte 'lenguaje de plantillas dinámicas' para obtener detalles del archivo de plantilla, o ejemplos/subdir para ejemplos más complicados.
Ejemplo:
spiff merge cf-release/templates/cf-deployment.yml my-cloud-stub.yml
Es posible leer un archivo desde la entrada estándar usando el nombre de archivo -
. Sólo podrá usarse una vez. Esto permite usar spiff como parte de una canalización para procesar simplemente una única secuencia o para procesar una secuencia basada en varias plantillas/stubs.
El archivo de plantilla (primer argumento) puede ser una secuencia de múltiples documentos que contenga múltiples documentos YAML separados por una línea que contenga solo ---
. Cada documento YAML se procesará de forma independiente con los archivos resguardados proporcionados. El resultado es el flujo de documentos procesados en el mismo orden. Si el nodo raíz de un documento está marcado como temporal, el documento se omite del flujo de salida. Por ejemplo, esto se puede utilizar para generar manifiestos de Kubernetes para que los utilice kubectl
.
El comando merge
ofrece varias opciones:
La opción --partial
. Si se proporciona esta opción, spiff maneja la evaluación de expresiones incompletas. Todos los errores se ignoran y las partes irresolubles del documento yaml se devuelven como cadenas.
Con la opción --json
la salida estará en formato JSON en lugar de YAML.
La opción --path <path>
se puede utilizar para generar una ruta anidada, en lugar del documento procesado completo.
Si la salida es una lista, la opción --split
genera cada elemento de la lista como un documento separado. El formato yaml utiliza como de costumbre ---
como línea separadora. El formato json genera una secuencia de documentos json , uno por línea.
Con --select <field path>
es posible seleccionar un campo dedicado del documento procesado para la salida
Con --evaluate <dynaml expression>
es posible evaluar una expresión dinámica determinada en el documento procesado para la salida. La expresión se evalúa antes de aplicar la ruta de selección, que luego funcionará en el resultado de la evaluación.
La opción --state <path>
habilita el soporte estatal de spiff . Si el archivo dado existe, se coloca en la parte superior de la lista de códigos auxiliares configurados para el procesamiento de fusión. Además de la salida del documento procesado, se filtra por nodos marcados con el marcador &state
. Este documento filtrado luego se almacena en el archivo indicado, guardando el archivo de estado anterior con el sufijo .bak
. Esto se puede utilizar junto con una combinación manual como la que ofrece la biblioteca de servicios públicos del estado.
Con la opción --bindings <path>
se puede especificar un archivo yaml, cuyo contenido se utiliza para crear enlaces adicionales para el procesamiento. El documento yaml debe consistir en un mapa. Cada clave se utiliza como enlace adicional. El documento de vinculaciones no se procesa, los valores se utilizan tal como se definen.
Con la opción --tag <tag>:<path>
se puede especificar un archivo yaml, cuyo contenido se utiliza como valor para una etiqueta global predefinida (ver Etiquetas). Se puede acceder a las etiquetas mediante expresiones de referencia del formato <tag>::<ref>
. A diferencia de los enlaces, el contenido etiquetado no compite con los nodos del documento, sino que utiliza otro espacio de nombres de referencia.
Con la opción --define <key>=<value>
(taquigrafía -D
) se pueden especificar valores de enlace adicionales en la línea de comando anulando los valores de enlace del archivo de enlace. La opción puede ocurrir varias veces.
Si la clave contiene puntos ( .
), se interpretará como una expresión de ruta para describir campos en valores de mapa profundos. Un punto (y un antes de un punto) se puede escapar con
para mantenerlo en el nombre del campo.
La opción --preserve-escapes
preservará el escape para expresiones dinámicas y directivas de combinación de lista/mapa. Esta opción se puede utilizar si se desean realizar más pasos de procesamiento de un resultado de procesamiento con spiff .
La opción --preserve-temporary
preservará los campos marcados como temporales en el documento final.
La opción --features=<featurelist>
habilitará estas funciones determinadas. Las nuevas funciones que sean incompatibles con el comportamiento anterior deben habilitarse explícitamente. Por lo general, esas características no rompen el comportamiento común, sino que introducen una interpretación dedicada para los valores yaml que se usaban antes como valores regulares.
Las bibliotecas de carpetas ofrecen algunas bibliotecas de utilidades útiles. También se pueden utilizar como ejemplo de la potencia de este motor de plantillas.
spiff diff manifest.yml other-manifest.yml
Muestre las diferencias estructurales entre dos manifiestos de implementación. Aquí también se admiten transmisiones con múltiples documentos. Para indicar que no hay diferencia, la cantidad de documentos en ambas secuencias debe ser idéntica y cada documento en la primera secuencia no debe tener diferencias en comparación con el documento con el mismo índice en la segunda secuencia. Las diferencias encontradas se muestran para cada documento por separado.
A diferencia de las herramientas de diferenciación básicas e incluso de bosh diff
, este comando tiene conocimiento semántico de un manifiesto de implementación y no está basado solo en texto. Por ejemplo, si dos manifiestos son iguales excepto que tienen algunos trabajos enumerados en diferentes órdenes, spiff diff
detectará esto, ya que el orden de los trabajos es importante en un manifiesto. Por otro lado, si dos manifiestos difieren solo en el orden de sus grupos de recursos, por ejemplo, producirá una diferencia vacía ya que el orden del grupo de recursos en realidad no importa para una implementación.
Además, a diferencia de bosh diff
, este comando no modifica ninguno de los archivos.
Está seguido para comprobar las diferencias entre una implementación y la siguiente.
Flujo típico:
$ spiff merge template.yml [templates...] > deployment.yml
$ bosh download manifest [deployment] current.yml
$ spiff diff deployment.yml current.yml
$ bosh deployment deployment.yml
$ bosh deploy
spiff convert --json manifest.yml
El subcomando convert
se puede utilizar para convertir archivos de entrada a json o simplemente para normalizar el orden de los campos. Las opciones disponibles son --json
, --path
, --split
o --select
según su significado para el subcomando merge
.
spiff encrypt secret.yaml
El subcomando encrypt
se puede utilizar para cifrar o descifrar datos de acuerdo con la función encrypt
dinámico. La contraseña se puede proporcionar como segundo argumento o se puede tomar de la variable de entorno SPIFF_ENCRYPTION_KEY
. El último argumento se puede utilizar para pasar el método de cifrado (consulte la función encrypt
)
Los datos se toman del archivo especificado. Si se proporciona -
, se lee desde la entrada estándar.
Si se proporciona la opción -d
, los datos se descifran; de lo contrario, los datos se leen como un documento yaml y se imprime el resultado cifrado.
Las nuevas funciones que sean incompatibles con el comportamiento anterior deben habilitarse explícitamente. Por lo general, esas características no rompen el comportamiento común, pero introducen una interpretación dedicada para los valores yaml que se usaban antes como valores regulares y, por lo tanto, pueden romper los casos de uso existentes.
Actualmente se admiten las siguientes marcas de características:
Característica | Desde | Estado | Significado |
---|---|---|---|
interpolation | 1.7.0-beta-1 | alfa | dynaml como parte de cadenas yaml |
control | 1.7.0-beta-4 | alfa | estructuras de control basadas en yaml |
Los indicadores de funciones activas se pueden consultar utilizando la función dinámica features()
como lista de cadenas. Si esta función se llama con un argumento de cadena, devuelve si la característica dada está actualmente habilitada.
Las características se pueden habilitar mediante la línea de comando usando la opción --features
, mediante la biblioteca go usando la función WithFeatures
o, en general, configurando la variable de entorno SPIFF_FEATURES
en una lista de características. Esta configuración siempre se utilizó de forma predeterminada. Al utilizar la configuración spiff Plain()
de la biblioteca go, se ignoran todas las variables de entorno.
Una función se puede especificar por nombre o por nombre precedido del prefijo no
para desactivarla.
La carpeta de bibliotecas contiene algunas bibliotecas de plantillas útiles. Básicamente, estos son solo códigos auxiliares que se agregan a la lista de archivos de combinación para ofrecer funciones de utilidad para el procesamiento de la combinación.
Spiff utiliza un lenguaje de plantillas declarativo y sin lógica llamado 'dynaml' (yaml dinámico).
Se garantiza que cada nodo dynaml se resolverá en un nodo YAML. No es interpolación de cadenas. Esto evita que los desarrolladores tengan que pensar en cómo se representará un valor en la plantilla resultante.
Un nodo dynaml aparece en el archivo .yml como una cadena que indica una expresión rodeada por dos paréntesis (( <dynaml> ))
. Se pueden utilizar como valor de un mapa o como entrada en una lista. La expresión puede abarcar varias líneas. En cualquier caso, el valor de la cadena yaml no debe terminar con una nueva línea (por ejemplo, usando |-
).
Si un valor entre paréntesis no debe interpretarse como una expresión dinámica y mantenerse tal como está en la salida, se puede escapar mediante un signo de exclamación directamente después de los corchetes de apertura.
Por ejemplo, ((! .field ))
se asigna al valor de cadena (( .field ))
y ((!! .field ))
se asigna al valor de cadena ((! .field ))
.
La siguiente es una lista completa de expresiones dinámicas:
(( foo ))
Busque la clave 'foo' más cercana (es decir, alcance léxico) en la plantilla actual e introdúzcala.
p.ej:
fizz :
buzz :
foo : 1
bar : (( foo ))
bar : (( foo ))
foo : 3
bar : (( foo ))
Este ejemplo resolverá:
fizz :
buzz :
foo : 1
bar : 1
bar : 3
foo : 3
bar : 3
Lo siguiente no se resolverá porque el nombre de la clave es el mismo que el valor que se fusionará:
foo : 1
hi :
foo : (( foo ))
(( foo.bar.[1].baz ))
Busque la clave 'foo' más cercana y desde allí siga hasta .bar.[1].baz
.
Un camino es una secuencia de pasos separados por puntos. Un paso es una palabra para mapas o dígitos entre corchetes para la indexación de listas. El índice puede ser negativo (un menos seguido de dígitos). Los índices negativos se toman desde el final de la lista (índice efectivo = índice + longitud (lista)).
Un camino que no se puede resolver conduce a un error de evaluación. Si se espera que en ocasiones no se proporcione una referencia, se debe utilizar en combinación con '||' (ver más abajo) para garantizar la resolución.
Nota : La gramática dinámica ha sido reelaborada para habilitar la sintaxis de índice habitual. En lugar de foo.bar.[1]
ahora es posible utilizar foo.bar[1]
.
Nota : Las referencias siempre están dentro de la plantilla o del código auxiliar y el orden no importa. Puede hacer referencia a otro nodo dinámico y suponer que está resuelto, y el nodo de referencia eventualmente se resolverá una vez que se resuelva el nodo dependiente.
p.ej:
properties :
foo : (( something.from.the.stub ))
something : (( merge ))
Esto se resolverá siempre que "algo" se pueda resolver y siempre que genere algo como esto:
from :
the :
stub : foo
Si la ruta comienza con un punto ( .
), la ruta siempre se evalúa desde la raíz del documento. Si la raíz del documento es una lista, el primer nivel del mapa se usa para resolver la expresión de ruta si comienza con .__map
. Esto se puede utilizar para evitar la necesidad de utilizar el propio índice de la lista (como .[1].path
), que podría cambiar si se agregan entradas de la lista.
Las entradas de la lista que consisten en un mapa con un campo name
se pueden abordar directamente por su valor de nombre como componente de ruta.
Nota : Esto también funciona para las rutas absolutas de los documentos de lista.
p.ej:
La era de alicia en
list :
- name : alice
age : 25
Se puede hacer referencia a ella utilizando la ruta list.alice.age
, en lugar de list[0].age
.
De forma predeterminada, se utiliza un campo con name
como campo clave. Si se debe utilizar otro campo como campo clave, se puede marcar en una entrada de la lista como clave anteponiendo al nombre del campo la key:
. Esta palabra clave se elimina durante el procesamiento y no formará parte del resultado final del procesamiento.
p.ej:
list :
- key:person : alice
age : 25
alice : (( list.alice ))
se resolverá
list :
- person : alice
age : 25
alice :
person : alice
age : 25
Este nuevo campo clave también se observará durante la combinación de listas.
Si el campo clave seleccionado comienza con !
, la función clave está desactivada. El signo de exclamación también se elimina del nombre del campo efectivo.
Si los valores para el campo clave no son únicos, también se deshabilita.
(( foo.[bar].baz ))
Busque la clave 'foo' más cercana y desde allí siga hasta los campos descritos por la bar
de expresión y luego hasta .baz.
El índice puede ser una constante entera (sin espacios) como se describe en la última sección. Pero también podría ser una expresión dinámica arbitraria (incluso un número entero, pero con espacios). Si la expresión se evalúa como una cadena, busca el campo dedicado. Si la expresión se evalúa como un número entero, se aborda el elemento de la matriz con este índice. El punto ( .
) delante del operador de índice es opcional.
p.ej:
properties :
name : alice
foo : (( values.[name].bar ))
values :
alice :
bar : 42
Esto resolverá foo
al valor 42
. El índice dinámico también puede estar al final de la expresión (sin .bar
).
Básicamente, esta es la forma más sencilla de expresar algo como eval("valores". nombre ".bar")
Si la expresión se evalúa como una lista, los elementos de la lista (cadenas o números enteros) se utilizan como elementos de ruta para acceder a campos más profundos.
p.ej:
properties :
name :
- foo
- bar
foo : (( values.[name] ))
values :
foo :
bar : 42
resuelve foo
nuevamente al valor 42
.
Nota : El operador de índice también se puede utilizar en el elemento raíz ( .[index]
).
Es posible especificar múltiples índices separados por comas en listas sucesivas ( foo[0][1]
es equivalente a `foo[0,1]). En tal caso los índices no podrán volver a ser listas.
(( list.[1..3] ))
La expresión de segmento se puede utilizar para extraer una sublista dedicada de una expresión de lista. El rango start ..
end extrae una lista de longitud end-start+1 con los elementos desde el inicio del índice hasta el final . Si el índice inicial es negativo, el segmento se toma desde el final de la lista desde longitud+inicio hasta longitud+fin . Si el índice final es menor que el índice inicial, el resultado es una matriz vacía.
p.ej:
list :
- a
- b
- c
foo : (( list.[1..length(list) - 1] ))
Es posible que se omita el índice inicial o final. Luego se selecciona según el tamaño real de la lista. Por lo tanto list.[1..length(list)]
es equivalente a list.[1..]
.
evalúa foo
a la lista [b,c]
.
(( 1.2e4 ))
Se admiten letras numéricas para números enteros y valores de coma flotante.
(( "foo" ))
Literal de cadena. Se admiten todas las codificaciones de cadenas json (por ejemplo n
, "
o uxxxx
).
(( [ 1, 2, 3 ] ))
Lista literal. Los elementos de la lista podrían volver a ser expresiones. Hay un literal de lista especial [1 .. -1]
que se puede usar para resolver un rango de números creciente o decreciente en una lista.
p.ej:
list : (( [ 1 .. -1 ] ))
rendimientos
list :
- 1
- 0
- -1
(( { "alice" = 25 } ))
El literal de mapa se puede utilizar para describir mapas como parte de una expresión dinámica. Tanto la clave como el valor podrían ser nuevamente expresiones, por lo que la expresión clave debe evaluarse como una cadena. De esta forma es posible crear mapas con claves no estáticas. Se ha elegido el operador de asignación =
en lugar de los dos puntos habituales :
carácter utilizado en yaml, porque esto daría lugar a conflictos con la sintaxis de yaml.
Un literal de mapa puede constar de cualquier número de asignaciones de campos separadas por una coma ,
.
p.ej:
name : peter
age : 23
map : (( { "alice" = {}, name = age } ))
rendimientos
name : peter
age : 23
map :
alice : {}
peter : 23
Otra forma de componer listas basadas en expresiones son las funciones makemap
y list_to_map
.
(( ( "alice" = 25 ) alice ))
Cualquier expresión puede estar precedida por cualquier número de literales de alcance explícitos. Un literal de alcance describe un mapa cuyos valores están disponibles para la resolución de referencia relativa de la expresión (alcance estático). Crea un enlace local adicional para los nombres de pila.
Un literal de alcance puede consistir en cualquier número de asignaciones de campos separadas por una coma ,
. Tanto la clave como el valor vienen dados por expresiones, mientras que la expresión clave debe evaluarse como una cadena. Todas las expresiones se evalúan en el siguiente alcance externo, esto significa que las configuraciones posteriores en un alcance no pueden usar configuraciones anteriores en el mismo literal de alcance.
p.ej:
scoped : (( ( "alice" = 25, "bob" = 26 ) alice + bob ))
rendimientos
scoped : 51
El nombre de un campo también puede indicarse mediante un símbolo ( $
nombre ).
(( foo bar ))
Expresión de concatenación utilizada para concatenar una secuencia de expresiones dinámicas.
(( "foo" bar ))
Concatenación (donde bar es otra expresión dinámica). Cualquier secuencia de valores simples (cadena, entero y booleano) se puede concatenar, dada por cualquier expresión dinámica.
p.ej:
domain : example.com
uri : (( "https://" domain ))
En este ejemplo, uri
se resolverá con el valor "https://example.com"
.
(( [1,2] bar ))
Concatenación de listas como expresión (donde bar es otra expresión dinámica). Se puede concatenar cualquier secuencia de listas, dada por cualquier expresión dinámica.
p.ej:
other_ips : [ 10.0.0.2, 10.0.0.3 ]
static_ips : (( ["10.0.1.2","10.0.1.3"] other_ips ))
En este ejemplo, static_ips
se resolverá con el valor [ 10.0.1.2, 10.0.1.3, 10.0.0.2, 10.0.0.3 ]
.
Si la segunda expresión se evalúa como un valor distinto de una lista (entero, booleano, cadena o mapa), el valor se agrega a la primera lista.
p.ej:
foo : 3
bar : (( [1] 2 foo "alice" ))
produce la lista [ 1, 2, 3, "alice" ]
para bar
.
(( map1 map2 ))
Concatenación de mapas como expresión. Se puede concatenar cualquier secuencia de mapas, dada por cualquier expresión dinámica. De este modo las entradas se fusionarán. Las entradas con la misma clave se sobrescriben de izquierda a derecha.
p.ej:
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat : (( foo bar ))
rendimientos
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat :
alice : 24
bob : 26
paul : 27
(( auto ))
Cálculo automático de valores sensible al contexto.
En el atributo 'tamaño' de un grupo de recursos, esto significa calcular en función del total de instancias de todos los trabajos que declaran estar en el grupo de recursos actual.
p.ej:
resource_pools :
- name : mypool
size : (( auto ))
jobs :
- name : myjob
resource_pool : mypool
instances : 2
- name : myotherjob
resource_pool : mypool
instances : 3
- name : yetanotherjob
resource_pool : otherpool
instances : 3
En este caso, el tamaño del grupo de recursos se resolverá en "5".
(( merge ))
Traiga la ruta actual desde los archivos auxiliares que se están fusionando.
p.ej:
foo :
bar :
baz : (( merge ))
Intentará traer foo.bar.baz
desde el primer código auxiliar, o el segundo, etc., devolviendo el valor del último código auxiliar que lo proporciona.
Si el valor correspondiente no está definido, devolverá nulo. Entonces tiene la misma semántica que las expresiones de referencia; una combinación nula es una plantilla sin resolver. Ver ||
.
<<: (( merge ))
Fusión de mapas o listas con el contenido de un mismo elemento encontrado en algún stub.
** Atención ** Esta forma de merge
tiene un problema de compatibilidad. En versiones anteriores a la 1.0.8, esta expresión nunca se analizaba, solo era relevante la existencia de la clave <<:
:. Por lo tanto, a menudo hay usos de <<: (( merge ))
donde se refiere a <<: (( merge || nil ))
. La primera variante requeriría contenido en al menos un código auxiliar (como siempre para el operador de fusión). Ahora esta expresión se evalúa correctamente, pero esto rompería los conjuntos de plantillas de manifiesto existentes, que usan la primera variante, pero significan la segunda. Por lo tanto, este caso se trata explícitamente para describir una fusión opcional. Si realmente se trata de una fusión requerida, se debe incluir un calificador explícito adicional.
Nota : en lugar de usar un campo de inserción <<:
para colocar expresiones de combinación, ahora también es posible usar <<<:
lo que permite usar analizadores yaml normales para documentos yaml elegantes. <<:
se mantiene por compatibilidad con versiones anteriores. ser utilizado ( (( merge required ))
).
Si la clave de combinación no debe interpretarse como una clave normal en lugar de una directiva de combinación, se puede escapar con un signo de exclamación ( !
).
Por ejemplo, una clave de mapa <<<!
dará como resultado una clave de cadena <<<
y <<<!!
dará como resultado una clave de cadena <<<!
valores.yml
foo :
a : 1
b : 2
plantilla.yml
foo :
<< : (( merge ))
b : 3
c : 4
spiff merge template.yml values.yml
produce:
foo :
a : 1
b : 2
c : 4
valores.yml
foo :
- 1
- 2
plantilla.yml
foo :
- 3
- << : (( merge ))
- 4
spiff merge template.yml values.yml
produce:
foo :
- 3
- 1
- 2
- 4
- <<: (( merge on key ))
spiff
puede fusionar listas de mapas con un campo clave. Esas listas se manejan como mapas con el valor del campo clave como clave. De forma predeterminada se utiliza el name
de la clave. Pero con el selector on
un nombre de clave arbitrario se puede especificar para una expresión de combinación de lista.
p.ej:
list :
- << : (( merge on key ))
- key : alice
age : 25
- key : bob
age : 24
fusionado con
list :
- key : alice
age : 20
- key : peter
age : 13
rendimientos
list :
- key : peter
age : 13
- key : alice
age : 20
- key : bob
age : 24
Si no se desea insertar nuevas entradas (como lo solicita la expresión de combinación de inserción), sino solo anular las entradas existentes, un campo clave existente puede tener como prefijo la etiqueta key:
para indicar un nombre de clave no estándar, por ejemplo - key:key: alice
.
<<: (( merge replace ))
Reemplaza el contenido completo de un elemento por el contenido que se encuentra en algún código auxiliar en lugar de realizar una combinación profunda del contenido existente.
valores.yml
foo :
a : 1
b : 2
plantilla.yml
foo :
<< : (( merge replace ))
b : 3
c : 4
spiff merge template.yml values.yml
produce:
foo :
a : 1
b : 2
valores.yml
foo :
- 1
- 2
plantilla.yml
foo :
- << : (( merge replace ))
- 3
- 4
spiff merge template.yml values.yml
produce:
foo :
- 1
- 2
<<: (( foo ))
Fusión de mapas y listas que se encuentran en la misma plantilla o código auxiliar.
foo :
a : 1
b : 2
bar :
<< : (( foo )) # any dynaml expression
b : 3
rendimientos:
foo :
a : 1
b : 2
bar :
a : 1
b : 3
Esta expresión simplemente agrega nuevas entradas a la lista real. No fusiona entradas existentes con el contenido descrito por la expresión de fusión.
bar :
- 1
- 2
foo :
- 3
- << : (( bar ))
- 4
rendimientos:
bar :
- 1
- 2
foo :
- 3
- 1
- 2
- 4
Un caso de uso común para esto es fusionar listas de rangos o ips estáticas en una lista de ips. Otra posibilidad es utilizar una única expresión de concatenación.
<<: (( merge foo ))
Fusión de mapas o listas con el contenido de un elemento arbitrario encontrado en algún stub (Redirecting merge). No habrá más fusiones (profundas) con el elemento del mismo nombre que se encuentra en algún código auxiliar. (La combinación profunda de listas requiere mapas con name
del campo)
Las fusiones de redireccionamiento también se pueden utilizar como valor de campo directo. Se pueden combinar con reemplazo de fusiones como (( merge replace foo ))
.
valores.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
plantilla.yml
foo :
<< : (( merge bar))
b : 3
c : 4
spiff merge template.yml values.yml
produce:
foo :
a : 1
b : 2
c : 4
Otra forma de fusionar con otro elemento en algún stub también se podría hacer de la forma tradicional:
valores.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
plantilla.yml
bar :
<< : (( merge ))
b : 3
c : 4
foo : (( bar ))
Pero en este escenario la combinación aún realiza la combinación profunda con el nombre del elemento original. Por lo tanto, spiff merge template.yml values.yml
produce:
bar :
a : 1
b : 2
c : 4
foo :
a : 10
b : 20
c : 4
valores.yml
foo :
- 10
- 20
bar :
- 1
- 2
plantilla.yml
foo :
- 3
- << : (( merge bar ))
- 4
spiff merge template.yml values.yml
produce:
foo :
- 3
- 1
- 2
- 4
<<: (( merge none ))
Si la referencia de una combinación de redireccionamiento se establece en la constante none
, no se realiza ninguna combinación. Esta expresión siempre produce el valor nulo.
por ejemplo: para
plantilla.yml
map :
<< : (( merge none ))
value : notmerged
valores.yml
map :
value : merged
spiff merge template.yml values.yml
produce:
map :
value : notmerged
Esto se puede usar para fusionar campos explícitos usando la función stub
para acceder a partes dedicadas de códigos auxiliares ascendentes.
p.ej:
plantilla.yml
map :
<< : (( merge none ))
value : (( "alice" "+" stub(map.value) ))
valores.yml
map :
value : bob
spiff merge template.yml values.yml
produce:
test :
value : alice+bob
Esto también funciona para campos dedicados:
plantilla.yml
map :
value : (( merge none // "alice" "+" stub() ))
valores.yml
map :
value : bob
spiff merge template.yml values.yml
produce:
test :
value : alice+bob
(( a || b ))
Utiliza aob si a no se puede resolver.
p.ej:
foo :
bar :
- name : some
- name : complicated
- name : structure
mything :
complicated_structure : (( merge || foo.bar ))
Esto intentará fusionarse en mything.complicated_structure
o, si no se puede fusionar, usará el valor predeterminado especificado en foo.bar
.
El operador //
comprueba además si a
se puede resolver con un valor válido (distinto de ~
).
(( 1 + 2 * foo ))
Las expresiones dinámicas se pueden utilizar para ejecutar cálculos aritméticos de números enteros y de punto flotante. Las operaciones admitidas son +
, -
, *
y /
. El operador de módulo ( %
) solo admite operandos enteros.
p.ej:
valores.yml
foo : 3
bar : (( 1 + 2 * foo ))
spiff merge values.yml
produce 7
para bar
. Esto se puede combinar con concatenaciones (el cálculo tiene mayor prioridad que la concatenación en expresiones dinámicas):
foo : 3
bar : (( foo " times 2 yields " 2 * foo ))
El resultado es que la cadena 3 times 2 yields 6
.
(( "10.10.10.10" - 11 ))
Además de la aritmética con números enteros, también es posible utilizar la suma y la resta en direcciones IP y cidrs.
p.ej:
ip : 10.10.10.10
range : (( ip "-" ip + 247 + 256 * 256 ))
rendimientos
ip : 10.10.10.10
range : 10.10.10.10-10.11.11.1
La resta también funciona en dos direcciones IP o cidrs para calcular la cantidad de direcciones IP entre dos direcciones IP.
p.ej:
diff : (( 10.0.1.0 - 10.0.0.1 + 1 ))
produce el valor 256. Las constantes de dirección IP se pueden usar directamente en expresiones dinámicas. Se convierten implícitamente en cadenas y nuevamente en direcciones IP si una operación lo requiere.
La multiplicación y la división se pueden utilizar para manejar cambios de rango de IP en CIDR. Con la división se puede particionar una red. El tamaño de la red aumenta para permitir al menos una cantidad dedicada de subredes debajo del CIDR original. Luego, la multiplicación se puede utilizar para obtener la enésima subred siguiente del mismo tamaño.
p.ej:
subnet : (( "10.1.2.1/24" / 12 )) # first subnet CIDR for 16 subnets
next : (( "10.1.2.1/24" / 12 * 2)) # 2nd next (3rd) subnet CIDRS
rendimientos
subnet : 10.1.2.0/28
next : 10.1.2.32/28
Además, hay funciones que funcionan en CIDR IPv4:
cidr : 192.168.0.1/24
range : (( min_ip(cidr) "-" max_ip(cidr) ))
next : (( max_ip(cidr) + 1 ))
num : (( min_ip(cidr) "+" num_ip(cidr) "=" min_ip(cidr) + num_ip(cidr) ))
contains : (( contains_ip(cidr, "192.168.0.2") ))
rendimientos
cidr : 192.168.0.1/24
range : 192.168.0.0-192.168.0.255
next : 192.168.1.0
num : 192.168.0.0+256=192.168.1.0
contains : true
(( a > 1 ? foo :bar ))
Dynaml admite los operadores de comparación <
, <=
, ==
, !=
, >=
y >
. Los operadores de comparación trabajan con valores enteros. Los controles de igualdad también funcionan en listas y mapas. El resultado es siempre un valor booleano. Para negar una condición, se puede utilizar el operador unario not ( !
).
Además existe el operador condicional ternario ?:
, que se puede utilizar para evaluar expresiones dependiendo de una condición. El primer operando se utiliza como condición. La expresión se evalúa hasta el segundo operando, si la condición es verdadera, y hasta el tercero, en caso contrario.
p.ej:
foo : alice
bar : bob
age : 24
name : (( age > 24 ? foo :bar ))
produce el valor bob
para el name
de la propiedad.
Una expresión se considera false
si su evaluación es
false
En caso contrario se considera true
Observación
El uso del símbolo :
puede entrar en conflicto con la sintaxis de yaml, si la expresión completa no es un valor de cadena entrecomillado.
Los operadores -or
y -and
se pueden utilizar para combinar operadores de comparación para componer condiciones más complejas.
Observación:
El símbolo del operador más tradicional ||
(y &&
) no se pueden utilizar aquí porque el operador ||
ya existe en dynaml con una semántica diferente, que no es válida para operaciones lógicas. La expresión false || true
se evalúa como false
porque produce el primer operando, si está definido, independientemente de su valor. Para ser lo más compatible posible, esto no se puede cambiar y los símbolos simples or
y and
no se pueden usar, porque esto invalidaría la concatenación de referencias con dichos nombres.
(( 5 -or 6 ))
Si ambos lados de un operador -or
o -and
se evalúan como valores enteros, se ejecuta una operación bit a bit y el resultado es nuevamente un número entero. Por lo tanto, la expresión 5 -or 6
se evalúa como 7
.
Dynaml admite un conjunto de funciones predefinidas. Una función generalmente se llama así
result : (( functionname(arg, arg, ...) ))
Se pueden definir funciones adicionales como parte del documento yaml utilizando expresiones lambda. Entonces, el nombre de la función es una expresión agrupada o la ruta al nodo que aloja la expresión lambda.
(( format( "%s %d", alice, 25) ))
Formatee una cadena basándose en argumentos dados por expresiones dinámicas. Hay una segunda versión de esta función: error
formatea un mensaje de error y establece la evaluación como fallida.
(( join( ", ", list) ))
Une entradas de listas o valores directos a un único valor de cadena utilizando una cadena separadora determinada. Los argumentos a unir pueden ser expresiones dinámicas que se evalúan en listas, cuyos valores nuevamente son cadenas o números enteros, o valores de cadenas o números enteros.
p.ej:
alice : alice
list :
- foo
- bar
join : (( join(", ", "bob", list, alice, 10) ))
produce el valor de cadena bob, foo, bar, alice, 10
para join
.
(( split( ",", string) ))
Divida una cadena para un separador dedicado. El resultado es una lista. En lugar de una cadena separadora, se podría proporcionar un valor entero, que divide la cadena proporcionada en una lista de cadenas de longitud limitada. La longitud se cuenta en runas, no en bytes.
p.ej:
list : (( split("," "alice, bob") ))
limited : (( split(4, "1234567890") ))
rendimientos:
list :
- alice
- ' bob '
limited :
- " 1234 "
- " 5678 "
- " 90 "
Se podría especificar un tercer argumento opcional. Limita el número de entradas de la lista devueltas. El valor -1 conduce a una longitud de lista ilimitada.
Si se debe utilizar una expresión regular como cadena separadora, se puede utilizar la función split_match
.
(( trim(string) ))
Recorta una cadena o todos los elementos de una lista de cadenas. Hay un segundo argumento de cadena opcional. Se puede utilizar para especificar un conjunto de caracteres que se cortarán. El conjunto de corte predeterminado consta de un espacio y un carácter de tabulación.
p.ej:
list : (( trim(split("," "alice, bob")) ))
rendimientos:
list :
- alice
- bob
(( element(list, index) ))
Devuelve un elemento de lista dedicado dado por su índice.
p.ej:
list : (( trim(split("," "alice, bob")) ))
elem : (( element(list,1) ))
rendimientos:
list :
- alice
- bob
elem : bob
(( element(map, key) ))
Devuelve un campo de mapa dedicado dado por su clave.
map :
alice : 24
bob : 25
elem : (( element(map,"bob") ))
rendimientos:
map :
alice : 24
bob : 25
elem : 25
Esta función también puede manejar claves que contienen puntos (.).
(( compact(list) ))
Filtrar una lista omitiendo entradas vacías.
p.ej:
list : (( compact(trim(split("," "alice, , bob"))) ))
rendimientos:
list :
- alice
- bob
(( uniq(list) ))
Uniq proporciona una lista sin duplicados.
p.ej:
list :
- a
- b
- a
- c
- a
- b
- 0
- " 0 "
uniq : (( uniq(list) ))
rendimientos para el campo uniq
:
uniq :
- a
- b
- c
- 0
(( contains(list, "foobar") ))
Comprueba si una lista contiene un valor dedicado. Los valores también pueden ser listas o mapas.
p.ej:
list :
- foo
- bar
- foobar
contains : (( contains(list, "foobar") ))
rendimientos:
list :
- foo
- bar
- foobar
contains : true
La función también contains
trabajos en cadenas para buscar subcadenas o mapas para buscar una clave. En esos casos el elemento debe ser una cadena.
p.ej:
contains : (( contains("foobar", "bar") ))
produce true
.
(( basename(path) ))
La función basename
devuelve el nombre del último elemento de una ruta. El argumento puede ser un nombre de ruta normal o una URL.
p.ej:
pathbase : (( basename("alice/bob") ))
urlbase : (( basename("http://foobar/alice/bob?any=parameter") ))
rendimientos:
pathbase : bob
urlbase : bob
(( dirname(path) ))
La función dirname
devuelve el directorio principal de una ruta. El argumento puede ser un nombre de ruta normal o una URL.
p.ej:
pathbase : (( dirname("alice/bob") ))
urlbase : (( dirname("http://foobar/alice/bob?any=parameter") ))
rendimientos:
pathbase : alice
urlbase : /alice
(( parseurl("http://github.com") ))
Esta función analiza una URL y genera un mapa con todos los elementos de una URL. Los campos port
, userinfo
y password
son opcionales.
p.ej:
url : (( parseurl("https://user:[email protected]:443/mandelsoft/spiff?branch=master&tag=v1#anchor") ))
rendimientos:
url :
scheme : https
host : github.com
port : 443
path : /mandelsoft/spiff
fragment : anchor
query : branch=master&tag=v1
values :
branch : [ master ]
tag : [ v1 ]
userinfo :
username : user
password : pass
(( index(list, "foobar") ))
Comprueba si una lista contiene un valor dedicado y devuelve el índice de la primera coincidencia. Los valores también pueden ser listas o mapas. Si no se pudo encontrar ninguna entrada -1
se devuelve.
p.ej:
list :
- foo
- bar
- foobar
index : (( index(list, "foobar") ))
rendimientos:
list :
- foo
- bar
- foobar
index : 2
El index
de funciones también funciona en cadenas para buscar subcontras.
p.ej:
index : (( index("foobar", "bar") ))
rendimientos 3
.
(( lastindex(list, "foobar") ))
La función lastindex
funciona como index
, pero se devuelve el índice de la última ocurrencia.
El sort
de función se puede usar para clasificar las listas de Integer o String. La operación de clasificación es estable.
p.ej:
list :
- alice
- foobar
- bob
sorted : (( sort(list) ))
rendimientos para sorted
- alice
- bob
- foobar
Si se deben ordenar otros tipos, especialmente tipos complejos como listas o mapas, o se requiere una regla de comparación diferente, se puede especificar una función de comparación como un segundo argumento opcional. La función de comparación debe ser una expresión lambda que tome dos argumentos. El tipo de resultado debe ser integer
o bool
que indique si A es menos de b . Si se devuelve un entero, debería ser
p.ej:
list :
- alice
- foobar
- bob
sorted : (( sort(list, |a,b|->length(a) < length(b)) ))
rendimientos para sorted
- bob
- alice
- foobar
(( replace(string, "foo", "bar") ))
Reemplace todas las ocurrencias de una subcadena en una cadena por una cadena de reemplazo. Con un argumento entero opcional, el número de sustituciones puede ser limitado (-1 media ilimitada).
p.ej:
string : (( replace("foobar", "o", "u") ))
rendimiento fuubar
.
Si se debe usar una expresión regular como cadena de búsqueda, se puede usar la función replace_match
. Aquí la cadena de búsqueda se evalúa como expresión regular. Puede contain sub expresiones. Estas coincidencias se pueden usar en la cadena de reemplazo
p.ej:
string : (( replace_match("foobar", "(o*)b", "b${1}") ))
rendimiento fbooar
.
El argumento de reemplazo también podría ser una función Lambda. En este caso, para cada coincidencia se llama a la función para determinar el valor de reemplazo. El argumento de entrada única es una lista de coincidencias reales de subconselección.
p.ej:
string : (( replace_match("foobar-barfoo", "(o*)b", |m|->upper(m.[1]) "b" ) ))
rendimiento de fOObar-barfoo
.
(( substr(string, 1, 2) ))
Extraiga una cadena de ondas de una cadena, comenzando desde un índice de inicio dado hasta un índice final opcional (exclusivo). Si no se da un índice final, se extrae el subtruvt hasta el final de la cadena. Ambos índices pueden ser negativos. En este caso se toman desde el final de la cadena.
p.ej:
string : " foobar "
end1 : (( substr(string,-2) ))
end2 : (( substr(string,3) ))
range : (( substr(string,1,-1) ))
evaluar
string : foobar
end1 : ar
end2 : bar
range : ooba
(( match("(f.*)(b.*)", "xxxfoobar") ))
Devuelve la coincidencia de una expresión regular para un valor de cadena dado. La coincidencia es una lista de los valores coincidentes para las subconsiones contenidas en la expresión regular. El índice 0 se refiere a la coincidencia de la expresión regular completa. Si el valor de la cadena no coincide con una lista vacía.
p.ej:
matches : (( match("(f.*)*(b.*)", "xxxfoobar") ))
rendimientos:
matches :
- foobar
- foo
- bar
Se puede dar un tercer argumento de tipo entero para solicitar una coincidencia múltiple de un máximo de n repeticiones. Si el valor es negativo, se informan todas las repetiones. El resultado es una lista de todas las coincidencias, cada una en el formato descrito anteriormente.
(( keys(map) ))
Determine la lista ordenada de claves utilizadas en un mapa.
p.ej:
map :
alice : 25
bob : 25
keys : (( keys(map) ))
rendimientos:
map :
alice : 25
bob : 25
keys :
- alice
- bob
(( length(list) ))
Determine la longitud de una lista, un mapa o un valor de cadena.
p.ej:
list :
- alice
- bob
length : (( length(list) ))
rendimientos:
list :
- alice
- bob
length : 2
(( base64(string) ))
La función base64
genera una codificación base64 de una cadena dada. base64_decode
decodifica una cadena codificada base64.
p.ej:
base64 : (( base64("test") ))
test : (( base64_decode(base64)))
evaluar
base54 : dGVzdA==
test : test
Se puede usar un segundo argumento opcional para especificar la longitud de línea máxima. En este caso, el resultado será una cadena de múltiples líneas.
(( hash(string) ))
La función hash
genera varios tipos de hash para la cadena dada. Por defecto, se genera hash sha256
. Un segundo argumento opcional especifica el tipo hash. Los tipos posibles son md4
, md5
, sha1
, sha224
, sha256
, sha384
, sha2512
, sha512/224
o sha512/256
.
Los hashes md5
todavía pueden ser generados por el Deprecisado Finctio md5(string)
.
p.ej:
data : alice
hash :
deprecated : (( md5(data) ))
md4 : (( hash(data,"md4") ))
md5 : (( hash(data,"md5") ))
sha1 : (( hash(data,"sha1") ))
sha224 : (( hash(data,"sha224") ))
sha256 : (( hash(data,"sha256") ))
sha384 : (( hash(data,"sha384") ))
sha512 : (( hash(data,"sha512") ))
sha512_224 : (( hash(data,"sha512/224") ))
sha512_256 : (( hash(data,"sha512/256") ))
evaluar
data : alice
hash :
deprecated : 6384e2b2184bcbf58eccf10ca7a6563c
md4 : 616c69636531d6cfe0d16ae931b73c59d7e0c089c0
md5 : 6384e2b2184bcbf58eccf10ca7a6563c
sha1 : 522b276a356bdf39013dfabea2cd43e141ecc9e8
sha224 : 38b7e5d5651aaf85694a7a7c6d5db1275af86a6df93a36b8a4a2e771
sha256 : 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90
sha384 : 96a5353e625adc003a01bdcd9b21b21189bdd9806851829f45b81d3dfc6721ee21f6e0e98c4dd63bc559f66c7a74233a
sha512 : 408b27d3097eea5a46bf2ab6433a7234a33d5e49957b13ec7acc2ca08e1a13c75272c90c8d3385d47ede5420a7a9623aad817d9f8a70bd100a0acea7400daa59
sha512_224 : c3b8cfaa37ae15922adf3d21606e3a9836ba2a9d7838b040b7c96fd7
sha512_256 : ad0a339b08dc090fe3b16eae376f7e162836e8728da9c45466842e19508d7627
(( bcrypt("password", 10) ))
La función bcrypt
genera un hash de contraseña BCRYPT para la cadena dada utilizando el factor de costo especificado (predeterminado a 10, si falta).
p.ej:
hash : (( bcrypt("password", 10) ))
evaluar
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
(( bcrypt_check("password", hash) ))
La función bcrypt_check
valida una contraseña contra un hash BCRYPT dado.
p.ej:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
evaluar
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : true
(( md5crypt("password") ))
La función md5crypt
genera un hash de contraseña encriptada Apache MD5 para la cadena dada.
p.ej:
hash : (( md5crypt("password") ))
evaluar
hash : $apr1$3Qc1aanY$16Sb5h7U1QrcqwZbDJIYZ0
(( md5crypt_check("password", hash) ))
La función md5crypt_check
valida una contraseña contra un hash encriptado Apache MD5 dado.
p.ej:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
evaluar
hash : $apr1$B77VuUUZ$NkNFhkvXHW8wERSRoi74O1
valid : true
(( decrypt("secret") ))
Esta función se puede usar para almacenar secretos cifrados en un archivo yaml spiff. El resultado procesado contendrá el valor descifrado. Todos los tipos de nodos se pueden encriptar y descifrarse, incluidos mapas y listas completas.
La contraseña para el descifrado se puede dar como segundo argumento, o (la forma preferida) puede especificarse por la variable de entorno SPIFF_ENCRYPTION_KEY
.
Un último argumento opcional puede seleccionar el método de cifrado. El único método admitido hasta ahora es 3DES
. Se pueden agregar otros métodos para versiones spiff dedicadas utilizando el registro del método de cifrado ofrecido por la biblioteca spiff.
Un valor se puede encriptar utilizando la función encrypt("secret")
.
p.ej:
password : this a very secret secret and may never be exposed to unauthorized people
encrypted : (( encrypt("spiff is a cool tool", password) ))
decrypted : (( decrypt(encrypted, password) ))
evaluado a algo como
decrypted : spiff is a cool tool
encrypted : d889f9e4cc7ae13effcbc8bb8cd0c38d1fb2197738444f753c48796d7946083e6639e5a1bf8f77648f2a1ddf37023c65ff57d52d0519d1d92cbcf87d3e263cba
password : this a very secret secret and may never be exposed to unauthorized people
(( rand("[:alnum:]", 10) ))
La función rand
genera valores aleatorios. El primer argumento decide qué tipo de valores se solicitan. Sin ningún argumento genera un número aleatorio positivo en el rango int64
.
tipo de argumento | resultado |
---|---|
entero | Valor entero en el rango [0, n ) para N y ( n , 0] para N negativo n |
booleano | valor booleano |
cadena | Una cadena de runa, donde la runa se encuentra en el rango de caracteres dado, se puede usar cualquier combinación de clases de caracteres o rangos de caracteres utilizables para regexp. Si se especifica un argumento de longitud adicional, la cadena resultante tendrá la longitud dada. |
p.ej:
int : (( rand() ))
int10 : (( rand(10) ))
neg10 : (( rand(-10) ))
bool : (( rand(true) ))
string : (( rand("[:alpha:][:digit:]-", 10) ))
upper : (( rand("A-Z", 10) ))
punct : (( rand("[:punct:]", 10) ))
alnum : (( rand("[:alnum:]", 10) ))
evaluar
int : 8037669378456096839
int10 : 7
neg10 : -5
bool : true
string : ghhjAYPMlD
upper : LBZQFRSURL
alnum : 0p6KS7EhAj
punct : ' &{;,^])"(# '
(( type(foobar) ))
El type
de función produce una cadena que denota el tipo de expresión dada.
p.ej:
template :
<< : (( &template ))
types :
- int : (( type(1) ))
- float : (( type(1.0) ))
- bool : (( type(true) ))
- string : (( type("foobar") ))
- list : (( type([]) ))
- map : (( type({}) ))
- lambda : (( type(|x|->x) ))
- template : (( type(.template) ))
- nil : (( type(~) ))
- undef : (( type(~~) ))
evalúa los tipos para
types :
- int : int
- float : float
- bool : bool
- string : string
- list : list
- map : map
- lambda : lambda
- template : template
(( defined(foobar) ))
La función defined
verifica si una expresión puede evaluarse con éxito. Produce el valor booleano true
si la expresión puede ser evaluada y false
de lo contrario.
p.ej:
zero : 0
div_ok : (( defined(1 / zero ) ))
zero_def : (( defined( zero ) ))
null_def : (( defined( null ) ))
evaluar
zero : 0
div_ok : false
zero_def : true
null_def : false
Esta función se puede usar en combinación del operador condicional para evaluar las expresiones dependiendo de la resolución de otra expresión.
(( valid(foobar) ))
La función valid
verifica si una expresión puede evaluarse con éxito y evaluarse a un valor definido, no es igual a nil
. Produce el valor booleano true
si la expresión puede ser evaluada y false
de lo contrario.
p.ej:
zero : 0
empty :
map : {}
list : []
div_ok : (( valid(1 / zero ) ))
zero_def : (( valid( zero ) ))
null_def : (( valid( ~ ) ))
empty_def : (( valid( empty ) ))
map_def : (( valid( map ) ))
list_def : (( valid( list ) ))
evaluar
zero : 0
empty : null
map : {}
list : []
div_ok : false
zero_def : true
null_def : false
empty_def : false
map_def : true
list_def : true
(( require(foobar) ))
La función require
produce un error si el argumento dado está indefinido o nil
, de lo contrario produce el valor dado.
p.ej:
foo : ~
bob : (( foo || "default" ))
alice : (( require(foo) || "default" ))
evaluar
foo : ~
bob : ~
alice : default
(( stub(foo.bar) ))
El stub
de función produce el valor de un campo dedicado que se encuentra en el primer stub ascendente que lo define.
p.ej:
Template.yml
value : (( stub(foo.bar) ))
Fusionado con Stub
stub.yml
foo :
bar : foobar
evaluar
value : foobar
El argumento transmitido a esta función debe ser un literal de referencia o una expresión que evalúe a una cadena que denota una referencia o una lista de cadenas que denota la lista de elementos de ruta para la referencia. Si no se da ningún argumento o un ( ~~
) indefinido, se usa la ruta de campo real.
Tenga en cuenta que una referencia única determinada no se evaluará como expresión, si su valor debe usarse, debe transformarse en una expresión, por ejemplo, denotando (ref)
o [] ref
para una expresión de la lista.
Alternativamente, la operación merge
podría usarse, por ejemplo, merge foo.bar
. La diferencia es que stub
no se fusiona, por lo tanto, el campo aún se fusionará (con la ruta original en el documento).
(( tagdef("tag", value) ))
La función tagdef
se puede usar para definir etiquetas dinámicas (ver etiquetas). A diferencia del marcador de etiqueta, esta función permite especificar el nombre de la etiqueta y su valor previsto por una expresión. Por lo tanto, se puede usar para componer elementos como map
o sum
para crear una etiqueta dinámica con valores calculados.
Se puede usar un tercer argumento opcional para especificar el alcance previsto ( local
o global
). Por defecto, se crea una etiqueta local. Las etiquetas locales son visibles solo en el nivel de procesamiento real (plantilla o sub), mientras que las etiquetas globales, una vez definidas, pueden usarse en todos los niveles de procesamiento adicionales (trozo o plantilla).
Alternativamente, el nombre de la etiqueta se puede prefijo con un inicio ( *
) para declarar una etiqueta global.
El valor de la etiqueta especificado se utilizará como resultado para la función.
p.ej:
Template.yml
value : (( tagdef("tag:alice", 25) ))
alice : (( tag:alice::. ))
evaluar
value : 25
alice : 25
(( eval(foo "." bar ) ))
Evalúe el resultado de la evaluación de una expresión de cadena nuevamente como expresión de Dynaml. Esto puede, por ejemplo, usarse para realizar indirecciones.
por ejemplo: la expresión en
alice :
bob : married
foo : alice
bar : bob
status : (( eval( foo "." bar ) ))
calcula la ruta a un campo, que luego se evalúa nuevamente para producir el valor de este campo compuesto:
alice :
bob : married
foo : alice
bar : bob
status : married
(( env("HOME" ) ))
Lea el valor de una variable de entorno cuyo nombre se da como expresión de Dynaml. Si la variable de entorno no se establece, la evaluación falla.
En un segundo sabor, la función env
acepta múltiples argumentos y/o argumentos de lista, que se unen a una sola lista. Cada entrada en esta lista se usa como nombre de una variable de entorno y el resultado de la función es un mapa de las variables dadas dadas como elemento YAML. Por la presente se omiten las variables de entorno inexistentes.
(( parse(yamlorjson) ))
Analice una cadena YAML o JSON y devuelva el contenido como valor YAML. Por lo tanto, se puede utilizar para una evaluación de dinaml adicional.
p.ej:
json : |
{ "alice": 25 }
result : (( parse( json ).alice ))
produce el valor 25
para el result
del campo.
El parse
la función admite un segundo argumento opcional, el modo de análisis . Aquí son posibles los mismos modos que para la función de lectura. El modo de análisis predeterminado es import
, el contenido se analiza y no hay más evaluación durante este paso.
(( asjson(expr) ))
Esta función transforma un valor YAML dado por su argumento a una cadena JSON . La función correspondiente asyaml
produce el valor YAML como cadena de documento YAML .
p.ej:
data :
alice : 25
mapped :
json : (( asjson(.data) ))
yaml : (( asyaml(.data) ))
resuelve
data :
alice : 25
mapped :
json : ' {"alice":25} '
yaml : |+
alice: 25
(( catch(expr) ))
Esta función ejecuta una expresión y produce algún mapa de información de evaluación. Siempre tiene éxito, incluso si la expresión falla. El mapa incluye los siguientes campos:
nombre | tipo | significado |
---|---|---|
valid | booleano | La expresión es válida |
error | cadena | El texto del mensaje de error de la evaluación |
value | cualquier | el valor de la expresión, si la evaluación fue exitosa |
p.ej:
data :
fail : (( catch(1 / 0) ))
valid : (( catch( 5 * 5) ))
resuelve
data :
fail :
error : division by zero
valid : false
valid :
error : " "
valid : true
value : 25
(( static_ips(0, 1, 3) ))
Genere una lista de IP estáticas para un trabajo.
p.ej:
jobs :
- name : myjob
instances : 2
networks :
- name : mynetwork
static_ips : (( static_ips(0, 3, 4) ))
Esto creará 3 IP a partir de la subred de mynetwork
y devolverá dos entradas, ya que solo hay dos instancias. Las dos entradas serán las compensaciones 0 y terceras de los rangos de IP estáticos definidos por la red.
Por ejemplo, dado el archivo bye.yml :
networks : (( merge ))
jobs :
- name : myjob
instances : 3
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
y archivo Hi.yml :
networks :
- name : cf1
subnets :
- cloud_properties :
security_groups :
- cf-0-vpc-c461c7a1
subnet : subnet-e845bab1
dns :
- 10.60.3.2
gateway : 10.60.3.1
name : default_unused
range : 10.60.3.0/24
reserved :
- 10.60.3.2 - 10.60.3.9
static :
- 10.60.3.10 - 10.60.3.70
type : manual
spiff merge bye.yml hi.yml
regresa
jobs :
- instances : 3
name : myjob
networks :
- name : cf1
static_ips :
- 10.60.3.10
- 10.60.3.13
- 10.60.3.70
networks :
- name : cf1
subnets :
- cloud_properties :
security_groups :
- cf-0-vpc-c461c7a1
subnet : subnet-e845bab1
dns :
- 10.60.3.2
gateway : 10.60.3.1
name : default_unused
range : 10.60.3.0/24
reserved :
- 10.60.3.2 - 10.60.3.9
static :
- 10.60.3.10 - 10.60.3.70
type : manual
.
Si Bye.yml fue en su lugar
networks : (( merge ))
jobs :
- name : myjob
instances : 2
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
spiff merge bye.yml hi.yml
En cambio, regresa
jobs :
- instances : 2
name : myjob
networks :
- name : cf1
static_ips :
- 10.60.3.10
- 10.60.3.13
networks :
- name : cf1
subnets :
- cloud_properties :
security_groups :
- cf-0-vpc-c461c7a1
subnet : subnet-e845bab1
dns :
- 10.60.3.2
gateway : 10.60.3.1
name : default_unused
range : 10.60.3.0/24
reserved :
- 10.60.3.2 - 10.60.3.9
static :
- 10.60.3.10 - 10.60.3.70
type : manual
static_ips
también acepta argumentos de la lista, siempre que todos los elementos contenidos transitivamente sean listas nuevamente o valores enteros. Esto permite abreviar la lista de IP de la siguiente manera:
static_ips: (( static_ips([1..5]) ))
(( ipset(ranges, 3, 3,4,5,6) ))
Mientras que la función estática por razones históricas se basa en la estructura de un manifiesto Bosh y funciona solo en ubicaciones dedicadas en el manifiesto, la función IPSet ofrece un cálculo similar puramente basado en sus argumentos. Por lo tanto, los rangos de IP disponibles y los números requeridos de IP se pasan como argumentos.
El primer argumento (rangos) puede ser un rango único como una cadena simple o una lista de cadenas. Cada cadena podría ser
El segundo argumento especifica el número solicitado de direcciones IP en el conjunto de resultados.
Los argumentos adicionales especifican los índices de los IP para elegir (a partir de 0) en los rangos dados. Aquí se pueden usar listas de índices nuevamente.
p.ej:
ranges :
- 10.0.0.0 - 10.0.0.255
- 10.0.2.0/24
ipset : (( ipset(ranges,3,[256..260]) ))
Resuelve IPSet a [ 10.0.2.0, 10.0.2.1, 10.0.2.2 ]
.
Si no se especifican índices IP (solo dos argumentos), los IP se eligen a partir del comienzo del primer rango hasta el final del último rango dado, sin indirección.
(( list_to_map(list, "key") ))
Una lista de entradas de mapa con nombre de nombre/campos de teclas explícitos se asignará a un mapa con las teclas dedicadas. Por defecto, se usa el name
del campo clave, que puede cambiar por el segundo argumento opcional. También se tendrá en cuenta un campo clave denotado explícitamente en la lista.
p.ej:
list :
- key:foo : alice
age : 24
- foo : bob
age : 30
map : (( list_to_map(list) ))
se asignará a
list :
- foo : alice
age : 24
- foo : bob
age : 30
map :
alice :
age : 24
bob :
age : 30
En combinación con plantillas y expresiones lambda, esto puede usarse para generar mapas con valores clave nombrados arbitrariamente, aunque las expresiones de dinaml no están permitidas para valores clave.
(( makemap(fieldlist) ))
En este sabor, makemap
crea un mapa con entradas descritas por la lista de campo dada. Se espera que la lista contenga mapas con la key
de entradas y value
, que describe entradas de mapa dedicadas.
p.ej:
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map : (( makemap(list) ))
rendimientos
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map :
" 5 " : 25
alice : 24
bob : 25
Si el valor clave es un booleano o un entero, se asignará a una cadena.
(( makemap(key, value) ))
En este sabor, makemap
crea un mapa con entradas descritas por los pares de argumentos dados. Los argumentos pueden ser una secuencia de pares clave/valores (dados por argumentos separados).
p.ej:
map : (( makemap("peter", 23, "paul", 22) ))
rendimientos
map :
paul : 22
peter : 23
En contraste con el sabor makemap
anterior, este también podría ser manejado por literales de mapas.
(( merge(map1, map2) ))
Además de la palabra clave merge
también hay una función llamada merge
(siempre debe ser seguida por un soporte de apertura). Se puede utilizar para fusionar mapas severales tomados del documento real análogo al proceso de fusión de stub. Si los mapas se especifican por expresiones de referencia, no pueden contener ninguna expresión de dinaml , porque siempre se evalúan en el contexto del documento real antes de evaluar los argumentos.
p.ej:
map1 :
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
resuelve result
a
result :
alice : 26
bob : 24 # <---- expression evaluated before mergeing
Alternativamente, se pueden aprobar plantillas de mapa (¡sin operador de evaluación!). En este caso, las expresiones de dinaml de la plantilla se evalúan al fusionar los documentos dados en cuanto a las llamadas regulares de fusión de lámpara .
p.ej:
map1 :
<< : (( &template ))
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
resuelve result
a
result :
alice : 26
bob : 26
Un mapa también podría ser dado por una expresión de mapa. Aquí es posible especificar expresiones de Dynaml utilizando la sintaxis habitual:
p.ej:
map1 :
alice : 24
bob : 25
map2 :
alice : 26
peter : 8
result : (( merge(map1, map2, { "bob"="(( carl ))", "carl"=100 }) ))
resuelve result
a
result :
alice : 26
bob : 100
En lugar de múltiples argumentos, se puede dar un argumento de una sola lista. La lista debe contener los mapas que se fusionarán.
Las fusiones anidadas tienen acceso a todas las fijaciones externas. Las referencias relativas se buscan primero en el documento real. Si no se encuentran allí, todos los enlaces externos se usan para buscar la referencia, de las uniones internas a las externas. Además, el contexto ( __ctx
) ofrece un campo OUTER
, que es una lista de todos los documentos externos de las fusiones anidadas, que pueden usarse para buscar referencias absolutas.
p.ej:
data :
alice :
age : 24
template :
<< : (( &template ))
bob : 25
outer1 : (( __ctx.OUTER.[0].data )) # absolute access to outer context
outer2 : (( data.alice.age )) # relative access to outer binding
sum : (( .bob + .outer2 ))
merged : (( merge(template) ))
Resoluciones merged
para
merged :
bob : 25
outer1 :
alice :
age : 24
outer2 : 24
sum : 49
(( intersect(list1, list2) ))
La función intersect
intersecta múltiples listas. Una lista puede contener entradas de cualquier tipo.
p.ej:
list1 :
- - a
- - b
- a
- b
- { a: b }
- { b: c }
- 0
- 1
- " 0 "
- " 1 "
list2 :
- - a
- - c
- a
- c
- { a: b }
- { b: b }
- 0
- 2
- " 0 "
- " 2 "
intersect : (( intersect(list1, list2) ))
resuelve intersect
a
intersect :
- - a
- a
- { a: b }
- 0
- " 0 "
(( reverse(list) ))
La función reverse
invierte el orden de una lista. La lista puede contener entradas de cualquier tipo.
p.ej:
list :
- - a
- b
- { a: b }
- { b: c }
- 0
- 1
reverse : (( reverse(list) ))
resuelve reverse
a
reverse :
- 1
- 0
- { b: c }
- { a: b }
- b
- - a
(( validate(value,"dnsdomain") ))
La función validate
valida una expresión utilizando un conjunto de validadores. El primer argumento es el valor para validar y todos los demás argumentos son validadores que deben tener éxito para aceptar el valor. Si al menos un validador falla, se genera un mensaje de error apropiado que explica el motivo de falla.
Un validador se denota mediante una cadena o una lista que contiene el tipo de validador como cadena y sus argumentos. ¡Se puede negar un validador con una precedente !
en su nombre.
Los siguientes validadores están disponibles:
Tipo | Argumentos | Significado |
---|---|---|
empty | ninguno | Lista, mapa o cadena vacía |
dnsdomain | ninguno | nombre de dominio DNS |
wildcarddnsdomain | ninguno | Nombre de dominio DNS comodín |
dnslabel | ninguno | etiqueta DNS |
dnsname | ninguno | dominio DNS o dominio comodín |
ip | ninguno | dirección IP |
cidr | ninguno | cidra |
publickey | ninguno | Clave pública en formato PEM |
privatekey | ninguno | Clave privada en formato PEM |
certificate | ninguno | Certificado en formato PEM |
ca | ninguno | Certificado para CA |
semver | Lista opcional de restricciones | Validar la versión de Semver contra las limitaciones |
type | Lista de teclas de tipo aceptado | Al menos una tecla de tipo debe coincidir |
valueset | Lista del argumento con valores | valores posibles |
value o = | valor | Verifique el valor dedicado |
gt o > | valor | mayor que (número/cadena) |
lt o < | valor | Menos de (número/cadena) |
ge o >= | valor | mayor o igual a (número/cadena) |
le o <= | valor | Menos o igual a (número/cadena) |
match o ~= | expresión regular | Valor de cadena que coincide con la expresión regular |
list | Lista opcional de validadores de entrada | es la lista y las entradas coinciden con validadores |
map | [[<<palidador de clave>,] <Validador de entrada>] | ¿El mapa y las teclas y las entradas coinciden con los validadores? |
mapfield | <Nombre de campo> [, <validador>] | Entrada requerida en el mapa |
optionalfield | <Nombre de campo> [, <validador>] | Entrada opcional en el mapa |
and | Lista de validadores | Todos los validadores deben tener éxito |
or | Lista de validadores | Al menos un validador debe tener éxito |
not o ! | validador | niegue los argumentos del validador (s) |
Si la validación tiene éxito, se devuelve el valor.
p.ej:
dnstarget : (( validate("192.168.42.42", [ "or", "ip", "dnsdomain" ]) ))
evaluar
dnstarget : 192.168.42.42
Si la validación falla, se genera un error que explica el motivo de la falla.
p.ej:
dnstarget : (( validate("alice+bob", [ "or", "ip", "dnsdomain" ]) ))
produce el siguiente error:
*condition 1 failed: (is no ip address: alice+bob and is no dns domain: [a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')])
Un validador también podría ser una expresión lambda que tome al menos un argumento y devuelva un valor booleano. De esta manera, es posible proporcionar validadores propios como parte del documento YAML.
p.ej:
val : (( validate( 0, |x|-> x > 1 ) ))
Si se declara más de un parámetro, los argumentos adicionales deben especificarse como argumentos de validador. El primer argumento es siempre el valor para verificar.
p.ej:
val : (( validate( 0, [|x,m|-> x > m, 5] ) ))
La función Lambda también puede devolver una lista con 1, 2 o 3 elementos. Esto se puede usar para proporcionar mensajes apropiados.
Índice | Significado |
---|---|
0 | El primer índice siempre es el resultado de la coincidencia, debe ser evaluable como booleano |
1 | Si se dan dos elementos, el segundo índice es el mensaje que describe el resultado real |
2 | Aquí el índice 1 disminuye el mensaje de éxito y 2 el mensaje de falla |
p.ej:
val : (( validate( 6, [|x,m|-> [x > m, "is larger than " m, "is less than or equal to " m], 5] ) ))
Solo por mencionar, la especificación de validador puede administrarse en línea como se muestra en los ejemplos anteriores, pero como expresiones de referencia, también. Los not
, and
or
los validadores aceptan especificaciones de validador profundamente anidadas.
p.ej:
dnsrecords :
domain : 1.2.3.4
validator :
- map
- - or # key validator
- dnsdomain
- wildcarddnsdomain
- ip # entry validator
val : (( validate( map, validator) ))
(( check(value,"dnsdomain") ))
La check
de la función se puede usar para que coincida con una estructura YAML con un verificador de valor basado en YAML. Por la presente, se puede usar la misma descripción del cheque ya descrita para Validate. El resultado de la llamada es un valor booleano que indica el resultado de la coincidencia. No falla si el cheque falla.
(( error("message") ))
El error
de la función se puede usar para causar fallas de evaluación explícita con un mensaje dedicado.
Esto se puede utilizar, por ejemplo, para reducir un error de procesamiento complejo a un mensaje significativo al agregar la función de error como predeterminado para la expresión de comples potencialmente fallando.
p.ej:
value : (( <some complex potentially failing expression> || error("this was an error my friend") ))
Otro escenario podría ser omitir un mensaje descriptivo para los campos requeridos faltantes utilizando una expresión de error como valor (predeterminado) para un campo destinado a definirse en un trozo aguas arriba.
Dynaml admite varias funciones matemáticas:
enteros que regresan: ceil
, floor
, round
y roundtoeven
devastando carrozas o enteros: abs
Floats de regreso: sin
, cos
, sinh
, cosh
, asin
, acos
, asinh
, acosh
, sqrt
, exp
, log
, log10
,
Dynaml admite varias conversiones de tipo entre los valores integer
, float
, bool
y string
por funciones apropiadas.
p.ej:
value : (( integer("5") ))
Convierte una cadena a un valor entero.
La conversión de un entero a una cadena acepta un argumento entero adicional opcional para especificar la base para la conversión, por ejemplo, string(55,2)
dará como resultado "110111"
. La base predeterminada es 10. La base debe estar entre 2 y 36.
Spiff admite el acceso al contenido fuera de la plantilla y los subcontos. Es posible leer archivos, ejecutar comandos y tuberías. Todas esas funciones existen en dos sabores.
sync
, que tiene la intención de sincronizar el procesamiento de plantillas con un estado dedicado (proporcionado por contenido externo). Aquí las operaciones de almacenamiento en caché no serían útiles, por lo tanto, hay un segundo sabor sin dejar de acuerdo. Cada función está disponible con el sufijo _uncached
(por ejemplo, read_uncached()
) (( read("file.yml") ))
Lea un archivo y devuelva su contenido. Hay soporte para tres tipos de contenido: archivos yaml
, archivos text
y archivos binary
. La lectura en modo binario dará como resultado una cadena de múltiples líneas codificada base64.
Si el sufijo de archivo es .yml
, .yaml
o .json
, de forma predeterminada se usa el tipo YAML. Si el archivo debe leerse como text
, este tipo debe especificarse explícitamente. En todos los demás casos, el valor predeterminado es text
, por lo tanto, leer un archivo binario (por ejemplo un archivo) requiere urgentemente especificar el modo binary
.
Se puede usar un segundo parámetro opcional para especificar explícitamente el tipo de retorno deseado: yaml
o text
. Para los documentos YAML se admiten algunos tipos adicionales: multiyaml
, template
, templates
, import
e importmulti
.
Se analizará un documento YAML y se devuelve el árbol. Se puede acceder a los elementos del árbol mediante expresiones de dinaml regulares.
Además, el archivo YAML puede volver a contener expresiones Dynaml. Todas las expresiones de dinaml incluidas se evaluarán en el contexto de la expresión de lectura. Esto significa que el mismo archivo incluido en diferentes lugares en un documento YAML puede dar lugar a diferentes subtrees, dependiendo de las expresiones Dynaml utilizadas.
Si es posible leer un YAML de documentos múltiples, también. Si se proporciona el tipo multiyaml
, se devuelve un nodo de lista con los nodos raíz del documento YAML.
El documento YAML o JSON también puede leer como plantilla especificando la template
de tipo. Aquí el resultado será un valor de plantilla, que puede usarse como plantillas en línea regulares. Si se especifican templates
, un documento múltiple se asigna a una lista de plantillas.
Si el tipo de lectura está configurado para import
, el contenido del archivo se lee como documento YAML y el nodo raíz se usa para sustituir la expresión. Las posibles expresiones de dinaml contenidas en el documento no se evaluarán con la unión real de la expresión junto con la llamada de lectura, pero como hubiera sido parte del archivo original. Por lo tanto, este modo solo se puede usar, si no hay más procesamiento del resultado de lectura o los valores entregados no están procesados.
Esto se puede utilizar junto con una referencia encadenada (para el examen (( read(...).selection ))
) para delectar un fragmento dedicado del documento importado. Luego, la evaluación se realizará solo para la porción seleccionada. Las expresiones y referencias en las otras partes no se evalúan y no pueden conducir a un error.
p.ej:
Template.yaml
ages :
alice : 25
data : (( read("import.yaml", "import").first ))
import.yaml
first :
age : (( ages.alice ))
second :
age : (( ages.bob ))
no fallará, porque la second
sección nunca se evalúa.
Este modo debe tomarse con precaución, ya que a menudo conduce a resultados inesperados.
El tipo de lectura importmulti
se puede utilizar para importar archivos YAML de documentos múltiples como una lista de nodos.
Un documento de texto se devolverá como una sola cadena.
También es posible leer documentos binarios. El contenido no se puede usar como una cadena (o documento YAML), directamente. Por lo tanto, se debe especificar el modo binary
de lectura. El contenido se devuelve como un valor de cadena de múltiples líneas codificada Base64.
(( exec("command", arg1, arg2) ))
Ejecutar un comando. Los argumentos pueden ser cualquier expresión dinaml, incluidas las expresiones de referencia evaluadas en listas o mapas. Las listas o mapas se pasan como argumentos individuales que contienen un documento YAML con el fragmento dado.
El resultado se determina analizando la salida estándar del comando. Puede ser un documento YAML o una sola cadena múltiple o valor entero. Un documento YAML debe comenzar con el prefijo del documento ---
. Si el comando falla, la expresión se maneja como indefinida.
p.ej
arg :
- a
- b
list : (( exec( "echo", arg ) ))
string : (( exec( "echo", arg.[0] ) ))
rendimientos
arg :
- a
- b
list :
- a
- b
string : a
Alternativamente, exec
se puede llamar con un argumento de una sola lista que describe completamente la línea de comando.
El mismo comando se ejecutará una vez, solo, incluso si se usa en múltiples expresiones.
(( pipe(data, "command", arg1, arg2) ))
Ejecutar un comando y alimentar su entrada estándar con datos dedicados. El argumento del comando debe ser una cadena. Los argumentos para el comando pueden ser cualquier expresión dinaml, incluidas las expresiones de referencia evaluadas en listas o mapas. Las listas o mapas se pasan como argumentos individuales que contienen un documento YAML con el fragmento dado.
El flujo de entrada se genera a partir de los datos dados. Si este es un tipo simple, su representación de cadena se usa. De lo contrario, se genera un documento YAML a partir de los datos de entrada. El resultado se determina analizando la salida estándar del comando. Puede ser un documento YAML o una sola cadena múltiple o valor entero. Un documento YAML debe comenzar con el prefijo del documento ---
. Si el comando falla, la expresión se maneja como indefinida.
p.ej
data :
- a
- b
list : (( pipe( data, "tr", "a", "z") ))
rendimientos
arg :
- a
- b
list :
- z
- b
Alternativamente, se puede llamar pipe
con datos y un argumento de lista que describe completamente la línea de comando.
El mismo comando se ejecutará una vez, solo, incluso si se usa en múltiples expresiones.
(( write("file.yml", data) ))
Escriba un archivo y devuelva su contenido. Si el resultado se puede analizar como documento YAML, se devuelve el documento. Se puede usar un tercer argumento opcional para pasar las opciones de escritura. Los argumentos de opción podrían ser un permiso de archivo entero que denota el archivo (el valor predeterminado es 0644
) o una cadena separada por comas con opciones. Las opciones compatibles son
binary
: los datos están decodificados Base64 antes de escribir0
está indicando un valor octal. (( tempfile("file.yml", data) ))
Escriba un archivo temporal AA y devuelva su nombre de ruta. Se puede usar un tercer argumento opcional para aprobar opciones de escritura. Básicamente se comporta como write
Atención : solo existe un archivo temporal durante el procesamiento de fusión. Se eliminará después.
Se puede usar, por ejemplo, para proporcionar un argumento de archivo temporal para la función exec
.
(( lookup_file("file.yml", list) ))
Buscar un archivo es una lista de directorios. El resultado es una lista de archivos existentes. Con lookup_dir
es posible buscar un directorio.
Si no se pueden encontrar archivos existentes, se devuelve la lista vacía.
Es posible pasar múltiples argumentos de lista o cadena para componer la ruta de búsqueda.
(( mkdir("dir", 0755) ))
Cree un directorio y todos sus directorios intermedios si aún no existen.
La parte del permiso es opcional (predeterminado 0755). La ruta del directorio podría ser dada por un valor similar a Atring o como una lista de componentes de ruta.
(( list_files(".") ))
Listar archivos en un directorio. El resultado es una lista de archivos existentes. Con list_dirs
es posible enumerar los directorios.
(( archive(files, "tar") ))
Cree un archivo del tipo dado (predeterminado es tar
) que contiene los archivos enumerados. El resultado es el archivo codificado Base64.
Los tipos de archivo compatibles son tar
y targz
.
files
pueden ser una lista o mapa de las entradas de archivo. En el caso de un mapa, la tecla MAP se usa como predeterminada para la ruta del archivo. Una entrada de archivo es un mapa con los siguientes campos:
campo | tipo | significado |
---|---|---|
path | cadena | Opcional para mapas, la ruta del archivo en el archivo, predeterminado por la tecla MAP |
mode | int o int String | modo de archivo o opciones de escritura. Básicamente se comporta como el argumento de opción para write . |
data | cualquier | Contenido de archivo, Yaml será organizado como documento YAML. Si mode indica el modo binario, un valor de cadena se decodificará base64. |
base64 | cadena | Base64 Datos binarios codificados |
p.ej:
yaml :
alice : 26
bob : 27
files :
" data/a/test.yaml " :
data : (( yaml ))
" data/b/README.md " :
data : |+
### Test Docu
**Note**: This is a test
archive : (( archive(files,"targz") ))
content : (( split("n", exec_uncached("tar", "-tvf", tempfile(archive,"binary"))) ))
rendimientos:
archive : |-
H4sIAAAAAAAA/+zVsQqDMBAG4Mx5igO3gHqJSQS3go7tUHyBqIEKitDEoW9f
dLRDh6KlJd/yb8ll+HOd8SY1qbfOJw8zDmQHiIhayjURcZuIQhOeSZlphVwL
glwsAXvM8mJ23twJ4qfnbB/3I+I4pmboW1uA0LSZmgJETr89VXCUtf9Neq1O
5blKxm6PO972X/FN/7nKVej/EaIogto6D+XUzpQydpm8ZayA+tY76B0YWHYD
DV9CEATBf3kGAAD//5NlAmIADAAA
content :
- -rw-r--r-- 0/0 22 2019-03-18 09:01 data/a/test.yaml
- -rw-r--r-- 0/0 41 2019-03-18 09:01 data/b/README.md
files :
data/a/test.yaml :
data :
alice : 26
bob : 27
data/b/README.md :
data : |+
### Test Docu
**Note**: This is a test
yaml :
alice : 26
bob : 27
Spiff admite el manejo de nombres de versiones semánticas. Admite toda la funcionalidad del paquete Masterminds Semver que acepta versiones con o sin una v
líder.
(( semver("v1.2-beta.1") ))
Compruebe si una cadena determinada es una versión semántica y devuelve su forma normalizada (sin una parte de lanzamiento v
-Veading y una parte de lanzamiento completa con el número de versión mayor, menor y de parche).
p.ej:
normalized : (( semver("v1.2-beta.1") ))
resuelve
normalized : 1.2.0-beta.1
(( semverrelease("v1.2.3-beta.1") ))
Devuelva la parte de lanzamiento de una versión semántica que omite los metadatos y la información previa al prevenimiento.
p.ej:
release : (( semverrelease("v1.2.3-beta.1") ))
resuelve
release : v1.2.3
Si se da un argumento de cadena adicional, esta función reemplaza la versión mediante la versión de la versión semántica dada que preserva metadatos y información previa a la carga.
p.ej:
new : (( semverrelease("1.2.3-beta.1", "1.2.1) ))
resuelve
new : 1.2.1-beta.1
(( semvermajor("1.2.3-beta.1") ))
Determine el número de versión principal de la versión semántica dada. El resultado es un entero.
p.ej:
major : (( semvermajor("1.2.3-beta.1") ))
resuelve
major : 1
La función semverincmajor
se puede usar para incrementar el número de versión principal y restablecer la versión menor, la versión del parche y los sufijos de lanzamiento.
p.ej:
new : (( semverincmajor("1.2.3-beta.1") ))
resuelve
new : 2.0.0
(( semverminor("1.2.3-beta.1") ))
Determine el número de versión menor de la versión semántica dada. El resultado es un entero.
p.ej:
minor : (( semverminor("1.2.3-beta.1") ))
resuelve
minor : 2
La función semverincminor
se puede usar para incrementar el número de versión menor y restablecer la versión de parche y los sufijos de lanzamiento.
p.ej:
new : (( semverincmajor("v1.2.3-beta.1") ))
resuelve
new : v1.3.0
(( semverpatch("1.2.3-beta.1") ))
Determine el número de versión de parche de la versión semántica dada. El resultado es un entero.
p.ej:
patch : (( semverpatch("1.2.3-beta.1") ))
resuelve
patch : 3
La función semverincpatch
se puede usar para incrementar el número de versión del parche o restablecer los sufijos de liberación. Si hay sufijos de rlease, se eliminan y la información de lanzamiento se mantiene sin cambios, de lo contrario el número de versión del parche aumenta.
p.ej:
final : (( semverincpatch("1.2.3-beta.1") ))
new : (( semverincpatch(final) ))
resuelve
final : 1.2.3
new : 1.2.4
(( semverprerelease("1.2.3-beta.1") ))
Determine la pre -sugerencia de la versión semántica dada. El resultado es una cadena.
p.ej:
prerelease : (( semverprerelease("1.2.3-beta.1") ))
resuelve
prerelease : beta.1
Si se establece un argumento de cadena adicional, esta función se establece, reemplaza o borra (si se establece en una cadena vacía) la prerelaseadora
p.ej:
new : (( semverprerelease("1.2.3-beta.1", "beta.2) ))
resuelve
new : 1.2.3-beta.2
(( semvermetadata("1.2.3+demo") ))
Determine los metadatos de la versión semántica dada. El resultado es una cadena.
p.ej:
metadata : (( semvermetadata("1.2.3+demo") ))
resuelve
metadata : demo
Si se establece un argumento de cadena adicional, esta función se establece, reemplaza o borra (si se establece en una cadena vacía) los metadatos.
p.ej:
new : (( semvermetadata("1.2.3-test", "demo) ))
resuelve
new : 1.2.3+demo
(( semvercmp("1.2.3", 1.2.3-beta.1") ))
Compare dos versiones semánticas. Un prevenimiento siempre es más pequeño que el lanzamiento final. El resultado es un entero con los siguientes valores:
resultado | significado |
---|---|
-1 | La primera versión es antes de la segunda versión |
0 | Ambas versiones son iguales |
1 | Primero Versuon es después del segundo |
p.ej:
compare : (( semvercmp("1.2.3", "1.2.3-beta.1") ))
resuelve
compare : 1
(( semvermatch("1.2.3", "~1.2") ))
Haga coincidir la versión semántica dada con una lista de contrinitaciones. El resultado es un booleano. Es posible especificar cualquier número de restricciones de versión. Si no se da restricción, la función solo verifica si la cadena dada es una versión semántica.
p.ej:
match : (( semvermatch("1.2.3", "~1.2") ))
resuelve
match : true
La lista completa de posibles especificaciones de restricciones se puede encontrar aquí.
(( semversort("1.2.3", "1.2.1") ))
Ordene una lista de versiones en orden ascendente. Se conserva una v
líder.
p.ej:
sorted : (( semversort("1.2.3", "1.2.1") ))
resuelve
sorted :
- 1.2.1
- 1.2.3
La lista de versiones a ordenar también se puede especificar con un solo argumento de la lista.
Spiff admite algunas funciones útiles para funcionar con certificados y claves X509 . Consulte también a la sección útil para conocer para encontrar algunos consejos para proporcionar estado.
(( x509genkey(spec) ))
Esta función se puede usar generar claves RSA o ECDSA privadas. El resultado será una clave codificada PEM como valor de cadena de línea múltiple. Si se proporciona un tamaño de clave (entero o cadena) como argumento, se generará una clave RSA con el tamaño de la clave dado (por ejemplo 2048). Dado uno de los valores de cadena
La función generará una clave ECDSA apropiada.
p.ej:
keys :
key : (( x509genkey(2048) ))
resuelve algo como
key : |+
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwxdZDfzxqz4hlRwTL060pm1J12mkJlXF0VnqpQjpnRTq0rns
CxMxvSfb4crmWg6BRaI1cEN/zmNcT2sO+RZ4jIOZ2Vi8ujqcbzxqyoBQuMNwdb32
...
oqMC9QKBgQDEVP7FDuJEnCpzqddiXTC+8NsC+1+2/fk+ypj2qXMxcNiNG1Az95YE
gRXbnghNU7RUajILoimAHPItqeeskd69oB77gig4bWwrzkijFXv0dOjDhQlmKY6c
pNWsImF7CNhjTP7L27LKk49a+IGutyYLnXmrlarcNYeCQBin1meydA==
-----END RSA PRIVATE KEY-----
(( x509publickey(key) ))
Para una clave o certificado determinado en formato PEM (por ejemplo, generado con la función X509GenKey) esta función extrae la clave pública y la devuelve nuevamente en formato PEM como una cadena de múltiples líneas.
p.ej:
keys :
key : (( x509genkey(2048) ))
public : (( x509publickey(key)
resuelve algo como
key : |+
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwxdZDfzxqz4hlRwTL060pm1J12mkJlXF0VnqpQjpnRTq0rns
CxMxvSfb4crmWg6BRaI1cEN/zmNcT2sO+RZ4jIOZ2Vi8ujqcbzxqyoBQuMNwdb32
...
oqMC9QKBgQDEVP7FDuJEnCpzqddiXTC+8NsC+1+2/fk+ypj2qXMxcNiNG1Az95YE
gRXbnghNU7RUajILoimAHPItqeeskd69oB77gig4bWwrzkijFXv0dOjDhQlmKY6c
pNWsImF7CNhjTP7L27LKk49a+IGutyYLnXmrlarcNYeCQBin1meydA==
-----END RSA PRIVATE KEY-----
public : |+
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwxdZDfzxqz4hlRwTL060pm1J12mkJlXF0VnqpQjpnRTq0rnsCxMx
vSfb4crmWg6BRaI1cEN/zmNcT2sO+RZ4jIOZ2Vi8ujqcbzxqyoBQuMNwdb325Bf/
...
VzYqyeQyvvRbNe73BXc5temCaQayzsbghkoWK+Wrc33yLsvpeVQBcB93Xhus+Lt1
1lxsoIrQf/HBsiu/5Q3M8L6klxeAUcDbYwIDAQAB
-----END RSA PUBLIC KEY-----
Para generar una clave pública SSH, se puede establecer un argumento de formato adicional opcional en ssh
. El resultado será un formato de clave pública regular utilizable para SSH. El formato predeterminado es pem
que proporciona el formato de salida PEM que se muestra arriba.
Las claves RSA están por defecto en el formato PKCS#1 ( RSA PUBLIC KEY
) en PEM. Si se requiere el formato PKIX genérico ( PUBLIC KEY
), se debe administrar el argumento de formato pkix
.
Usando el formato ssh
Esta función también se puede usar para convertir una clave pública formateada PEM en una tecla SSH,
(( x509cert(spec) ))
La función x509cert
crea certificados firmados localmente, ya sea uno o un certificado firmado por una CA dada. Devuelve un certificado codificado PEM como un valor de cadena de múltiples líneas.
El parámetro de especificación única toma un mapa con algunos campos opcionales y no opcionales utilizados para especificar la información del certificado. Puede ser una expresión de mapa en línea o cualquier referencia de mapa en el resto del documento YAML.
Se observan los siguientes campos de mapa:
Nombre de campo | Tipo | Requerido | Significado |
---|---|---|---|
commonName | cadena | opcional | Campo de nombre común del sujeto |
organization | Lista de cadenas o cadenas | opcional | Campo de organización del tema |
country | Lista de cadenas o cadenas | opcional | Campo de país del sujeto |
isCA | booleano | opcional | Opción de CA de certificado |
usage | Lista de cadenas o cadenas | requerido | Claves de uso para el certificado (ver más abajo) |
validity | entero | opcional | intervalo de validez en horas |
validFrom | cadena | opcional | Hora de inicio en el formato "1 de enero 01:22:31 2019" |
hosts | Lista de cadenas o cadenas | opcional | Lista de nombres o direcciones IP de DNS |
privateKey | cadena | Requerido o Key Public Key | clave privada para generar el certificado para |
publicKey | cadena | requerido o privado key | clave pública para generar el certificado para |
caCert | cadena | opcional | certificado para firmar con |
caPrivateKey | cadena | opcional | Key Priavte para caCert |
Para los certificados autofirmados, se debe establecer el campo privateKey
. publicKey
y los campos ca
deben omitirse. Si se administra el campo caCert
, también se requiere el campo caKey
. Si el campo privateKey
se da junto con el caCert
, la clave pública para el certificado se extrae de la clave privada.
Los campos adicionales se ignoran en silencio.
Se admiten las siguientes claves de uso (se ignora el caso):
Llave | Significado |
---|---|
Signature | X509.KeyUsageGiTalsignature |
Commitment | x509.KeyUsageContentCommitment |
KeyEncipherment | x509.KeyUsageKeyEncipherment |
DataEncipherment | x509.KeyUsageDataenciMment |
KeyAgreement | x509.KeyUsageKeyagreement |
CertSign | x509.KeyUsageCertsign |
CRLSign | x509.KeyUsageCrlsign |
EncipherOnly | x509.KeyUsageEncipheronly |
DecipherOnly | x509.KeyUsageGeCipheronly |
Any | x509.ExtKeyUsageany |
ServerAuth | x509.ExtKeyUsageerVerAuth |
ClientAuth | x509.ExtKeyUsageClientAuth |
codesigning | x509.ExtKeyUsageDesigning |
EmailProtection | x509.extKeyUsageEmailProtection |
IPSecEndSystem | x509.extKeyUsageIsCendSystem |
IPSecTunnel | x509.ExtKeyUsageIpsectunnel |
IPSecUser | x509.ExtKeyUsageipSecuser |
TimeStamping | x509.extkeyUsageTimestamping |
OCSPSigning | x509.ExtKeyUsageOcSigning |
MicrosoftServerGatedCrypto | x509.extkeyUsagemicrosoftServergatedCrypto |
NetscapeServerGatedCrypto | x509.extkeyUsagenetScapeServergatedCrypto |
MicrosoftCommercialCodeSigning | x509.extkeyUsagemicrosoftcomercialCodesigning |
MicrosoftKernelCodeSigning | x509.extkeyUsagemicrosoftkernelcodesigning |
p.ej:
spec :
<< : (( &local ))
ca :
organization : Mandelsoft
commonName : Uwe Krueger
privateKey : (( data.cakey ))
isCA : true
usage :
- Signature
- KeyEncipherment
data :
cakey : (( x509genkey(2048) ))
cacert : (( x509cert(spec.ca) ))
genera un certificado raíz autofirmado y resuelve algo como
cakey : |+
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwxdZDfzxqz4hlRwTL060pm1J12mkJlXF0VnqpQjpnRTq0rns
CxMxvSfb4crmWg6BRaI1cEN/zmNcT2sO+RZ4jIOZ2Vi8ujqcbzxqyoBQuMNwdb32
...
oqMC9QKBgQDEVP7FDuJEnCpzqddiXTC+8NsC+1+2/fk+ypj2qXMxcNiNG1Az95YE
gRXbnghNU7RUajILoimAHPItqeeskd69oB77gig4bWwrzkijFXv0dOjDhQlmKY6c
pNWsImF7CNhjTP7L27LKk49a+IGutyYLnXmrlarcNYeCQBin1meydA==
-----END RSA PRIVATE KEY-----
cacert : |+
-----BEGIN CERTIFICATE-----
MIIDCjCCAfKgAwIBAgIQb5ex4iGfyCcOa1RvnKSkMDANBgkqhkiG9w0BAQsFADAk
MQ8wDQYDVQQKEwZTQVAgU0UxETAPBgNVBAMTCGdhcmRlbmVyMB4XDTE4MTIzMTE0
...
pOUBE3Tgim5rnpa9K9RJ/m8IVqlupcONlxQmP3cCXm/lBEREjODPRNhU11DJwDdJ
5fd+t5SMEit2BvtTNFXLAwz48EKTxsDPdnHgiQKcbIV8NmgUNPHwXaqRMBLqssKl
Cyvds9xGtAtmZRvYNI0=
-----END CERTIFICATE-----
(( x509parsecert(cert) ))
Esta función analiza un certificado dado en formato PEM y devuelve un mapa de campos:
Nombre de campo | Tipo | Requerido | Significado |
---|---|---|---|
commonName | cadena | opcional | Campo de nombre común del sujeto |
organization | lista de cadenas | opcional | Campo de organización del tema |
country | lista de cadenas | opcional | Campo de país del sujeto |
isCA | booleano | siempre | Opción de CA de certificado |
usage | lista de cadenas | siempre | Claves de uso para el certificado (ver más abajo) |
validity | entero | siempre | intervalo de validez en horas |
validFrom | cadena | siempre | Hora de inicio en el formato "1 de enero 01:22:31 2019" |
validUntil | cadena | siempre | Hora de inicio en el formato "1 de enero 01:22:31 2019" |
hosts | lista de cadenas | opcional | Lista de nombres o direcciones IP de DNS |
dnsNames | lista de cadenas | opcional | Lista de nombres DNS |
ipAddresses | lista de cadenas | opcional | Lista de direcciones IP |
publicKey | cadena | siempre | clave pública para generar el certificado para |
p.ej:
data :
<< : (( &temporary ))
spec :
commonName : test
organization : org
validity : 100
isCA : true
privateKey : (( gen.key ))
hosts :
- localhost
- 127.0.0.1
usage :
- ServerAuth
- ClientAuth
- CertSign
gen :
key : (( x509genkey() ))
cert : (( x509cert(spec) ))
cert : (( x509parsecert(data.gen.cert) ))
resuelve
cert :
commonName : test
dnsNames :
- localhost
hosts :
- 127.0.0.1
- localhost
ipAddresses :
- 127.0.0.1
isCA : true
organization :
- org
publickey : |+
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA+UIZQUTa/j+WlXC394bccBTltV+Ig3+zB1l4T6Vo36pMBmU4JIkJ
...
TCsrEC5ey0cCeFij2FijOJ5kmm4cK8jpkkb6fLeQhFEt1qf+QqgBw3targ3LnZQf
uE9t5MIR2X9ycCQSDNBxcuafHSwFrVuy7wIDAQAB
-----END RSA PUBLIC KEY-----
usage :
- CertSign
- ServerAuth
- ClientAuth
validFrom : Mar 11 15:34:36 2019
validUntil : Mar 15 19:34:36 2019
validity : 99 # yepp, that's right, there has already time passed since the creation
Spiff admite algunas funciones útiles para funcionar con las teclas de cableguardas . Consulte también a la sección útil para conocer para encontrar algunos consejos para proporcionar estado.
(( wggenkey() ))
This function can be used generate private wireguard key. The result will base64 encoded.
p.ej:
keys :
key : (( wggenkey() ))
resolves to something like
key : WH9xNVJuSuh7sDVIyUAlmxc+woFDJg4QA6tGUVBtGns=
(( wgpublickey(key) ))
For a given key (for example generated with the wggenkey function) this function extracts the public key and returns it again in base64 format-
p.ej:
keys :
key : (( wggenkey() ))
public : (( wgpublickey(key)
resolves to something like
key : WH9xNVJuSuh7sDVIyUAlmxc+woFDJg4QA6tGUVBtGns=
public : n405KfwLpfByhU9pOu0A/ENwp0njcEmmQQJvfYHHQ2M=
(( lambda |x|->x ":" port ))
Lambda expressions can be used to define additional anonymous functions. They can be assigned to yaml nodes as values and referenced with path expressions to call the function with approriate arguments in other dynaml expressions. For the final document they are mapped to string values.
There are two forms of lambda expressions. Mientras
lvalue : (( lambda |x|->x ":" port ))
yields a function taking one argument by directly taking the elements from the dynaml expression,
string : " |x|->x " : " port "
lvalue : (( lambda string ))
evaluates the result of an expression to a function. The expression must evaluate to a function or string. If the expression is evaluated to a string it parses the function from the string.
Since the evaluation result of a lambda expression is a regular value, it can also be passed as argument to function calls and merged as value along stub processing.
A complete example could look like this:
lvalue : (( lambda |x,y|->x + y ))
mod : (( lambda|x,y,m|->(lambda m)(x, y) + 3 ))
value : (( .mod(1,2, lvalue) ))
rendimientos
lvalue : lambda |x,y|->x + y
mod : lambda|x,y,m|->(lambda m)(x, y) + 3
value : 6
If a complete expression is a lambda expression the keyword lambda
can be omitted.
Lambda expressions evaluate to lambda values, that are used as final values in yaml documents processed by spiff .
Note : If the final document still contains lambda values, they are transferred to a textual representation. It is not guaranteed that this representation can correctly be parsed again, if the document is re-processed by spiff . Especially for complex scoped and curried functions this is not possible.
Therefore function nodes should always be temporary or local to be available during processing or merging, but being omitted for the final document.
A typical function call uses positional arguments. Here the given arguments satisfy the declared function parameters in the given order. For lambda values it is also possible to use named arguments in the call expression. Here an argument is assigned to a dedicated parameter as declared by the lambda expression. The order of named arguments can be arbitrarily chosen.
p.ej:
func : (( |a,b,c|->{$a=a, $b=b, $c=c } ))
result : (( .func(c=1, b=2, a=1) ))
It is also posible to combine named with positional arguments. Hereby the positional arguments must follow the named ones.
p.ej:
func : (( |a,b,c|->{$a=a, $b=b, $c=c } ))
result : (( .func(c=1, 1, 2) ))
The same argument MUST NOT be satified by both, a named and a positional argument.
Instead of using the parameter name it is also possible to use the parameter index, instead.
p.ej:
func : (( |a,b,c|->{$a=a, $b=b, $c=c } ))
result : (( .func(3=1, 1) ))
As such, this feature seems to be quite useless, but it shows its power if combined with optional parameters or currying as shown in the next paragraphs.
A lambda expression might refer to absolute or relative nodes of the actual yaml document of the call. Relative references are evaluated in the context of the function call. Por lo tanto
lvalue : (( lambda |x,y|->x + y + offset ))
offset : 0
values :
offset : 3
value : (( .lvalue(1,2) ))
yields 6
for values.value
.
Besides the specified parameters, there is an implicit name ( _
), that can be used to refer to the function itself. It can be used to define self recursive function. Together with the logical and conditional operators a fibunacci function can be defined:
fibonacci : (( lambda |x|-> x <= 0 ? 0 :x == 1 ? 1 :_(x - 2) + _( x - 1 ) ))
value : (( .fibonacci(5) ))
yields the value 8
for the value
property.
By default reference expressions in a lambda expression are evaluated in the static scope of the lambda dedinition followed by the static yaml scope of the caller. Absolute references are always evalated in the document scope of the caller.
The name _
can also be used as an anchor to refer to the static definition scope of the lambda expression in the yaml document that was used to define the lambda function. Those references are always interpreted as relative references related to the this static yaml document scope. There is no denotation for accessing the root element of this definition scope.
Relative names can be used to access the static definition scope given inside the dynaml expression (outer scope literals and parameters of outer lambda parameters)
p.ej:
env :
func : (( |x|->[ x, scope, _, _.scope ] ))
scope : definition
call :
result : (( env.func("arg") ))
scope : call
yields the result
list:
call :
result :
- arg
- call
- (( lambda|x|->[x, scope, _, _.scope] )) # the lambda expression as lambda value
- definition
This also works across multiple stubs. The definition context is the stub the lambda expression is defined in, even if it is used in stubs down the chain. Therefore it is possible to use references in the lambda expression, not visible at the caller location, they carry the static yaml document scope of their definition with them.
Inner lambda expressions remember the local binding of outer lambda expressions. This can be used to return functions based on arguments of the outer function.
p.ej:
mult : (( lambda |x|-> lambda |y|-> x * y ))
mult2 : (( .mult(2) ))
value : (( .mult2(3) ))
yields 6
for property value
.
Trailing parameters may be defaulted in the lambda expression by assigning values in the declaration. Those parameter are then optional, it is not required to specify arguments for those parameters in function calls.
p.ej:
mult : (( lambda |x,y=2|-> x * y ))
value : (( .mult(3) ))
yields 6
for property value
.
It is possible to default all parameters of a lambda expression. The function can then be called without arguments. There might be no non-defaulted parameters after a defaulted one.
A call with positional arguments may only omit arguments for optional parameters from right to left. If there should be an explicit argument for the right most parameter, arguments for all parameters must be specified or named arguments must be used. Here the desired optional parameter can explicitly be set prior to the regular positional arguments.
p.ej:
func : (( |a,b=1,c=2|->{$a=a, $b=b, $c=c } ))
result : (( .func(c=3, 2) ))
evaluates result
to
result :
a : 2
b : 1
c : 3
The expression for the default does not need to be a constant value or even expression, it might refer to other nodes in the yaml document. The default expression is always evaluated in the scope of the lambda expression declaration at the time the lambda expression is evaluated.
p.ej:
stub.yaml
default : 2
mult : (( lambda |x,y=default * 2|-> x * y ))
template.yaml
mult : (( merge ))
scope :
default : 3
value : (( .mult(3) ))
evaluates value
to 12
The last parameter in the parameter list of a lambda expression may be a varargs parameter consuming additional argument in a fnction call. This parameter is always a list of values, one entry per additional argument.
A varargs parameter is denoted by a ...
following the last parameter name.
p.ej:
func : (( |a,b...|-> [a] b ))
result : (( .func(1,2,3) ))
yields the list [1, 2, 3]
for property result
.
If no argument is given for the varargs parameter its value is the empty list.
The ...
operator can also be used for inline list expansion.
If a vararg parameter should be set by a named argument its value must be a list.
Using the currying operator ( *(
) a lambda function may be transformed to another function with less parameters by specifying leading argument values.
The result is a new function taking the missing arguments (currying) and using the original function body with a static binding for the specified parameters.
p.ej:
mult : (( lambda |x,y|-> x * y ))
mult2 : (( .mult*(2) ))
value : (( .mult2(3) ))
Currying may be combined with defaulted parameters. But the resulting function does not default the leading parameters, it is just a new function with less parameters pinning the specified ones.
If the original function uses a variable argument list, the currying may span any number of the variable argument part, but once at least one such argument is given, the parameter for the variable part is satisfied. It cannot be extended by a function call of the curried function.
p.ej:
func : (( |a,b...|->join(a,b) ))
func1 : (( .func*(",","a","b")))
# invalid: (( .func1("c") ))
value : (( .func1() ))
evaluates value
to "a,b"
.
It is also possible to use currying for builtin functions, like join
.
p.ej:
stringlist : (( join*(",") ))
value : (( .stringlist("a", "b") ))
evaluates value
to "a,b"
.
There are several builtin functions acting on unevaluated or unevaluatable arguments, like defined
. For these functions currying is not possible.
Using positional arguments currying is only possible from right to left. But currying can also be done for named arguments. Here any parameter combination, regardless of the position in the parameter list, can be preset. The resulting function then has the unsatisfied parameters in their original order. Switching the parameter order is not possible.
p.ej:
func : (( |a,b=1,c=2|->{$a=a, $b=b, $c=c } ))
curry : (( .func(c=3, 2) ))
result : (( .curry(5) ))
evalutes result
to
result :
a : 2
b : 5
c : 3
The resulting function keeps the parameter b
. Hereby the default value will be kept. Therefore it can just be called without argument ( .curry()
), which would produce
result :
a : 2
b : 1
c : 3
Atención :
For compatibility reasons currying is also done, if a lambda function without defaulted parameters is called with less arguments than declared parameters.
This behaviour is deprecated and will be removed in the future. It is replaced by the currying operator.
p.ej:
mult : (( lambda |x,y|-> x * y ))
mult2 : (( .mult(2) ))
value : (( .mult2(3) ))
evaluates value
to 6.
(( catch[expr|v,e|->v] ))
This expression evaluates an expression ( expr
) and then executes a lambda function with the evaluation state of the expression. It always succeeds, even if the expression fails. The lambda function may take one or two arguments, the first is always the evaluated value (or nil
in case of an error). The optional second argument gets the error message the evaluation of the expression failed (or nil
otherwise)
The result of the function is the result of the whole expression. If the function fails, the complete expression fails.
p.ej:
data :
fail : (( catch[1 / 0|v,e|->{$value=v, $error=e}] ))
valid : (( catch[5 * 5|v,e|->{$value=v, $error=e}] ))
resolves to
data :
fail :
error : division by zero
value : null
valid :
error : null
value : 25
(( sync[expr|v,e|->defined(v.field),v.field|10] ))
If an expression expr
may return different results for different evaluations, it is possible to synchronize the final output with a dedicated condition on the expression value. Such an expression could, for example, be an uncached read
, exec
or pipe
call.
The second element must evaluate to a lambda value, given by either a regular expression or by a lambda literal as shown in the title. It may take one or two arguments, the actual value of the value expression and optionally an error message in case of a failing evaluation. The result of the evaluation of the lamda expression decides whether the state of the evaluation of the value expression is acceptable ( true
) or not ( false
).
If the value is accepted, an optional third expression is used to determine the final result of the sync[]
expression. It might be given as an expression evaluating to a lambda value, or by a comma separated expression using the same binding as the preceeding lambda literal. If not given, the value of the synched expression is returned.
If the value is not acceptable, the evaluation is repeated until a timeout applies. The timeout in seconds is given by an optional fourth expression (default is 5 min). Either the fourth, or the both, the third and the fourth elements may be omitted.
The lambda values might be given as literal, or by expression, leading to the following flavors:
sync[expr|v,e|->cond,value|10]
sync[expr|v,e|->cond|valuelambda|10]
sync[expr|v,e|->cond|v|->value|10]
sync[expr|condlambda|valuelambda|10]
sync[expr|condlambda|v|->value|10]
with or without the timeout expression.
p.ej:
data :
alice : 25
result : (( sync[data|v|->defined(v.alice),v.alice] ))
resolves to
data :
alice : 25
result : 25
This example is quite useless, because the sync expression is a constant. It just demonstrates the usage.
Mappings are used to produce a new list from the entries of a list or map , or a new map from entries of a map containing the entries processed by a dynaml expression. The expression is given by a lambda function. There are two basic forms of the mapping function: It can be inlined as in (( map[list|x|->x ":" port] ))
, or it can be determined by a regular dynaml expression evaluating to a lambda function as in (( map[list|mapping.expression))
(here the mapping is taken from the property mapping.expression
, which should hold an approriate lambda function).
The mapping comes in two target flavors: with []
or {}
in the syntax. The first flavor always produces a list from the entries of the given source. The second one takes only a map source and produces a filtered or transformed map .
Additionally the mapping uses three basic mapping behaviours:
map
. Here the result of the lambda function is used as new value to replace the original one. Oselect
. Here the result of the lambda function is used as a boolean to decide whether the entry should be kept ( true
) or omitted ( false
).sum
. Here always the list flavor is used, but the result type and content is completely determined by the parameterization of the statement by successively aggregating one entry after the other into an arbitrary initial value. Note : The special reference _
is not set for inlined lambda functions as part of the mapping syntax. Therefore the mapping statements (and all other statements using inlined lambda functions as part of their syntax) can be used inside regular lambda functions without hampering the meaning of this special refrence for the surrounding explicit lambda expression.
(( map[list|elem|->dynaml-expr] ))
Execute a mapping expression on members of a list to produce a new (mapped) list. The first expression ( list
) must resolve to a list. The last expression ( x ":" port
) defines the mapping expression used to map all members of the given list. Inside this expression an arbitrarily declared simple reference name (here x
) can be used to access the actually processed list element.
p.ej
port : 4711
hosts :
- alice
- bob
mapped : (( map[hosts|x|->x ":" port] ))
rendimientos
port : 4711
hosts :
- alice
- bob
mapped :
- alice:4711
- bob:4711
This expression can be combined with others, for example:
port : 4711
list :
- alice
- bob
joined : (( join( ", ", map[list|x|->x ":" port] ) ))
which magically provides a comma separated list of ported hosts:
port : 4711
list :
- alice
- bob
joined : alice:4711, bob:4711
(( map[list|idx,elem|->dynaml-expr] ))
In this variant, the first argument idx
is provided with the index and the second elem
with the value for the index.
p.ej
list :
- name : alice
age : 25
- name : bob
age : 24
ages : (( map[list|i,p|->i + 1 ". " p.name " is " p.age ] ))
rendimientos
list :
- name : alice
age : 25
- name : bob
age : 24
ages :
- 1. alice is 25
- 2. bob is 24
(( map[map|key,value|->dynaml-expr] ))
Mapping of a map to a list using a mapping expression. The expression may have access to the key and/or the value. If two references are declared, both values are passed to the expression, the first one is provided with the key and the second one with the value for the key. If one reference is declared, only the value is provided.
p.ej
ages :
alice : 25
bob : 24
keys : (( map[ages|k,v|->k] ))
rendimientos
ages :
alice : 25
bob : 24
keys :
- alice
- bob
(( map{map|elem|->dynaml-expr} ))
Using {}
instead of []
in the mapping syntax, the result is again a map with the old keys and the new entry values. As for a list mapping additionally a key variable can be specified in the variable list.
persons :
alice : 27
bob : 26
older : (( map{persons|x|->x + 1} ) ))
just increments the value of all entries by one in the field older
:
older :
alice : 28
bob : 27
Observación
An alternate way to express the same is to use sum[persons|{}|s,k,v|->s { k = v + 1 }]
.
(( map{list|elem|->dynaml-expr} ))
Using {}
instead of []
together with a list in the mapping syntax, the result is again a map with the list elements as key and the mapped entry values. For this all list entries must be strings. As for a list mapping additionally an index variable can be specified in the variable list.
persons :
- alice
- bob
length : (( map{persons|x|->length(x)} ) ))
just creates a map mapping the list entries to their length:
length :
alice : 5
bob : 3
(( select[expr|elem|->dynaml-expr] ))
With select
a map or list can be filtered by evaluating a boolean expression for every entry. An entry is selected if the expression evaluates to true equivalent value. (see conditions).
Basically it offers all the mapping flavors available for map[]
p.ej
list :
- name : alice
age : 25
- name : bob
age : 26
selected : (( select[list|v|->v.age > 25 ] ))
evaluates selected to
selected :
- name : bob
age : 26
Observación
An alternate way to express the same is to use map[list|v|->v.age > 25 ? v :~]
.
(( select{map|elem|->dynaml-expr} ))
Using {}
instead of []
in the mapping syntax, the result is again a map with the old keys filtered by the given expression.
persons :
alice : 25
bob : 26
older : (( select{persons|x|->x > 25} ))
just keeps all entries with a value greater than 25 and omits all others:
selected :
bob : 26
This flavor only works on maps .
Observación
An alternate way to express the same is to use sum[persons|{}|s,k,v|->v > 25 ? s {k = v} :s]
.
Aggregations are used to produce a single result from the entries of a list or map aggregating the entries by a dynaml expression. The expression is given by a lambda function. There are two basic forms of the aggregation function: It can be inlined as in (( sum[list|0|s,x|->s + x] ))
, or it can be determined by a regular dynaml expression evaluating to a lambda function as in (( sum[list|0|aggregation.expression))
(here the aggregation function is taken from the property aggregation.expression
, which should hold an approriate lambda function).
(( sum[list|initial|sum,elem|->dynaml-expr] ))
Execute an aggregation expression on members of a list to produce an aggregation result. The first expression ( list
) must resolve to a list. The second expression is used as initial value for the aggregation. The last expression ( s + x
) defines the aggregation expression used to aggregate all members of the given list. Inside this expression an arbitrarily declared simple reference name (here s
) can be used to access the intermediate aggregation result and a second reference name (here x
) can be used to access the actually processed list element.
p.ej
list :
- 1
- 2
sum : (( sum[list|0|s,x|->s + x] ))
rendimientos
list :
- 1
- 2
sum : 3
(( sum[list|initial|sum,idx,elem|->dynaml-expr] ))
In this variant, the second argument idx
is provided with the index and the third elem
with the value for the index.
p.ej
list :
- 1
- 2
- 3
prod : (( sum[list|0|s,i,x|->s + i * x ] ))
rendimientos
list :
- 1
- 2
- 3
prod : 8
(( sum[map|initial|sum,key,value|->dynaml-expr] ))
Aggregation of the elements of a map to a single result using an aggregation expression. The expression may have access to the key and/or the value. The first argument is always the intermediate aggregation result. If three references are declared, both values are passed to the expression, the second one is provided with the key and the third one with the value for the key. If two references are declared, only the second one is provided with the value of the map entry.
p.ej
ages :
alice : 25
bob : 24
sum : (( map[ages|0|s,k,v|->s + v] ))
rendimientos
ages :
alice : 25
bob : 24
sum : 49
Projections work over the elements of a list or map yielding a result list. Hereby every element is mapped by an optional subsequent reference expression. This may contain again projections, dynamic references or lambda calls. Basically this is a simplified form of the more general mapping yielding a list working with a lambda function using only a reference expression based on the elements.
(( expr.[*].value ))
All elements of a map or list given by the expression expr
are dereferenced with the subsequent reference expression (here .expr
). If this expression works on a map the elements are ordered accoring to their key values. If the subsequent reference expression is omitted, the complete value list isreturned. For a list expression this means the identity operation.
p.ej:
list :
- name : alice
age : 25
- name : bob
age : 26
- name : peter
age : 24
names : (( list.[*].name ))
yields for names
:
names :
- alice
- bob
- peter
or for maps:
networks :
ext :
cidr : 10.8.0.0/16
zone1 :
cidr : 10.9.0.0/16
cidrs : (( .networks.[*].cidr ))
yields for cidrs
:
cidrs :
- 10.8.0.0/16
- 10.9.0.0/16
(( list.[1..2].value ))
This projection flavor only works for lists. The projection is done for a dedicated slice of the initial list.
p.ej:
list :
- name : alice
age : 25
- name : bob
age : 26
- name : peter
age : 24
names : (( list.[1..2].name ))
yields for names
:
names :
- bob
- peter
In argument lists or list literals the list expansion operator ( ...
) can be used. It is a postfix operator on any list expression. It substituted the list expression by a sequence of the list members. It can be be used in combination with static list argument denotation.
p.ej:
list :
- a
- b
result : (( [ 1, list..., 2, list... ] ))
evaluates result
to
result :
- 1
- a
- b
- 2
- a
- b
The following example demonstrates the usage in combination with the varargs operator in functions:
func : (( |a,b...|-> [a] b ))
list :
- a
- b
a : (( .func(1,2,3) ))
b : (( .func("x",list..., "z") ))
c : (( [ "x", .func(list...)..., "z" ] ))
evaluates the following results:
a :
- 1
- 2
- 3
b :
- x
- a
- b
- z
c :
- x
- a
- b
- z
Please note, that the list expansion might span multiple arguments (including the varargs parameter) in lambda function calls.
Nodes of the yaml document can be marked to enable dedicated behaviours for this node. Such markers are part of the dynaml syntax and may be prepended to any dynaml expression. They are denoted by the &
character directly followed by a marker name. If the expression is a combination of markers and regular expressions, the expression follows the marker list enclosed in brackets (for example (( &temporary( a + b ) ))
).
Note : Instead of using a <<:
insert field to place markers it is possible now to use <<<:
, also, which allows to use regular yaml parsers for spiff-like yaml documents. <<:
is kept for backward compatibility.
(( &temporary ))
Maps, lists or simple value nodes can be marked as temporary . Temporary nodes are removed from the final output document, but are available during merging and dynaml evaluation.
p.ej:
temp :
<< : (( &temporary ))
foo : bar
value : (( temp.foo ))
rendimientos:
value : bar
Adding - <<: (( &temporary ))
to a list can be used to mark a list as temporary.
The temporary marker can be combined with regular dynaml expressions to tag plain fields. Hereby the parenthesised expression is just appended to the marker
p.ej:
data :
alice : (( &temporary ( "bar" ) ))
foo : (( alice ))
rendimientos:
data :
foo : bar
The temporary marker can be combined with the template marker to omit templates from the final output.
(( &local ))
The marker &local
acts similar to &temporary
but local nodes are always removed from a stub directly after resolving dynaml expressions. Such nodes are therefore not available for merging and they are not used for further merging of stubs and finally the template.
(( &dynamic ))
This marker can be used to mark a template expression (direct or referenced) to enforce the re-evaluation of the template in the usage context whenever the node is used to override or inject a node value along the processing chain. It can also be used together with &inject
or &default
.
p.ej:
template.yaml
data : 1
merged with
stub.yaml
id : (( &dynamic &inject &template(__ctx.FILE) ))
will resolve to
id : template.yaml
data : 1
The original template is kept along the merge chain and is evaluated separately in the context of the very stub or template it is used.
Using this marker for nodes not evaluationg to a template value is not possible.
(( &inject ))
This marker requests the marked item to be injected into the next stub level, even is the hosting element (list or map) does not requests a merge. This only works if the next level stub already contains the hosting element.
p.ej:
template.yaml
alice :
foo : 1
stub.yaml
alice :
bar : (( &inject(2) ))
nope : not injected
bob :
<< : (( &inject ))
foobar : yep
is merged to
alice :
foo : 1
bar : 2
bob :
foobar : yep
(( &default ))
Nodes marked as default will be used as default values for downstream stub levels. If no such entry is set there it will behave like &inject
and implicitly add this node, but existing settings will not be overwritten.
Maps (or lists) marked as default will be considered as values. The map is used as a whole as default if no such field is defined downstream.
p.ej:
template.yaml
data : { }
stub.yaml
data :
foobar :
<< : (( &default ))
foo : claude
bar : peter
is merged to
data :
foobar :
foo : claude
bar : peter
Their entries will neither be used for overwriting existing downstream values nor for defaulting non-existng fields of a not defaulted map field.
p.ej:
template.yaml
data :
foobar :
bar : bob
stub.yaml
data :
foobar :
<< : (( &default ))
foo : claude
bar : peter
is merged to
data :
foobar :
bar : bob
If sub sequent defaulting is desired, the fields of a default map must again be marked as default.
p.ej:
template.yaml
data :
foobar :
bar : bob
stub.yaml
data :
foobar :
<< : (( &default ))
foo : (( &default ("claude") ))
bar : peter
is merged to
data :
foobar :
foo : claude
bar : bob
Note : The behaviour of list entries marked as default is undefined.
(( &state ))
Nodes marked as state are handled during the merge processing as if the marker would not be present. But there will be a special handling for enabled state processing (option --state <path>
) at the end of the template processing. Additionally to the regular output a document consisting only of state nodes (plus all nested nodes) will be written to a state file. This file will be used as top-level stub for further merge processings with enabled state support.
This enables to keep state between two merge processings. For regular merging sich nodes are only processed during the first processing. Later processings will keep the state from the first one, because those nodes will be overiden by the state stub added to the end of the sub list.
If those nodes additionally disable merging (for example using (( &state(merge none) ))
) dynaml expressions in sub level nodes may perform explicit merging using the function stub()
to refer to values provided by already processed stubs (especially the implicitly added state stub). For an example please refer to the state library.
(( &template ))
Nodes marked as template will not be evaluated at the place of their occurrence. Instead, they will result in a template value stored as value for the node. They can later be instantiated inside a dynaml expression (see below).
(( &tag:name ))
The tag marker can be used to assign a logical name to a node value. This name can then be used in tagged reference expressions to refer to this node value (see below).
A tagged reference has the form <tagname>::<path>
. The <path>
may denote any sub node of a tagged node. If the value of a complete node (or a simple value node) should be used, the <path>
must denote the root path ( .
).
Tags can be used to label node values in multi-document streams (used as template). After defined for a document the tag can then be used to reference node values from the actual or previous document(s) of a document sequence in a multi-document stream. Tags can be added for complex or simple value nodes. A tagged reference may be used to refer to the tagged value as a whole or sub structure.
(( &tag:name(value) ))
This syntax is used to tag a node whose value is defined by a dynaml expression. It can also be used to denote tagged simple value nodes. (As usual the value part is optional for adding markers to structured values (see Markers).)
p.ej:
template.yaml
data :
<< : (( &tag:persons ))
alice : (( &tag:alice(25)
If the name is prefixed with a star ( *
), the tag is defined globally. Gobal tags surive stub processing and their value is visible in subsequent stub (and template) processings.
A tag name may consist of multiple components separated by a colon ( :
).
Tags can also be defined dynamically by the dynaml function tagdef.
(( tag::foo ))
Reference a sub path of the value of a tagged node.
p.ej:
template.yaml
data :
<< : (( &tag:persons ))
alice : 25
tagref : (( persons::alice ))
resolves tagref
to 25
(( tag::. ))
Reference the whole (structured) value of tagged node.
p.ej:
template.yaml
data :
alice : (( &tag:alice(25) ))
tagref : (( alice::. ))
resolves tagref
to 25
(( foo.bar::alice ))
Tag names may be structured. A tag name consists of a non-empty list of tag components separated by a dot or colon ( :
). A tag component may contain ASCII letters or numbers, starting wit a letter. Multi-component tags are subject to Tag Resolution.
A tag reference always contains a tag name and a path separated by a double colon ( ::
). The standard use-case is to describe a dedicated sub node for a tagged node value.
for example, if the tag X
describes the value
data :
alice : 25
bob : 24
the tagged reference X::data.alice
describes the value 25
.
For tagged references with a path other than .
(the whole tag value), structured tags feature a more sophisticated resolution mechanism. A structured tag consist of multiple tag components separated by a colon ( :
), for example lib:mylib
. Therefore, tags span a tree of namespaces or scopes used to resolve path references. A tag-less reference just uses the actual document or binding to resolve a path expression.
Evaluation of a path reference for a tag tries to resolve the path in the first tag tree level where the path is available (breadth-first search). If this level contains multiple tags that could resolve the given path, the resolution fails because it cannot be unambigiously resolved.
Por ejemplo:
tags :
- << : (( &tag:lib:alice ))
data : alice.alice
- << : (( &tag:lib:alice:v1))
data : alice.v1
- << : (( &tag:lib:bob))
other : bob
usage :
data : (( lib::data ))
effectively resolves usage.data
to lib:alice::data
and therefore to the value alice.alice
.
To achieve this all matching sub tags are orderd by their number of tag components. The first sub-level tag containing such a given path is selected. For this level, the matching tag must be non-ambigious. There must only be one tag with this level containing a matching path. If there are multiple ones the evaluation fails. In the above example this would be the case if tag lib:bob
would contain a field data
instead of or additional to other
.
This feature can be used in library stubs to provide qualified names for their elements that can be used with merging the containing document nodes into the template.
If the template file is a multi-document stream the tags are preserved during the complete processing. This means tags defined in a earlier document can be used in all following documents, also. But the tag names must be unique across all documents in a multi-document stream.
p.ej:
template.yaml
<< : (( &temporary ))
data :
<< : (( &tag:persons ))
alice : 25
bob : 24
---
alice : (( persons::alice ))
---
bob : (( persons::bob ))
resolves to
---
alice : 25
---
bob : 24
Tags defined by tag markers are available for stubs and templates. Global tags are available down the stub processing to the templates. Local tags are only avaialble on the processing level they are declared.
Additionally to the tags explicitly set by tag markers, there are implicit document tags given by the document index during the processing of a (multi-document) template. The implicit document tags are qualified with the prefix doc.
. This prefix should not be used to own tags in the documents
p.ej:
template.yaml
<< : (( &temporary ))
data :
<< : (( &tag:persons ))
alice : 25
bob : 24
---
alice : (( persons::alice ))
prev : (( doc.1::. ))
---
bob : (( persons::bob ))
prev : (( doc.2::. ))
resolves to
---
alice : 25
prev :
data :
alice : 25
bob : 24
---
bob : 24
prev :
alice : 25
prev :
data :
alice : 25
bob : 24
If the given document index is negative it denotes the document relative to the one actually processed (so, the tag doc.-1
denotes the previous document). The index doc.0
can be used to denote the actual document. Here always a path must be specified, it is not possible to refer to the complete document (with .
).
A map can be tagged by a dynaml expression to be used as template. Dynaml expressions in a template are not evaluated at its definition location in the document, but can be inserted at other locations using dynaml. At every usage location it is evaluated separately.
<<: (( &template ))
The dynaml expression &template
can be used to tag a map node as template:
p.ej:
foo :
bar :
<< : (( &template ))
alice : alice
bob : (( verb " " alice ))
The template will be the value of the node foo.bar
. As such it can be overwritten as a whole by settings in a stub during the merge process. Dynaml expressions in the template are not evaluated. A map can have only a single <<
field. Therefore it is possible to combine the template marker with an expression just by adding the expression in parenthesis.
Adding - <<: (( &template ))
to a list it is also possible to define list templates. It is also possible to convert a single expression value into a simple template by adding the template marker to the expression, for example foo: (( &template (expression) ))
The template marker can be combined with the temporary marker to omit templates from the final output.
Note : Instead of using a <<:
insert field to place the template marker it is possible now to use <<<:
, also, which allows to use regular yaml parsers for spiff-like yaml documents. <<:
is kept for backward compatibility.
(( *foo.bar ))
The dynaml expression *<reference expression>
can be used to evaluate a template somewhere in the yaml document. Dynaml expressions in the template are evaluated in the context of this expression.
p.ej:
foo :
bar :
<< : (( &template ))
alice : alice
bob : (( verb " " alice ))
use :
subst : (( *foo.bar ))
verb : loves
verb : hates
evaluates to
foo :
bar :
<< : (( &template ))
alice : alice
bob : (( verb " " alice ))
use :
subst :
alice : alice
bob : loves alice
verb : loves
verb : hates
_
The special reference _
( self ) can be used inside of lambda functions and templates . They refer to the containing element (the lambda function or template).
Additionally it can be used to lookup relative reference expressions starting with the defining document scope of the element skipping intermediate scopes.
p.ej:
node :
data :
scope : data
funcs :
a : (( |x|->scope ))
b : (( |x|->_.scope ))
c : (( |x|->_.data.scope ))
scope : funcs
call :
scope : call
a : (( node.funcs.a(1) ))
b : (( node.funcs.b(1) ))
c : (( node.funcs.c(1) ))
evaluates call
to
call :
a : call
b : funcs
c : data
scope : call
__
The special reference __
can be used to lookup references as relative references starting with the document node hosting the actually evaluated dynaml expression skipping intermediate scopes.
This can, for example be used to relatively access a lambda value field besides the actual field in a map. The usage of plain function names is reserved for builtin functions and are not used as relative references.
This special reference is also available in expressions in templates and refer to the map node in the template hosting the actually evaluated expression.
p.ej:
templates :
templ :
<< : (( &template ))
self : (( _ ))
value : (( ($self="value") __.self ))
result : (( scope ))
templ : (( _.scope ))
scope : templates
result :
inst : (( *templates.templ ))
scope : result
evaluates result
to
result :
inst :
result : result
templ : templates
self :
<< : (( &template ))
result : (( scope ))
self : (( _ ))
templ : (( _.scope ))
value : (( ($self="value") __.self ))
value :
<< : (( &template ))
result : (( scope ))
self : (( _ ))
templ : (( _.scope ))
value : (( ($self="value") __.self ))
scope : result
or with referencing upper nodes:
templates :
templ :
<< : (( &template ))
alice : root
data :
foo : (( ($bob="local") __.bob ))
bar : (( ($alice="local") __.alice ))
bob : static
result : (( *templates.templ ))
evaluates result
to
result :
alice : root
data :
bar : root
foo : static
bob : static
___
The special reference ___
can be used to lookup references in the outer most scope. It can therefore be used to access processing bindings specified for a document processing via command line or API. If no bindings are specified the document root is used.
Calling spiff merge template.yaml --bindings bindings.yaml
with a binding of
bindings.yaml
input1 : binding1
input2 : binding2
and the template
template.yaml
input1 : top1
map :
input : map
input1 : map1
results :
frommap : (( input1 ))
fromroot : (( .input1 ))
frombinding1 : (( ___.input1 ))
frombinding2 : (( input2 ))
evaluates map.results
to
results :
frombinding1 : binding1
frombinding2 : binding2
frommap : map1
fromroot : top1
__ctx.OUTER
The context field OUTER
is used for nested merges. It is a list of documents, index 0 is the next outer document, and so on.
(( {} ))
Provides an empty map.
(( [] ))
Provides an empty list. Basically this is not a dedicated literal, but just a regular list expression without a value.
(( ~ ))
Provides the null value.
(( ~~ ))
This literal evaluates to an undefined expression. The element (list entry or map field) carrying this value, although defined, will be removed from the document and handled as undefined for further merges and the evaluation of referential expressions.
p.ej:
foo : (( ~~ ))
bob : (( foo || ~~ ))
alice : (( bob || "default"))
evaluates to
alice : default
Inside every dynaml expression a virtual field __ctx
is available. It allows access to information about the actual evaluation context. It can be accessed by a relative reference expression.
The following fields are supported:
Field Name | Tipo | Significado |
---|---|---|
VERSION | cadena | current version of spiff |
FILE | cadena | name of actually processed template file |
DIR | cadena | name of directory of actually processed template file |
RESOLVED_FILE | cadena | name of actually processed template file with resolved symbolic links |
RESOLVED_DIR | cadena | name of directory of actually processed template file with resolved symbolic links |
PATHNAME | cadena | path name of actually processed field |
PATH | list[string] | path name as component list |
OUTER | yaml doc | outer documents for nested merges, index 0 is the next outer document |
BINDINGS | yaml doc | the external bindings for the actual processing (see also ___) |
If external bindings are specified they are the last elements in OUTER
.
p.ej:
template.yml
foo :
bar :
path : (( __ctx.PATH ))
str : (( __ctx.PATHNAME ))
file : (( __ctx.FILE ))
dir : (( __ctx.DIR ))
evaluates to
p.ej:
foo :
bar :
dir : .
file : template.yml
path :
- foo
- bar
- path
str : foo.bar.str
Dynaml expressions are evaluated obeying certain priority levels. This means operations with a higher priority are evaluated first. For example the expression 1 + 2 * 3
is evaluated in the order 1 + ( 2 * 3 )
. Operations with the same priority are evaluated from left to right (in contrast to version 1.0.7). This means the expression 6 - 3 - 2
is evaluated as ( 6 - 3 ) - 2
.
The following levels are supported (from low priority to high priority)
||
, //
foo bar
)-or
, -and
==
, !=
, <=
, <
, >
, >=
+
, -
*
, /
, %
( )
, !
, constants, references ( foo.bar
), merge
, auto
, lambda
, map[]
, and functionsThe complete grammar can be found in dynaml.peg.
Feature state: alpha
Attention: This is an alpha feature. It must be enabled on the command line with the --interpolation
or --features=interpolation
option. Also for the spiff library it must explicitly be enabled. By adding the key interpolation
to the feature list stored in the environment variable SPIFF_FEATURES
this feature will be enabled by default.
Typically a complete value can either be a literal or a dynaml expression. For string literals it is possible to use an interpolation syntax to embed dynaml expressions into strings.
Por ejemplo
data : test
interpolation : this is a (( data ))
replaces the part between the double brackets by the result of the described expression evaluation. Here the brackets can be escaped by the usual escaping ( ((!
) syntax.
Those string literals will implicitly be converted to complete flat dynaml expressions. The example above will therefore be converted into
(( "this is a " data ))
which is the regular dynaml equivalent. The escaping is very ticky, and may be there are still problems. Quotes inside an embedded dynaml expression can be escaped to enable quotes in string literals.
Incomplete or partial interpolation expressions will be ignored and just used as string.
Strings inside a dynaml expression are NOT directly interpolated again, thus
data : " test "
interpolated : " this is a (( length( " (( data )) " ) data )) "
will resolve interpolation
to this is 10test
and not to this is 4test
.
But if the final string after the expression evaluation again describes a string interpolation it will be processed, again.
data : test
interpolation : this is a (( "(( data ))" data ))
will resolve interpolation
to this is testtest
.
The embedded dynaml expression must be concatenatable with strings.
Feature state: alpha
In addition to describe conditions and loops with dynaml expressions it is also possible to use elements of the document structure to embed control structures.
Such a YAML-based control structure is always described as a map in YAML/JSON. The syntactical elements are expressed as map fields starting with <<
. Additionally, depending on the control structure, regular fields are possible. Control structures finally represent a value for the containing node. They may contain marker expressions ( <<
), also.
p.ej:
temp :
<< : (( &temporary ))
<<if : (( features("control") ))
<<then :
alice : 25
bob : 26
resolves to
final :
alice : 25
bob : 26
Tu use this alpha feature the feature flag control
must be enabled.
Please be aware: Control structure maps typically are always completely resolved before they are evaluated.
A control structure itself is always a dedicated map node in a document. It is substituted by a regular value node determined by the execution of the control structure.
The fields of a control structure map are not subject to overwriting by stubs, but the complete structure can be overwritten.
If used as value for a map field the resulting value is just used as effective value for this field.
If a map should be enriched by maps resulting from multiple control structures the special control structure <<merge:
can be used. It allows to specify a list of maps which should be merged with the actual control structure map to finally build the result value.
A control structure can be used as list value, also. In this case there is a dedicated interpretation of the resulting value of the control structure. If it is NOT a list value, for convenience, the value is directly used as list entry and substitutes the control structure map.
If the resulting value is again a list, it is inserted into the containing list at the place of occurrence of the control structure. So, if a list value should be used as dedicated entry in a list, the result of a control structure must be a list with the intended list as entry.
p.ej:
list :
- <<if : (( features("control") ))
<<then : alice
- <<if : (( features("control") ))
<<then :
- - peter
- <<if : (( features("control") ))
<<then :
- bob
resolves to
list :
- alice
- - peter
- bob
<<if:
The condition structure is defined by the syntax field <<if
. It additionally accepts the fields <<then
and <<else
.
The condition field must provide a boolean value. If it is true
the optional <<then
field is used to substitute the control structure, otherwise the optional <<else
field is used.
If the appropriate case is not specified, the result is the undefined (( ~~ ))
value. The containing field is therefore completely omitted from the output.
.p.ej:
x : test1
cond :
field :
<<if : (( x == "test" ))
<<then : alice
<<else : bob
evaluates cond.field
to bob
If the else case is omitted, the cond
field would be an empty map ( field
is omitted, because the contained control structure evaluates to undefined )
A comparable way to do this with regular dynaml could look like this:
cond : (( x == "test" ? "alice" :"bob" ))
A better way more suitable for complex cases would be:
local :
<< : (( &local))
then : alice
else : bob
cond : (( x == "test" ? local.then :local.else ))
<<switch:
The switch
control structure evaluates the switch value of the <<switch
field to a string and uses it to select an appropriate regular field in the control map.
If it is not found the value of the optional field <<default
is used. If no default is specified, the control structure evaluates to an error, if no appropriate regular field is available.
The nil value matches the default
case. If the switch value is undefined the control evaluates to the undefined value (( ~~ ))
.
p.ej:
x : alice
value :
<<switch : (( x ))
alice : 25
bob : 26
<<default : other
evaluates value
to 25
.
A comparable way to do this with regular dynaml could look like this:
local :
<< : (( &local))
cases :
alice : 25
bob : 26
default : other
value : (( local.cases[x] || local.default ))
<<type:
The type
control structure evaluates the type of the value of the <<type
field and uses it to select an appropriate regular field in the control map.
If it is not found the value of the optional field <<default
is used. If no default is specified, the control structure evaluates to an error, if no appropriate regular field is available.
p.ej:
x : alice
value :
<<type : (( x ))
string : alice
<<default : unknown
evaluates value
to alice
.
A comparable way to do this with regular dynaml could look like this:
local :
<< : (( &local))
cases :
string : alice
default : unknown
value : (( local.cases[type(x)] || local.default ))
For more complex scenarios not only switching on strings a second syntax can be used. Instead of using fields in the control map as cases, a dedicated field <<cases
may contain a list of cases, that are checked sequentially (In this flavor regular fields are not allowed anymore).
Every case is described again by a map containing the fields:
case
: the expected value to match the switch valuematch
: a lambda function taking one argument and yielding a boolean value used to match the given switch valuevalue
: (optional) the resulting value in case of a match. If not defined the result will be the undefined value. One of case
or match
must be present.
p.ej:
x : 5
selected :
<<switch : (( x ))
<<cases :
- case :
alice : 25
value : alice
- match : (( |v|->v == 5 ))
value : bob
<<default : unknown
resolves to
x : 5
selected : bob
If x
would be set to the complex value
x :
alice : 25
it would resolve to
x :
alice : 25
selected : alice
<<for:
The loop control is able to execute a multi-dimensional loop and produce a list or map based on the value combinations.
The loop ranges are specified by the value of the <<for
field. It is possible ol loop over lists or maps. The range specification can be given by either a map or list:
map : the keys of the map are the names of the control variables and the values must be lists or maps specifying the ranges.
The map key might optionally be a comma-separated pair (for example key,value
) of variable names. In this case the first name is the name for the index variable and the second one for the value variable.
If multiple ranges are specified iterations are alphabetically ordered by value variable name (first) and index variable name (second) to determine the traversing order.
list : if the control variables are defined by a list, each list element must contain two mandatory and one optional field(s):
name
: the name of the (list or map entry value) control variablevalues
: a list to define the value range.index
: (optional) the name of the variable providing the list index or map key of the loop range (defaulted to index-<name>
)Here the order in the list determine the traversal order.
Traversal is done by recursively iterating follow up ranges for every entry in the actual range. This means the last range is completely iterated for the first values of the first ranges first.
If no index variable is specified for a loop range there is an additional implicit binding for every control variable describing the actual list index or map key of the processed value for this dimension. It is denoted by index-<control variable>
If multiple loop ranges are specified, the ranges may mix iterations over maps and lists.
The iteration result value is determined by the value of the <<do
field. It is implicitly handled as template and is evaluated for every set of iteration values.
The result of the evaluation using only the <<do
value field is a list.
p.ej:
alice :
- a
- b
bob :
- 1
- 2
- 3
list :
<<for :
key,alice : (( .alice )) # sorted by using alice as primary sort key
bob : (( .bob ))
<<do :
value : (( alice "-" key "-" bob "-" index-bob ))
evaluates list
to
list :
- value : a-0-1-0
- value : a-0-2-1
- value : a-0-3-2
- value : b-1-1-0
- value : b-1-2-1
- value : b-1-3-2
It first iterates over the values for alice
. For each such value it then iterates over the values of bob
.
A comparable way to do this with regular dynaml could look like this:
list : (( sum[alice|[]|s,key,alice|-> s sum[bob|[]|s,index_bob,bob|->s (alice "-" key "-" bob "-" index_bob)]] ))
A result list may omit entries if the value expression evaluates to the undefined value ( ~~
). The nil value ( ~
) is kept. This way a for
control can be used to filter lists.
p.ej:
bob :
- 1
- 2
- 3
filtered :
<<for :
bob : (( .bob ))
<<do : (( bob == 2 ? ~~ :bob ))
resolves to
bob :
- 1
- 2
- 3
filtered :
- 1
- 3
If the result should be a map it is required to additionally specify a key value for every iteration. This is specified by the optional <<mapkey
field. Like the <<do
field it is implicitly handled as template and re-evaluated for every iteration.
p.ej:
x : suffix
alice :
- a
- b
bob :
- 1
- 2
- 3
map :
<<for :
- name : alice
values : (( .alice ))
- name : bob
values : (( .bob ))
<<mapkey : (( alice bob ))
<<do :
value : (( alice bob x ))
evaluates the field map
to
map :
a1 :
value : a1suffix
a2 :
value : a2suffix
a3 :
value : a3suffix
b1 :
value : b1suffix
b2 :
value : b2suffix
b3 :
value : b3suffix
Here the traversal order is irrelevant as long as the generated key values are unique. If several evaluations of the key expression yield the same value the last one will win.
A comparable way to do this with regular dynaml could look like this:
map : (( sum[alice|{}|s,index_alice,alice|-> s sum[bob|{}|s,index_bob,bob|->s {(alice bob)=alice bob x}]] ))
An iteration value is ignored if the key or the value evaluate to the undefined value (( ~~ ))
. Additionally the key may evaluate to the nil value (( ~ ))
, also.
p.ej:
bob :
b1 : 1
b2 : 2
b3 : 3
filtered :
<<for :
key,bob : (( .bob ))
<<mapkey : (( key ))
<<do : (( bob == 2 ? ~~ :bob ))
o
bob :
b1 : 1
b2 : 2
b3 : 3
filtered :
<<for :
key,bob : (( .bob ))
<<mapkey : (( bob == 2 ? ~~ :key ))
<<do : (( bob ))
determinarse
bob :
b1 : 1
b2 : 2
b3 : 3
filtered :
b1 : 1
b3 : 3
<<merge:
With merge
it is possible to merge maps given as list value of the <<merge
field with regular map fields from the control structure to determine the final map value.
The value for <<merge:
may be a single map or a list of maps to join with the directly given fields.
p.ej:
map :
<<merge :
- bob : 26
charlie : 1
- charlie : 27
alice : 25
charlie : 2
resolves to
map :
alice : 25
bob : 26
charlie : 27
If multiple maps contain the same key, the last value (in order of list) will win.
This might be combined with other control structures, for example to conditionally merge multiple maps:
p.ej:
x : charlie
map :
<<merge :
- <<if : (( x == "charlie" ))
<<then :
charlie : 27
- <<if : (( x == "alice" ))
<<then :
alice : 20
alice : 25
charlie : 2
resolves to
x : charlie
map :
alice : 25
charlie : 27
By default spiff
performs a deep structural merge of its first argument, the template file, with the given stub files. The merge is processed from right to left, providing an intermediate merged stub for every step. This means, that for every step all expressions must be locally resolvable.
Structural merge means, that besides explicit dynaml merge
expressions, values will be overridden by values of equivalent nodes found in right-most stub files. In general, flat value lists are not merged. Only lists of maps can be merged by entries in a stub with a matching index.
There is a special support for the auto-merge of lists containing maps, if the maps contain a name
field. Hereby the list is handled like a map with entries according to the value of the list entries' name
field. If another key field than name
should be used, the key field of one list entry can be tagged with the prefix key:
to indicate the indended key name. Such tags will be removed for the processed output.
In general the resolution of matching nodes in stubs is done using the same rules that apply for the reference expressions (( foo.bar.[1].baz )).
For example, given the file template.yml :
foo :
- name : alice
bar : template
- name : bob
bar : template
plip :
- id : 1
plop : template
- id : 2
plop : template
bar :
- foo : template
list :
- a
- b
and file stub.yml :
foo :
- name : bob
bar : stub
plip :
- key:id : 1
plop : stub
bar :
- foo : stub
list :
- c
- d
spiff merge template.yml stub.yml
regresa
foo :
- bar : template
name : alice
- bar : stub
name : bob
plip :
- id : 1
plop : stub
- id : 2
plop : template
bar :
- foo : stub
list :
- a
- b
Be careful that any name:
key in the template for the first element of the plip
list will defeat the key:id: 1
selector from the stub. When a name
field exist in a list element, then this element can only be targeted by this name. When the selector is defeated, the resulting value is the one provided by the template.
Merging the following files in the given order
deployment.yml
networks : (( merge ))
cf.yml
utils : (( merge ))
network : (( merge ))
meta : (( merge ))
networks :
- name : cf1
<< : (( utils.defNet(network.base.z1,meta.deployment_no,30) ))
- name : cf2
<< : (( utils.defNet(network.base.z2,meta.deployment_no,30) ))
infrastructure.yml
network :
size : 16
block_size : 256
base :
z1 : 10.0.0.0
z2 : 10.1.0.0
rules.yml
utils :
defNet : (( |b,n,s|->(*.utils.network).net ))
network :
<< : (( &template ))
start : (( b + n * .network.block_size ))
first : (( start + ( n == 0 ? 2 :0 ) ))
lower : (( n == 0 ? [] :b " - " start - 1 ))
upper : (( start + .network.block_size " - " max_ip(net.subnets.[0].range) ))
net :
subnets :
- range : (( b "/" .network.size ))
reserved : (( [] lower upper ))
static :
- (( first " - " first + s - 1 ))
instance.yml
meta :
deployment_no : 1
will yield a network setting for a dedicated deployment
networks :
- name : cf1
subnets :
- range : 10.0.0.0/16
reserved :
- 10.0.0.0 - 10.0.0.255
- 10.0.2.0 - 10.0.255.255
static :
- 10.0.1.0 - 10.0.1.29
- name : cf2
subnets :
- range : 10.1.0.0/16
reserved :
- 10.1.0.0 - 10.1.0.255
- 10.1.2.0 - 10.1.255.255
static :
- 10.1.1.0 - 10.1.1.29
Using the same config for another deployment of the same type just requires the replacement of the instance.yml
. Using a different instance.yml
meta :
deployment_no : 0
will yield a network setting for a second deployment providing the appropriate settings for a unique other IP block.
networks :
- name : cf1
subnets :
- range : 10.0.0.0/16
reserved :
- 10.0.1.0 - 10.0.255.255
static :
- 10.0.0.2 - 10.0.0.31
- name : cf2
subnets :
- range : 10.1.0.0/16
reserved :
- 10.1.1.0 - 10.1.255.255
static :
- 10.1.0.2 - 10.1.0.31
If you move to another infrastructure you might want to change the basic IP layout. You can do it just by adapting the infrastructure.yml
network :
size : 17
block_size : 128
base :
z1 : 10.0.0.0
z2 : 10.0.128.0
Without any change to your other settings you'll get
networks :
- name : cf1
subnets :
- range : 10.0.0.0/17
reserved :
- 10.0.0.128 - 10.0.127.255
static :
- 10.0.0.2 - 10.0.0.31
- name : cf2
subnets :
- range : 10.0.128.0/17
reserved :
- 10.0.128.128 - 10.0.255.255
static :
- 10.0.128.2 - 10.0.128.31
There are several scenarios yielding results that do not seem to be obvious. Here are some typical pitfalls.
The auto merge never adds nodes to existing structures
For example, merging
template.yml
foo :
alice : 25
con
stub.yml
foo :
alice : 24
bob : 26
rendimientos
foo :
alice : 24
Use <<: (( merge )) to change this behaviour, or explicitly add desired nodes to be merged:
template.yml
foo :
alice : 25
bob : (( merge ))
Simple node values are replaced by values or complete structures coming from stubs, structures are deep merged.
For example, merging
template.yml
foo : (( ["alice"] ))
con
stub.yml
foo :
- peter
- paul
rendimientos
foo :
- peter
- paul
But the template
foo : [ (( "alice" )) ]
is merged without any change.
Expressions are subject to be overridden as a whole
A consequence of the behaviour described above is that nodes described by an expession are basically overridden by a complete merged structure, instead of doing a deep merge with the structues resulting from the expression evaluation.
For example, merging
template.yml
men :
- bob : 24
women :
- alice : 25
people : (( women men ))
con
stub.yml
people :
- alice : 13
rendimientos
men :
- bob : 24
women :
- alice : 25
people :
- alice : 24
To request an auto-merge of the structure resulting from the expression evaluation, the expression has to be preceeded with the modifier prefer
( (( prefer women men ))
). This would yield the desired result:
men :
- bob : 24
women :
- alice : 25
people :
- alice : 24
- bob : 24
Nested merge expressions use implied redirections
merge
expressions implicity use a redirection implied by an outer redirecting merge. In the following example
meta :
<< : (( merge deployments.cf ))
properties :
<< : (( merge ))
alice : 42
the merge expression in meta.properties
is implicity redirected to the path deployments.cf.properties
implied by the outer redirecting merge
. Therefore merging with
deployments :
cf :
properties :
alice : 24
bob : 42
rendimientos
meta :
properties :
alice : 24
bob : 42
Functions and mappings can freely be nested
p.ej:
pot : (( lambda |x,y|-> y == 0 ? 1 :(|m|->m * m)(_(x, y / 2)) * ( 1 + ( y % 2 ) * ( x - 1 ) ) ))
seq : (( lambda |b,l|->map[l|x|-> .pot(b,x)] ))
values : (( .seq(2,[ 0..4 ]) ))
yields the list [ 1,2,4,8,16 ]
for the property values
.
Functions can be used to parameterize templates
The combination of functions with templates can be use to provide functions yielding complex structures. The parameters of a function are part of the scope used to resolve reference expressions in a template used in the function body.
p.ej:
relation :
template :
<< : (( &template ))
bob : (( x " " y ))
relate : (( |x,y|->*relation.template ))
banda : (( relation.relate("loves","alice") ))
evaluates to
relation :
relate : lambda|x,y|->*(relation.template)
template :
<< : (( &template ))
bob : (( x " " y ))
banda :
bob : loves alice
Scopes can be used to parameterize templates
Scope literals are also considered when instantiating templates. Therefore they can be used to set explicit values for relative reference expressions used in templates.
p.ej:
alice : 1
template :
<< : (( &template ))
sum : (( alice + bob ))
scoped : (( ( $alice = 25, "bob" = 26 ) *template ))
evaluates to
alice : 1
template :
<< : (( &template ))
sum : (( alice + bob ))
scoped :
sum : 51
Aggregations may yield complex values by using templates
The expression of an aggregation may return complex values by returning inline lists or instantiated templates. The binding of the function will be available (as usual) for the evaluation of the template. In the example below the aggregation provides a map with both the sum and the product of the list entries containing the integers from 1 to 4.
p.ej:
sum : (( sum[[1..4]|init|s,e|->*temp] ))
temp :
<< : (( &template ))
sum : (( s.sum + e ))
prd : (( s.prd * e ))
init :
sum : 0
prd : 1
yields for sum
the value
sum:
prd: 24
sum: 10
Taking advantage of the undefined value
At first glance it might look strange to introduce a value for undefined . But it can be really useful as will become apparent with the following examples.
Whenever a stub syntactically defines a field it overwrites the default in the template during merging. Therefore it would not be possible to define some expression for that field that eventually keeps the default value. Here the undefined value can help:
eg: merging
template.yml
alice : 24
bob : 25
con
stub.yml
alice : (( config.alice * 2 || ~ ))
bob : (( config.bob * 3 || ~~ ))
rendimientos
alice : ~
bob : 25
There is a problem accessing upstream values. This is only possible if the local stub contains the definition of the field to use. But then there will always be a value for this field, even if the upstream does not overwrite it.
Here the undefined value can help by providing optional access to upstream values. Optional means, that the field is only defined, if there is an upstream value. Otherwise it is undefined for the expressions in the local stub and potential downstream templates. This is possible because the field is formally defined, and will therefore be merged, only after evaluating the expression if it is not merged it will be removed again.
eg: merging
template.yml
alice : 24
bob : 25
peter : 26
con
mapping.yml
config :
alice : (( ~~ ))
bob : (( ~~ ))
alice : (( config.alice || ~~ ))
bob : (( config.bob || ~~ ))
peter : (( config.peter || ~~ ))
y
config.yml
config :
alice : 4711
peter : 0815
rendimientos
alice : 4711 # transferred from config's config value
bob : 25 # kept default value, because not set in config.yml
peter : 26 # kept, because mapping source not available in mapping.yml
This can be used to add an intermediate stub, that offers a dedicated configuration interface and contains logic to map this interface to a manifest structure already defining default values.
Templates versus map literals
As described earlier templates can be used inside functions and mappings to easily describe complex data structures based on expressions refering to parameters. Before the introduction of map literals this was the only way to achieve such behaviour. The advantage is the possibility to describe the complex structure as regular part of a yaml document, which allows using the regular yaml formatting facilitating readability.
p.ej:
scaling :
runner_z1 : 10
router_z1 : 4
jobs : (( sum[scaling|[]|s,k,v|->s [ *templates.job ] ] ))
templates :
job :
<< : (( &template ))
name : (( k ))
instances : (( v ))
evaluates to
scaling :
runner_z1 : 10
router_z1 : 4
jobs :
- instances : 4
name : router_z1
- instances : 10
name : runner_z1
...
With map literals this construct can significantly be simplified
scaling :
runner_z1 : 10
router_z1 : 4
jobs : (( sum[scaling|[]|s,k,v|->s [ {"name"=k, "value"=v} ] ] ))
Nevertheless the first, template based version might still be useful, if the data structures are more complex, deeper or with complex value expressions. For such a scenario the description of the data structure as template should be preferred. It provides a much better readability, because every field, list entry and value expression can be put into dedicated lines.
But there is still a qualitative difference. While map literals are part of a single expression always evaluated as a whole before map fields are available for referencing, templates are evaluated as regular yaml documents that might contain multiple fields with separate expressions referencing each other.
p.ej:
range : (( (|cidr,first,size|->(*templates.addr).range)("10.0.0.0/16",10,255) ))
templates :
addr :
<< : (( &template ))
base : (( min_ip(cidr) ))
start : (( base + first ))
end : (( start + size - 1 ))
range : (( start " - " end ))
evaluates range
to
range : 10.0.0.10 - 10.0.1.8
...
Defaulting and Requiring Fields
Traditionally defaulting in spiff is done by a downstream template where the playload data file is used as stub.
Fields with simple values can just be specified with their values. They will be overwritten by stubs using the regular spiff document merging mechanisms.
It is more difficult for maps or lists. If a map is specified in the template only its fields will be merged (see above), but it is never replaced as a whole by settings in the playload definition files. And Lists are never merged.
Therefore maps and lists that should be defaulted as a whole must be specified as initial expressions (referential or inline) in the template file.
eg: merging of
template.yaml
defaults :
<< : (( &temporary ))
person :
name : alice
age : bob
config :
value1 : defaultvalue
value2 : defaultvalue
person : (( defaults.person ))
y
payload.yaml
config :
value2 : configured
othervalue : I want this but don't get it
evaluates to
config :
person :
age : bob
name : alice
value1 : defaultvalue
value2 : configured
In such a scenario the structure of the resulting document is defined by the template. All kinds of variable fields or sub-structures must be forseen by the template by using <<: (( merge ))
expressions in maps.
eg: changing template to
template.yaml
defaults :
<< : (( &temporary ))
person :
name : alice
age : bob
config :
<< : (( merge ))
value1 : defaultvalue
value2 : defaultvalue
person : (( defaults.person ))
Known optional fields can be described using the undefined ( ~~
) expression:
template.yaml
config :
optional : (( ~~ ))
Such fields will only be part of the final document if they are defined in an upstream stub, otherwise they will be completely removed.
Required fields can be defined with the expression (( merge ))
. If no stub contains a value for this field, the merge cannot be fullfilled and an error is reported. If a dedicated message should be shown instead, the merge expression can be defaulted with an error function call.
p.ej:
template.yaml
config :
password : (( merge || error("the field password is required") ))
will produce the following error if no stub contains a value:
error generating manifest: unresolved nodes:
(( merge || error("the field password is required") )) in c.yaml config.password () *the field password is required
This can be simplified by reducing the expression to the sole error
expression.
Besides this template based defaulting it is also possible to provide defaults by upstream stubs using the &default
marker. Here the payload can be a downstream file.
X509 and providing State
When generating keys or certificates with the X509 Functions there will be new keys or certificates for every execution of spiff . But it is also possible to use spiff to maintain key state. A very simple script could look like this:
#! /bin/bash
DIR= " $( dirname " $0 " ) /state "
if [ ! -f " $DIR /state.yaml " ] ; then
echo " state: " > " $DIR /state.yaml "
fi
spiff merge " $DIR /template.yaml " " $DIR /state.yaml " > " $DIR /. $$ " && mv " $DIR /. $$ " " $DIR /state.yaml "
It uses a template file (containing the rules) and a state file with the actual state as stub. The first time it is executed there is an empty state and the rules are not overridden, therefore the keys and certificates are generated. Later on, only additional new fields are calculated, the state fields already containing values just overrule the dynaml expressions for those fields in the template.
If a re-generation is required, the state file can just be deleted.
A template may look like this:
state/template.yaml
spec :
<< : (( &local ))
ca :
organization : Mandelsoft
commonName : rootca
privateKey : (( state.cakey ))
isCA : true
usage :
- Signature
- KeyEncipherment
peer :
organization : Mandelsoft
commonName : etcd
publicKey : (( state.pub ))
caCert : (( state.cacert ))
caPrivateKey : (( state.cakey ))
validity : 100
usage :
- ServerAuth
- ClientAuth
- KeyEncipherment
hosts :
- etcd.mandelsoft.org
state :
cakey : (( x509genkey(2048) ))
capub : (( x509publickey(cakey) ))
cacert : (( x509cert(spec.ca) ))
key : (( x509genkey(2048) ))
pub : (( x509publickey(key) ))
peer : (( x509cert(spec.peer) ))
The merge then generates a rootca and some TLS certificate signed with this CA.
Generating, Deploying and Accessing Status for Kubernetes Resources
The sync
function offers the possibility to synchronize the template processing with external content. This can also be the output of a command execution. Therefore the template processing can not only be used to generate a deployment manifest, but also for applying this to a target system and retrieving deployment status values for the further processing.
A typical scenario of this kind could be a kubernetes setup including a service of type LoadBalancer . Once deployed it gets assigned status information about the IP address or hostname of the assigned load balancer. This information might be required for some other deployment manifest.
A simple template for such a deployment could like this:
service :
apiVersion : v1
kind : Service
metadata :
annotations :
dns.mandelsoft.org/dnsnames : echo.test.garden.mandelsoft.org
dns.mandelsoft.org/ttl : " 500 "
name : test-service
namespace : default
spec :
ports :
- name : http
port : 80
protocol : TCP
targetPort : 8080
sessionAffinity : None
type : LoadBalancer
deployment :
testservice : (( sync[pipe_uncached(service, "kubectl", "apply", "-f", "-", "-o", "yaml")|value|->defined(value.status.loadBalancer.ingress)] ))
otherconfig :
lb : (( deployment.testservice.status.loadBalancer.ingress ))
Crazy Shit: Graph Analaysis with spiff
It is easy to describe a simple graph with knots and edges (for example for a set of components and their dependencies) just by using a map of lists.
graph :
a :
- b
- c
b : []
c :
- b
- a
d :
- b
e :
- d
- b
Now it would be useful to figure out whether there are dependency cycles or to determine ordered transitive dependencies for a component.
Let's say something like this:
graph :
utilities :
closures : (( utilities.graph.evaluate(graph) ))
cycles : (( utilities.graph.cycles(closures) ))
Indeed, this can be done with spiff. The only thing required is a "small utilities stub" .
utilities :
<< : (( &temporary ))
graph :
_dep : (( |model,comp,closure|->contains(closure,comp) ? { $deps=[], $err=closure [comp]} :($deps=_._deps(model,comp,closure [comp]))($err=sum[deps|[]|s,e|-> length(s) >= length(e.err) ? s :e.err]) { $deps=_.join(map[deps|e|->e.deps]), $err=err} ))
_deps : (( |model,comp,closure|->map[model.[comp]|dep|->($deps=_._dep(model,dep,closure)) { $deps=[dep] deps.deps, $err=deps.err }] ))
join : (( |lists|->sum[lists|[]|s,e|-> s e] ))
min : (( |list|->sum[list|~|s,e|-> s ? e < s ? e :s :e] ))
normcycle : (( |cycle|->($min=_.min(cycle)) min ? sum[cycle|cycle|s,e|->s.[0] == min ? s :(s.[1..] [s.[1]])] :cycle ))
cycle : (( |list|->list ? ($elem=list.[length(list) - 1]) _.normcycle(sum[list|[]|s,e|->s ? s [e] :e == elem ? [e] :s]) :list ))
norm : (( |deps|->{ $deps=_.reverse(uniq(_.reverse(deps.deps))), $err=_.cycle(deps.err) } ))
reverse : (( |list|->sum[list|[]|s,e|->[e] s] ))
evaluate : (( |model|->sum[model|{}|s,k,v|->s { k=_.norm(_._dep(model,k,[]))}] ))
cycles : (( |result|->uniq(sum[result|[]|s,k,v|-> v.err ? s [v.err] :s]) ))
And magically spiff does the work just by calling
spiff merge closure.yaml graph.yaml utilities.yaml
closures :
a :
deps :
- c
- b
- a
err :
- a
- c
- a
b :
deps : []
err : []
c :
deps :
- a
- b
- c
err :
- a
- c
- a
d :
deps :
- b
err : []
e :
deps :
- d
- b
err : []
cycles :
- - a
- c
- a
graph :
a :
- b
- c
b : []
c :
- b
- a
d :
- b
e :
- d
- b
The evaluation of dynaml expressions may fail because of several reasons:
If a dynaml expression cannot be resolved to a value, it is reported by the spiff merge
operation using the following layout:
(( <failed expression> )) in <file> <path to node> (<referred path>) <tag><issue>
(( min_ip("10") )) in source.yml node.a.[0] () *CIDR argument required
Cyclic dependencies are detected by iterative evaluation until the document is unchanged after a step. Nodes involved in a cycle are therefore typically reported just as unresolved node without a specific issue.
The order of the reported unresolved nodes depends on a classification of the problem, denoted by a dedicated tag. The following tags are used (in reporting order):
Etiqueta | Significado |
---|---|
* | error in local dynaml expression |
@ | dependent or involved in cyclic dependencies |
- | subsequent error because of refering to a yaml node with an error |
Problems occuring during inline template processing are reported as nested problems. The classification is propagated to the outer node.
If a problem occurs in nested lamba calls the call stack together with the lamba function and is local binding is listed.
(( 2 + .func(2) )) in local/err.yaml value () *evaluation of lambda expression failed: lambda|x|->x > 0 ? _(x - 1) : *(template): {x: 2}
... evaluation of lambda expression failed: lambda|x|->x > 0 ? _(x - 1) : *(template): {x: 1}
... evaluation of lambda expression failed: lambda|x|->x > 0 ? _(x - 1) : *(template): {x: 0}
... resolution of template 'template' failed
(( z )) in local/err.yaml val ()*'z' not found
In case of parsing errors in dynaml expressions, the error location is shown now. If it is a multi line expression the line a character/symbol number in that line is show, otherwise the line numer is omitted.
((
2 ++ .func(2)
)) in local/err.yaml faulty () *parse error near line 2 symbol 2 - line 2 symbol 3: " "
Spiff provides a Go package ( spiffing
) that can be used to include spiff templates in Go programs.
An example program could look like this:
import (
"fmt"
"math"
"os"
"github.com/mandelsoft/spiff/dynaml"
"github.com/mandelsoft/spiff/spiffing"
)
func func_pow ( arguments [] interface {}, binding dynaml. Binding ) ( interface {}, dynaml. EvaluationInfo , bool ) {
info := dynaml . DefaultInfo ()
if len ( arguments ) != 2 {
return info . Error ( "pow takes 2 arguments" )
}
a , b , err := dynaml . NumberOperands ( arguments [ 0 ], arguments [ 1 ])
if err != nil {
return info . Error ( "%s" , err )
}
_ , i := a .( int64 )
if i {
r := math . Pow ( float64 ( a .( int64 )), float64 ( b .( int64 )))
if float64 ( int64 ( r )) == r {
return int64 ( r ), info , true
}
return r , info , true
} else {
return math . Pow ( a .( float64 ), b .( float64 )), info , true
}
}
var state = `
state: {}
`
var stub = `
unused: (( input ))
ages:
alice: (( pow(2,5) ))
bob: (( alice + 1 ))
`
var template = `
state:
<<<: (( &state ))
random: (( rand("[:alnum:]", 10) ))
ages: (( &temporary ))
example:
name: (( input )) # direct reference to additional values
sum: (( sum[ages|0|s,k,v|->s + v] ))
int: (( pow(2,4) ))
float: 2.1
pow: (( pow(1.1e1,2.1) ))
`
func Error ( err error ) {
if err != nil {
fmt . Fprintf ( os . Stderr , "Error: %s n " , err )
os . Exit ( 1 )
}
}
func main () {
values := map [ string ] interface {}{}
values [ "input" ] = "this is an input"
functions := spiffing . NewFunctions ()
functions . RegisterFunction ( "pow" , func_pow )
spiff , err := spiffing . New (). WithFunctions ( functions ). WithValues ( values )
Error ( err )
pstate , err := spiff . Unmarshal ( "state" , [] byte ( state ))
Error ( err )
pstub , err := spiff . Unmarshal ( "stub" , [] byte ( stub ))
Error ( err )
ptempl , err := spiff . Unmarshal ( "template" , [] byte ( template ))
Error ( err )
result , err := spiff . Cascade ( ptempl , []spiffing. Node { pstub }, pstate )
Error ( err )
b , err := spiff . Marshal ( result )
Error ( err )
newstate , err := spiff . Marshal ( spiff . DetermineState ( result ))
Error ( err )
fmt . Printf ( "==== new state === n " )
fmt . Printf ( "%s n " , string ( newstate ))
fmt . Printf ( "==== result === n " )
fmt . Printf ( "%s n " , string ( b ))
}
Es compatible