_ __ __
___ _ __ (_)/ _|/ _| _ _
/ __| '_ | | |_| |_ _| |_ _| |_
__ |_) | | _| _|_ _|_ _|
|___/ .__/|_|_| |_| |_| |_|
|_|
spiff++
é um fork do spiff que fornece uma extensão compatível para o spiff com base na versão mais recente, oferecendo um rico conjunto de novos recursos ainda não disponíveis no spiff. Todas as correções fornecidas pelo projeto spiff original também serão incorporadas ao spiff++. Como não haverá caminho de volta à base de origem do spiff, um novo repositório spiff++ independente foi criado para continuar o desenvolvimento do spiff++.spiff é uma ferramenta de linha de comando e um sistema de modelagem YAML híbrido declarativo no domínio. Embora os sistemas de modelagem regulares processem um arquivo de modelo substituindo as expressões do modelo por valores obtidos de fontes de dados externas, no domínio significa que o mecanismo de modelagem conhece a sintaxe e a estrutura do modelo processado. Portanto, ele pode obter os valores das expressões de modelo diretamente do documento processado, incluindo as partes indicadas pelas próprias expressões de modelo.
Por exemplo:
resource :
name : bosh deployment
version : 25
url : (( "http://resource.location/bosh?version=" version ))
description : (( "This document describes a " name " located at " url ))
Em vez de usar apenas fontes de valor externas, o spiff fornece um mecanismo de mesclagem para mesclar um modelo com qualquer número de stubs de mesclagem para produzir um documento final.
É uma ferramenta de linha de comando e sistema de modelagem YAML declarativo, especialmente projetado para gerar manifestos de implantação (por exemplo, manifestos BOSH, Kubernetes ou Landscaper).
Além da CLI existe uma biblioteca golang que permite a utilização do processamento do template spiff em qualquer programa GO (por exemplo Landscaper).
O mecanismo de modelagem oferece acesso ao sistema de arquivos com base em um sistema de arquivos virtual configurável ou no sistema de processo para executar comandos e incorporar a saída no processamento do modelo.
Conteúdo:
<<if:
<<switch:
<<type:
<<for:
<<merge:
Binários executáveis de lançamento oficial podem ser baixados por meio de versões do Github para máquinas Darwin, Linux e PowerPC (e máquinas virtuais).
Algumas das dependências do spiff mudaram desde o último lançamento oficial e o spiff não será atualizado para acompanhar essas dependências. Essas dependências são corrigidas ou copiadas na base de código local.
spiff merge template.yml [template2.yml ...]
Mescle vários arquivos de modelo em um manifesto e imprima-o.
Consulte 'linguagem de modelo dynaml' para obter detalhes do arquivo de modelo ou exemplos/subdiretório para exemplos mais complicados.
Exemplo:
spiff merge cf-release/templates/cf-deployment.yml my-cloud-stub.yml
É possível ler um arquivo da entrada padrão usando o nome do arquivo -
. Pode ser usado apenas uma vez. Isso permite usar o spiff como parte de um pipeline para processar apenas um único fluxo ou para processar um fluxo baseado em vários modelos/stubs.
O arquivo de modelo (primeiro argumento) pode ser um fluxo de vários documentos contendo vários documentos YAML separados por uma linha contendo apenas ---
. Cada documento YAML será processado de forma independente com os arquivos stub fornecidos. O resultado é o fluxo de documentos processados na mesma ordem. Se o nó raiz de um documento for marcado como temporário, o documento será omitido do fluxo de saída. Por exemplo, isso pode ser usado para gerar manifestos do kubernetes a serem usados pelo kubectl
.
O comando merge
oferece várias opções:
A opção --partial
. Se esta opção for fornecida, o spiff trata da avaliação de expressões incompletas. Todos os erros são ignorados e as partes não solucionáveis do documento yaml são retornadas como strings.
Com a opção --json
a saída estará no formato JSON em vez de YAML.
A opção --path <path>
pode ser usada para gerar um caminho aninhado, em vez do documento processado completo.
Se a saída for uma lista, a opção --split
gera cada elemento da lista como um documento separado. O formato yaml usa normalmente ---
como linha separadora. O formato JSON gera uma sequência de documentos JSON , um por linha.
Com --select <field path>
é possível selecionar um campo dedicado do documento processado para a saída
Com --evaluate <dynaml expression>
é possível avaliar uma determinada expressão dinâmica no documento processado para a saída. A expressão é avaliada antes da aplicação do caminho de seleção, que atuará no resultado da avaliação.
A opção --state <path>
habilita o suporte estatal de spiff . Se o arquivo fornecido existir, ele será colocado no topo da lista de stubs configurada para o arquivo fornecido, ele será colocado no topo da lista de stubs configurada para o processamento de mesclagem. Além da saída do documento processado, ele é filtrado por nós marcados com o marcador &state
. Este documento filtrado é então armazenado no arquivo indicado, salvando o arquivo de estado antigo com o sufixo .bak
. Isso pode ser usado junto com uma mesclagem manual oferecida pela biblioteca de utilitários estaduais.
Com a opção --bindings <path>
um arquivo yaml pode ser especificado, cujo conteúdo é usado para construir ligações adicionais para o processamento. O documento yaml deve consistir em um mapa. Cada chave é usada como ligação adicional. O documento de vinculações não é processado, os valores são usados conforme definido.
Com a opção --tag <tag>:<path>
pode ser especificado um arquivo yaml, cujo conteúdo é usado como valor para uma tag global predefinida (ver Tags). As tags podem ser acessadas por expressões de referência no formato <tag>::<ref>
. Ao contrário das ligações, o conteúdo marcado não compete com os nós do documento, ele usa outro namespace de referência.
Com a opção --define <key>=<value>
(abreviação -D
) valores de ligação adicionais podem ser especificados na linha de comando, substituindo os valores de ligação do arquivo de ligação. A opção pode ocorrer várias vezes.
Se a chave contiver pontos ( .
), ela será interpretada como uma expressão de caminho para descrever campos em valores profundos do mapa. Um ponto (e um antes de um ponto) pode ser escapado por
para mantê-lo no nome do campo.
A opção --preserve-escapes
preservará o escape para expressões dinaml e diretivas de mesclagem de lista/mapa. Esta opção pode ser usada se forem pretendidas etapas de processamento adicionais de um resultado de processamento com spiff .
A opção --preserve-temporary
preservará os campos marcados como temporários no documento final.
A opção --features=<featurelist>
habilitará esses recursos. Novos recursos incompatíveis com o comportamento antigo devem ser habilitados explicitamente. Normalmente, esses recursos não quebram o comportamento comum, mas introduzem uma interpretação dedicada para valores yaml que eram usados como valores regulares antes.
As bibliotecas de pastas oferecem algumas bibliotecas de utilitários úteis. Eles também podem ser usados como exemplo do poder deste mecanismo de modelagem.
spiff diff manifest.yml other-manifest.yml
Mostre diferenças estruturais entre dois manifestos de implantação. Aqui também são suportados fluxos com vários documentos. Para indicar que não há diferença, o número de documentos em ambos os fluxos deve ser idêntico e cada documento no primeiro fluxo não deve ter diferença em comparação com o documento com o mesmo índice no segundo fluxo. As diferenças encontradas são mostradas para cada documento separadamente.
Ao contrário das ferramentas básicas de comparação e até mesmo bosh diff
, este comando possui conhecimento semântico de um manifesto de implantação e não é apenas baseado em texto. Por exemplo, se dois manifestos são iguais, exceto por terem alguns trabalhos listados em ordens diferentes, spiff diff
detectará isso, uma vez que a ordem de trabalho é importante em um manifesto. Por outro lado, se dois manifestos diferirem apenas na ordem dos seus pools de recursos, por exemplo, então produzirá e esvaziará a diferença, uma vez que a ordem do pool de recursos não importa realmente para uma implantação.
Também diferentemente de bosh diff
, este comando não modifica nenhum dos arquivos.
Ele foi desenvolvido para verificar diferenças entre uma implantação e outra.
Fluxo 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
O subcomando convert
pode ser usado para converter arquivos de entrada para json ou apenas para normalizar a ordem dos campos. As opções disponíveis são --json
, --path
, --split
ou --select
de acordo com seus significados para o subcomando merge
.
spiff encrypt secret.yaml
O subcomando encrypt
pode ser usado para criptografar ou descriptografar dados de acordo com a função encrypt
dynaml. A senha pode ser fornecida como segundo argumento ou obtida da variável de ambiente SPIFF_ENCRYPTION_KEY
. O último argumento pode ser usado para passar o método de criptografia (veja a função encrypt
)
Os dados são retirados do arquivo especificado. Se -
for fornecido, ele será lido no stdin.
Se a opção -d
for fornecida, os dados serão descriptografados, caso contrário, os dados serão lidos como documento yaml e o resultado criptografado será impresso.
Novos recursos incompatíveis com o comportamento antigo devem ser habilitados explicitamente. Normalmente, esses recursos não quebram o comportamento comum, mas introduzem uma interpretação dedicada para valores yaml que eram usados como valores regulares antes e podem, portanto, quebrar os casos de uso existentes.
Os seguintes sinalizadores de recursos são atualmente suportados:
Recurso | Desde | Estado | Significado |
---|---|---|---|
interpolation | 1.7.0-beta-1 | alfa | dinaml como parte de strings yaml |
control | 1.7.0-beta-4 | alfa | estruturas de controle baseadas em yaml |
Sinalizadores de recursos ativos podem ser consultados usando a função dinaml features()
como lista de strings. Se esta função for chamada com um argumento de string, ela retornará se o recurso fornecido está habilitado no momento.
Os recursos podem ser habilitados pela linha de comando usando a opção --features
, pela biblioteca go usando a função WithFeatures
ou geralmente definindo a variável de ambiente SPIFF_FEATURES
para uma lista de recursos. Esta configuração sempre foi usada como padrão. Ao usar as configurações spiff Plain()
da biblioteca go, todas as variáveis de ambiente são ignoradas.
Um recurso pode ser especificado por nome ou por nome acrescentado do prefixo no
para desativá-lo.
A pasta de bibliotecas contém algumas bibliotecas úteis de modelos spiff . Estes são basicamente apenas stubs que são adicionados à lista de arquivos de mesclagem para oferecer funções utilitárias para o processamento de mesclagem.
Spiff usa uma linguagem de modelagem declarativa e sem lógica chamada 'dynaml' (yaml dinâmico).
É garantido que cada nó dinaml seja resolvido para um nó YAML. Não é interpolação de strings. Isso evita que os desenvolvedores pensem em como um valor será renderizado no modelo resultante.
Um nó dinaml aparece no arquivo .yml como uma string denotando uma expressão entre dois parênteses (( <dynaml> ))
. Eles podem ser usados como o valor de um mapa ou como uma entrada em uma lista. A expressão pode abranger várias linhas. Em qualquer caso, o valor da string yaml não deve terminar com uma nova linha (por exemplo, usando |-
)
Se um valor entre parênteses não deve ser interpretado como uma expressão dinâmica e mantido como está na saída, ele pode ser escapado por um ponto de exclamação diretamente após os colchetes de abertura.
Por exemplo, ((! .field ))
mapeia para o valor da string (( .field ))
e ((!! .field ))
mapeia para o valor da string ((! .field ))
.
A seguir está uma lista completa de expressões dinâmicas:
(( foo ))
Procure a chave 'foo' mais próxima (ou seja, escopo léxico) no modelo atual e traga-a.
por exemplo:
fizz :
buzz :
foo : 1
bar : (( foo ))
bar : (( foo ))
foo : 3
bar : (( foo ))
Este exemplo resolverá para:
fizz :
buzz :
foo : 1
bar : 1
bar : 3
foo : 3
bar : 3
O seguinte não será resolvido porque o nome da chave é igual ao valor a ser mesclado:
foo : 1
hi :
foo : (( foo ))
(( foo.bar.[1].baz ))
Procure a chave 'foo' mais próxima e, a partir daí, siga para .bar.[1].baz
.
Um caminho é uma sequência de etapas separadas por pontos. Uma etapa é uma palavra para mapas ou dígitos entre colchetes para indexação de lista. O índice pode ser negativo (um sinal de menos seguido de dígitos). Os índices negativos são obtidos do final da lista (índice efetivo = índice + comprimento (lista)).
Um caminho que não pode ser resolvido leva a um erro de avaliação. Se uma referência às vezes não for fornecida, ela deve ser usada em combinação com '||' (veja abaixo) para garantir a resolução.
Nota : A gramática dinaml foi reformulada para permitir a sintaxe de índice usual agora. Em vez de foo.bar.[1]
agora é possível usar foo.bar[1]
.
Nota : As referências estão sempre dentro do modelo ou esboço e a ordem não importa. Você pode consultar outro nó dinâmico e presumir que ele foi resolvido, e o nó de referência acabará sendo resolvido assim que o nó dependente for resolvido.
por exemplo:
properties :
foo : (( something.from.the.stub ))
something : (( merge ))
Isso será resolvido desde que 'algo' seja solucionável e desde que traga algo assim:
from :
the :
stub : foo
Se o caminho começar com um ponto ( .
), o caminho será sempre avaliado a partir da raiz do documento. Se a raiz do documento for uma lista, o primeiro nível do mapa será usado para resolver a expressão do caminho se ela começar com .__map
. Isso pode ser usado para evitar a necessidade de usar o próprio índice da lista (como .[1].path
), que pode mudar se entradas da lista forem adicionadas.
As entradas da lista que consistem em um mapa com campo name
podem ser endereçadas diretamente por seu valor de nome como componente de caminho.
Nota : Isso também funciona para caminhos absolutos de documentos de lista.
por exemplo:
A era de Alice em
list :
- name : alice
age : 25
pode ser referenciado usando o caminho list.alice.age
, em vez de list[0].age
.
Por padrão, um campo com name
é usado como campo-chave. Se outro campo for usado como campo-chave, ele poderá ser marcado em uma entrada da lista como chave, prefixando o nome do campo com a palavra-chave key:
. Esta palavra-chave é removida pelo processamento e não fará parte do resultado final do processamento.
por exemplo:
list :
- key:person : alice
age : 25
alice : (( list.alice ))
será resolvido
list :
- person : alice
age : 25
alice :
person : alice
age : 25
Este novo campo-chave também será observado durante a fusão das listas.
Se o campo-chave selecionado começar com !
, o recurso principal será desativado. O ponto de exclamação também é removido do nome do campo efetivo.
Se os valores do campo-chave não forem exclusivos, ele também será desativado.
(( foo.[bar].baz ))
Procure a chave 'foo' mais próxima e, a partir daí, siga até o(s) campo(s) descrito(s) pela bar
de expressão e depois até .baz.
O índice pode ser uma constante inteira (sem espaços), conforme descrito na última seção. Mas também pode ser uma expressão dinâmica arbitrária (até mesmo um número inteiro, mas com espaços). Se a expressão for avaliada como uma string, ela pesquisará o campo dedicado. Se a expressão for avaliada como um número inteiro, o elemento da matriz com esse índice será endereçado. O ponto ( .
) na frente do operador de índice é opcional.
por exemplo:
properties :
name : alice
foo : (( values.[name].bar ))
values :
alice :
bar : 42
Isso resolverá foo
para o valor 42
. O índice dinâmico também pode estar no final da expressão (sem .bar
).
Basicamente, esta é a maneira mais simples de expressar algo como eval("values." name ".bar")
Se a expressão for avaliada como uma lista, os elementos da lista (strings ou inteiros) serão usados como elementos de caminho para acessar campos mais profundos.
por exemplo:
properties :
name :
- foo
- bar
foo : (( values.[name] ))
values :
foo :
bar : 42
resolve foo
novamente para o valor 42
.
Nota : O operador de índice também pode ser usado no elemento raiz ( .[index]
).
É possível especificar vários índices separados por vírgula para listas sucessivas ( foo[0][1]
é equivalente a `foo[0,1]). Nesse caso, os índices podem não ser novamente listas.
(( list.[1..3] ))
A expressão de fatia pode ser usada para extrair uma sublista dedicada de uma expressão de lista. O intervalo start ..
end extrai uma lista de comprimento end-start+1 com os elementos do índice start a end . Se o índice inicial for negativo, a fatia será retirada do final da lista de length+start a length+end . Se o índice final for menor que o índice inicial, o resultado será uma matriz vazia.
por exemplo:
list :
- a
- b
- c
foo : (( list.[1..length(list) - 1] ))
O índice inicial ou final pode ser omitido. Em seguida, é selecionado de acordo com o tamanho real da lista. Portanto list.[1..length(list)]
é equivalente a list.[1..]
.
avalia foo
para a lista [b,c]
.
(( 1.2e4 ))
Literaturas numéricas são suportadas para números inteiros e valores de ponto flutuante.
(( "foo" ))
Literal de cadeia de caracteres. Todas as codificações de string json são suportadas (por exemplo n
, "
ou uxxxx
).
(( [ 1, 2, 3 ] ))
Lista literal. Os elementos da lista podem novamente ser expressões. Existe um literal de lista especial [1 .. -1]
, que pode ser usado para resolver um intervalo de números crescente ou decrescente para uma lista.
por exemplo:
list : (( [ 1 .. -1 ] ))
rendimentos
list :
- 1
- 0
- -1
(( { "alice" = 25 } ))
O literal do mapa pode ser usado para descrever mapas como parte de uma expressão dinâmica. Ambos, a chave e o valor, podem novamente ser expressões, em que a expressão chave deve ser avaliada como uma string. Desta forma é possível criar mapas com chaves não estáticas. O operador de atribuição =
foi escolhido em vez do caractere dois pontos regular :
usado no yaml, porque isso resultaria em conflitos com a sintaxe do yaml.
Um literal de mapa pode consistir em qualquer número de atribuições de campo separadas por vírgula ,
por exemplo:
name : peter
age : 23
map : (( { "alice" = {}, name = age } ))
rendimentos
name : peter
age : 23
map :
alice : {}
peter : 23
Outra forma de compor listas baseadas em expressões são as funções makemap
e list_to_map
.
(( ( "alice" = 25 ) alice ))
Qualquer expressão pode ser precedida por qualquer número de literais de escopo explícitos. Um literal de escopo descreve um mapa cujos valores estão disponíveis para resolução de referência relativa da expressão (escopo estático). Ele cria uma ligação local adicional para determinados nomes.
Um literal de escopo pode consistir em qualquer número de atribuições de campo separadas por vírgula ,
A chave, bem como o valor, são fornecidos por expressões, enquanto a expressão chave deve ser avaliada como uma string. Todas as expressões são avaliadas no próximo escopo externo, isso significa que configurações posteriores em um escopo não podem usar configurações anteriores no mesmo literal de escopo.
por exemplo:
scoped : (( ( "alice" = 25, "bob" = 26 ) alice + bob ))
rendimentos
scoped : 51
Um nome de campo também pode ser indicado por um símbolo ( $
name ).
(( foo bar ))
Expressão de concatenação usada para concatenar uma sequência de expressões dinâmicas.
(( "foo" bar ))
Concatenação (onde bar é outra expressão dinâmica). Quaisquer sequências de valores simples (string, inteiro e booleano) podem ser concatenadas, fornecidas por qualquer expressão dinâmica.
por exemplo:
domain : example.com
uri : (( "https://" domain ))
Neste exemplo, uri
será resolvido com o valor "https://example.com"
.
(( [1,2] bar ))
Concatenação de listas como expressão (onde bar é outra expressão dinâmica). Quaisquer sequências de listas podem ser concatenadas, fornecidas por qualquer expressão dinâmica.
por exemplo:
other_ips : [ 10.0.0.2, 10.0.0.3 ]
static_ips : (( ["10.0.1.2","10.0.1.3"] other_ips ))
Neste exemplo, static_ips
resolverá o valor [ 10.0.1.2, 10.0.1.3, 10.0.0.2, 10.0.0.3 ]
.
Se a segunda expressão for avaliada como um valor diferente de uma lista (inteiro, booleano, string ou mapa), o valor será anexado à primeira lista.
por exemplo:
foo : 3
bar : (( [1] 2 foo "alice" ))
produz a lista [ 1, 2, 3, "alice" ]
para bar
.
(( map1 map2 ))
Concatenação de mapas como expressão. Quaisquer sequências de mapas podem ser concatenadas, fornecidas por qualquer expressão dinâmica. Assim, as entradas serão mescladas. As entradas com a mesma chave são substituídas da esquerda para a direita.
por exemplo:
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat : (( foo bar ))
rendimentos
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat :
alice : 24
bob : 26
paul : 27
(( auto ))
Cálculo de valor automático sensível ao contexto.
No atributo 'tamanho' de um pool de recursos, isso significa calcular com base no total de instâncias de todos os trabalhos que se declaram no pool de recursos atual.
por exemplo:
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
Neste caso, o tamanho do conjunto de recursos será definido como '5'.
(( merge ))
Traga o caminho atual dos arquivos stub que estão sendo mesclados.
por exemplo:
foo :
bar :
baz : (( merge ))
Tentará trazer foo.bar.baz
do primeiro stub, ou do segundo, etc., retornando o valor do último stub que o fornece.
Se o valor correspondente não for definido, retornará nulo. Isto então tem a mesma semântica que as expressões de referência; uma mesclagem nula é um modelo não resolvido. Veja ||
.
<<: (( merge ))
Mesclagem de mapas ou listas com o conteúdo do mesmo elemento encontrado em algum stub.
** Atenção ** Esta forma de merge
possui um problema de compatibilidade. Nas versões anteriores a 1.0.8, esta expressão nunca era analisada, apenas a existência da chave <<:
era relevante. Portanto, muitas vezes há usos de <<: (( merge ))
onde <<: (( merge || nil ))
se refere. A primeira variante exigiria conteúdo em pelo menos um stub (como sempre para o operador de mesclagem). Agora esta expressão é avaliada corretamente, mas isso quebraria os conjuntos de modelos de manifesto existentes, que usam a primeira variante, mas significam a segunda. Portanto, este caso é tratado explicitamente para descrever uma mesclagem opcional. Se realmente se pretende uma fusão obrigatória, um qualificador explícito adicional deve
Nota : Em vez de usar um campo de inserção <<:
para colocar expressões de mesclagem, agora é possível usar <<<:
, também, o que permite usar analisadores yaml regulares para documentos yaml do tipo spiff. <<:
é mantido para compatibilidade com versões anteriores. ser usado ( (( merge required ))
).
Se a chave de mesclagem não deve ser interpretada como uma chave normal em vez de uma diretiva de mesclagem, ela pode ser escapada por um ponto de exclamação ( !
).
Por exemplo, uma chave de mapa <<<!
resultará em uma chave de string <<<
e <<<!!
resultará em uma chave de string <<<!
valores.yml
foo :
a : 1
b : 2
modelo.yml
foo :
<< : (( merge ))
b : 3
c : 4
spiff merge template.yml values.yml
produz:
foo :
a : 1
b : 2
c : 4
valores.yml
foo :
- 1
- 2
modelo.yml
foo :
- 3
- << : (( merge ))
- 4
spiff merge template.yml values.yml
produz:
foo :
- 3
- 1
- 2
- 4
- <<: (( merge on key ))
spiff
é capaz de mesclar listas de mapas com um campo-chave. Essas listas são tratadas como mapas com o valor do campo-chave como chave. Por padrão, o name
da chave é usado. Mas com o seletor on
uma chave arbitrária, o nome pode ser especificado para uma expressão de mesclagem de lista.
por exemplo:
list :
- << : (( merge on key ))
- key : alice
age : 25
- key : bob
age : 24
fundido com
list :
- key : alice
age : 20
- key : peter
age : 13
rendimentos
list :
- key : peter
age : 13
- key : alice
age : 20
- key : bob
age : 24
Se nenhuma inserção de novas entradas for desejada (conforme solicitado pela expressão de mesclagem de inserção), mas apenas a substituição de entradas existentes, um campo-chave existente pode ser prefixado com a tag key:
para indicar um nome de chave não padrão, por exemplo - key:key: alice
.
<<: (( merge replace ))
Substitui o conteúdo completo de um elemento pelo conteúdo encontrado em algum stub, em vez de fazer uma mesclagem profunda do conteúdo existente.
valores.yml
foo :
a : 1
b : 2
modelo.yml
foo :
<< : (( merge replace ))
b : 3
c : 4
spiff merge template.yml values.yml
produz:
foo :
a : 1
b : 2
valores.yml
foo :
- 1
- 2
modelo.yml
foo :
- << : (( merge replace ))
- 3
- 4
spiff merge template.yml values.yml
produz:
foo :
- 1
- 2
<<: (( foo ))
Mesclagem de mapas e listas encontrados no mesmo modelo ou esboço.
foo :
a : 1
b : 2
bar :
<< : (( foo )) # any dynaml expression
b : 3
rendimentos:
foo :
a : 1
b : 2
bar :
a : 1
b : 3
Esta expressão apenas adiciona novas entradas à lista real. Ela não mescla entradas existentes com o conteúdo descrito pela expressão de mesclagem.
bar :
- 1
- 2
foo :
- 3
- << : (( bar ))
- 4
rendimentos:
bar :
- 1
- 2
foo :
- 3
- 1
- 2
- 4
Um caso de uso comum para isso é mesclar listas de IPs estáticos ou intervalos em uma lista de IPs. Outra possibilidade é usar uma única expressão de concatenação.
<<: (( merge foo ))
Mesclagem de mapas ou listas com o conteúdo de um elemento arbitrário encontrado em algum stub (Redirecionando mesclagem). Não haverá mais mesclagem (profunda) com o elemento de mesmo nome encontrado em algum stub. (A fusão profunda de listas requer mapas com name
de campo)
O redirecionamento de mesclagens também pode ser usado como valor de campo direto. Eles podem ser combinados com a substituição de mesclagens como (( merge replace foo ))
.
valores.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
modelo.yml
foo :
<< : (( merge bar))
b : 3
c : 4
spiff merge template.yml values.yml
produz:
foo :
a : 1
b : 2
c : 4
Outra maneira de fazer um merge com outro elemento em algum stub também poderia ser feita da maneira tradicional:
valores.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
modelo.yml
bar :
<< : (( merge ))
b : 3
c : 4
foo : (( bar ))
Mas neste cenário a mesclagem ainda executa a mesclagem profunda com o nome do elemento original. Portanto, spiff merge template.yml values.yml
produz:
bar :
a : 1
b : 2
c : 4
foo :
a : 10
b : 20
c : 4
valores.yml
foo :
- 10
- 20
bar :
- 1
- 2
modelo.yml
foo :
- 3
- << : (( merge bar ))
- 4
spiff merge template.yml values.yml
produz:
foo :
- 3
- 1
- 2
- 4
<<: (( merge none ))
Se a referência de uma mesclagem de redirecionamento for definida como a constante none
, nenhuma mesclagem será feita. Essas expressões sempre produzem o valor nulo.
ex.: para
modelo.yml
map :
<< : (( merge none ))
value : notmerged
valores.yml
map :
value : merged
spiff merge template.yml values.yml
produz:
map :
value : notmerged
Isso pode ser usado para mesclagem explícita de campos usando a função stub
para acessar partes dedicadas de stubs upstream.
por exemplo:
modelo.yml
map :
<< : (( merge none ))
value : (( "alice" "+" stub(map.value) ))
valores.yml
map :
value : bob
spiff merge template.yml values.yml
produz:
test :
value : alice+bob
Isso também funciona para campos dedicados:
modelo.yml
map :
value : (( merge none // "alice" "+" stub() ))
valores.yml
map :
value : bob
spiff merge template.yml values.yml
produz:
test :
value : alice+bob
(( a || b ))
Usa a ou b se a não puder ser resolvido.
por exemplo:
foo :
bar :
- name : some
- name : complicated
- name : structure
mything :
complicated_structure : (( merge || foo.bar ))
Isso tentará mesclar em mything.complicated_structure
ou, se não puder ser mesclado, usará o padrão especificado em foo.bar
.
O operador //
verifica adicionalmente se a
pode ser resolvido para um valor válido (diferente de ~
).
(( 1 + 2 * foo ))
Expressões Dynaml podem ser usadas para executar cálculos aritméticos inteiros e de ponto flutuante. As operações suportadas são +
, -
, *
e /
. O operador de módulo ( %
) suporta apenas operandos inteiros.
por exemplo:
valores.yml
foo : 3
bar : (( 1 + 2 * foo ))
spiff merge values.yml
produz 7
para bar
. Isto pode ser combinado com concatenações (o cálculo tem prioridade mais alta que a concatenação em expressões dinaml):
foo : 3
bar : (( foo " times 2 yields " 2 * foo ))
O resultado é a string 3 times 2 yields 6
.
(( "10.10.10.10" - 11 ))
Além da aritmética em números inteiros, também é possível usar adição e subtração em endereços IP e cidrs.
por exemplo:
ip : 10.10.10.10
range : (( ip "-" ip + 247 + 256 * 256 ))
rendimentos
ip : 10.10.10.10
range : 10.10.10.10-10.11.11.1
A subtração também funciona em dois endereços IP ou cidrs para calcular o número de endereços IP entre dois endereços IP.
por exemplo:
diff : (( 10.0.1.0 - 10.0.0.1 + 1 ))
produz o valor 256. As constantes de endereço IP podem ser usadas diretamente em expressões dinâmicas. Eles são convertidos implicitamente em strings e novamente em endereços IP, se exigido por uma operação.
Multiplicação e divisão podem ser usadas para lidar com mudanças de intervalo de IP em CIDRs. Com a divisão, uma rede pode ser particionada. O tamanho da rede é aumentado para permitir pelo menos um número dedicado de sub-redes abaixo do CIDR original. A multiplicação então pode ser usada para obter a enésima próxima sub-rede do mesmo tamanho.
por exemplo:
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
rendimentos
subnet : 10.1.2.0/28
next : 10.1.2.32/28
Além disso, existem funções que funcionam em CIDRs 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") ))
rendimentos
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 suporta os operadores de comparação <
, <=
, ==
, !=
, >=
e >
. Os operadores de comparação funcionam com valores inteiros. As verificações de igualdade também funcionam em listas e mapas. O resultado é sempre um valor booleano. Para negar uma condição, o operador não unário ( !
) pode ser usado.
Além disso, existe o operador condicional ternário ?:
, que pode ser usado para avaliar expressões dependendo de uma condição. O primeiro operando é usado como condição. A expressão é avaliada para o segundo operando, se a condição for verdadeira, e para o terceiro, caso contrário.
por exemplo:
foo : alice
bar : bob
age : 24
name : (( age > 24 ? foo :bar ))
produz o valor bob
para a propriedade name
.
Uma expressão é considerada false
se for avaliada como
false
Caso contrário, é considerado true
Observação
O uso do símbolo :
pode colidir com a sintaxe yaml, se a expressão completa não for um valor de string entre aspas.
Os operadores -or
e -and
podem ser usados para combinar operadores de comparação para compor condições mais complexas.
Observação:
O símbolo de operador mais tradicional ||
(e &&
) não pode ser usado aqui, porque o operador ||
já existe em dinaml com uma semântica diferente, o que não vale para operações lógicas. A expressão false || true
é avaliado como false
, porque produz o primeiro operando, se estiver definido, independentemente de seu valor. Para ser o mais compatível possível, isso não pode ser alterado e os símbolos simples or
e and
não podem ser usados, pois isso invalidaria a concatenação de referências com tais nomes.
(( 5 -or 6 ))
Se ambos os lados de um operador -or
ou -and
forem avaliados como valores inteiros, uma operação bit a bit será executada e o resultado será novamente um número inteiro. Portanto, a expressão 5 -or 6
é avaliada como 7
.
Dynaml oferece suporte a um conjunto de funções predefinidas. Uma função é geralmente chamada como
result : (( functionname(arg, arg, ...) ))
Funções adicionais podem ser definidas como parte do documento yaml usando expressões lambda. O nome da função é então uma expressão agrupada ou o caminho para o nó que hospeda a expressão lambda.
(( format( "%s %d", alice, 25) ))
Formate uma string com base em argumentos fornecidos por expressões dinâmicas. Há um segundo tipo desta função: error
formata uma mensagem de erro e define a avaliação como falha.
(( join( ", ", list) ))
Junte entradas de listas ou valores diretos a um único valor de string usando uma determinada string separadora. Os argumentos para unir podem ser expressões dinâmicas avaliadas em listas, cujos valores novamente são strings ou inteiros, ou strings ou valores inteiros.
por exemplo:
alice : alice
list :
- foo
- bar
join : (( join(", ", "bob", list, alice, 10) ))
produz o valor da string bob, foo, bar, alice, 10
para join
.
(( split( ",", string) ))
Divida uma string para um separador dedicado. O resultado é uma lista. Em vez de uma string separadora, um valor inteiro pode ser fornecido, o que divide a string fornecida em uma lista de strings de comprimento limitado. O comprimento é contado em runas, não em bytes.
por exemplo:
list : (( split("," "alice, bob") ))
limited : (( split(4, "1234567890") ))
rendimentos:
list :
- alice
- ' bob '
limited :
- " 1234 "
- " 5678 "
- " 90 "
Um terceiro argumento opcional pode ser especificado. Limita o número de entradas da lista retornadas. O valor -1 leva a um comprimento de lista ilimitado.
Se uma expressão regular deve ser usada como string separadora, a função split_match
pode ser usada.
(( trim(string) ))
Corte uma string ou todos os elementos de uma lista de strings. Existe um argumento opcional de segunda string. Pode ser usado para especificar um conjunto de caracteres que serão cortados. O conjunto de cortes padrão consiste em um espaço e um caractere de tabulação.
por exemplo:
list : (( trim(split("," "alice, bob")) ))
rendimentos:
list :
- alice
- bob
(( element(list, index) ))
Retorna um elemento de lista dedicado fornecido por seu índice.
por exemplo:
list : (( trim(split("," "alice, bob")) ))
elem : (( element(list,1) ))
rendimentos:
list :
- alice
- bob
elem : bob
(( element(map, key) ))
Retorna um campo de mapa dedicado fornecido por sua chave.
map :
alice : 24
bob : 25
elem : (( element(map,"bob") ))
rendimentos:
map :
alice : 24
bob : 25
elem : 25
Esta função também é capaz de lidar com teclas contendo pontos (.).
(( compact(list) ))
Filtre uma lista omitindo entradas vazias.
por exemplo:
list : (( compact(trim(split("," "alice, , bob"))) ))
rendimentos:
list :
- alice
- bob
(( uniq(list) ))
Uniq fornece uma lista sem duplicatas.
por exemplo:
list :
- a
- b
- a
- c
- a
- b
- 0
- " 0 "
uniq : (( uniq(list) ))
rendimentos para o campo uniq
:
uniq :
- a
- b
- c
- 0
(( contains(list, "foobar") ))
Verifica se uma lista contém um valor dedicado. Os valores também podem ser listas ou mapas.
por exemplo:
list :
- foo
- bar
- foobar
contains : (( contains(list, "foobar") ))
rendimentos:
list :
- foo
- bar
- foobar
contains : true
A função contains
também funciona em strings para procurar substrings ou mapas para procurar uma chave. Nesses casos, o elemento deve ser uma string.
por exemplo:
contains : (( contains("foobar", "bar") ))
produz true
.
(( basename(path) ))
A função basename
retorna o nome do último elemento de um caminho. O argumento pode ser um nome de caminho normal ou uma URL.
por exemplo:
pathbase : (( basename("alice/bob") ))
urlbase : (( basename("http://foobar/alice/bob?any=parameter") ))
rendimentos:
pathbase : bob
urlbase : bob
(( dirname(path) ))
A função dirname
retorna o diretório pai de um caminho. O argumento pode ser um nome de caminho normal ou uma URL.
por exemplo:
pathbase : (( dirname("alice/bob") ))
urlbase : (( dirname("http://foobar/alice/bob?any=parameter") ))
rendimentos:
pathbase : alice
urlbase : /alice
(( parseurl("http://github.com") ))
Esta função analisa uma URL e produz um mapa com todos os elementos de uma URL. A port
Fields, userinfo
e password
são opcionais.
por exemplo:
url : (( parseurl("https://user:[email protected]:443/mandelsoft/spiff?branch=master&tag=v1#anchor") ))
rendimentos:
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") ))
Verifica se uma lista contém um valor dedicado e retorna o índice da primeira correspondência. Os valores também podem ser listas ou mapas. Se nenhuma entrada puder ser encontrada, -1
for retornado.
por exemplo:
list :
- foo
- bar
- foobar
index : (( index(list, "foobar") ))
rendimentos:
list :
- foo
- bar
- foobar
index : 2
O index
de função também trabalha em strings para procurar sub -cordas.
por exemplo:
index : (( index("foobar", "bar") ))
rendimentos 3
.
(( lastindex(list, "foobar") ))
A função lastindex
funciona como index
, mas o índice da última ocorrência é retornado.
A sort
da função pode ser usada para classificar listas inteiras ou strings. A operação de classificação é estável.
por exemplo:
list :
- alice
- foobar
- bob
sorted : (( sort(list) ))
rendimentos para sorted
- alice
- bob
- foobar
Se outros tipos forem classificados, especialmente tipos complexos, como listas ou mapas, ou uma regra de comparação diferente é necessária, uma função de comparação pode ser especificada como um segundo argumento opcional. A função de comparação deve ser uma expressão de lambda, levando dois argumentos. O tipo de resultado deve ser integer
ou bool
indicando se a é menor que b . Se um número inteiro for devolvido, deve ser
por exemplo:
list :
- alice
- foobar
- bob
sorted : (( sort(list, |a,b|->length(a) < length(b)) ))
rendimentos para sorted
- bob
- alice
- foobar
(( replace(string, "foo", "bar") ))
Substitua todas as ocorrências de uma sub -string em uma string por uma string de substituição. Com um quarto argumento inteiro opcional, o número de substituições pode ser limitado (-1 significa ilimitado).
por exemplo:
string : (( replace("foobar", "o", "u") ))
Gosta fuubar
.
Se uma expressão regular deve ser usada como string de pesquisa, a função replace_match
poderá ser usada. Aqui a sequência de pesquisa é avaliada como expressão regular. Pode conata subestões. Essas correspondências podem ser usadas na sequência de substituição
por exemplo:
string : (( replace_match("foobar", "(o*)b", "b${1}") ))
Gosta de fbooar
.
O argumento de substituição também pode ser uma função lambda. Nesse caso, para cada correspondência, a função é chamada para determinar o valor de reposição. O argumento de entrada único é uma lista de correspondências reais de sub -expressão.
por exemplo:
string : (( replace_match("foobar-barfoo", "(o*)b", |m|->upper(m.[1]) "b" ) ))
Gosta de fOObar-barfoo
.
(( substr(string, 1, 2) ))
Extraia uma sequência de stub de uma string, começando de um determinado índice de início até um índice final opcional (exclusivo). Se nenhum índice final receber o sub -Struvt até o final da string for extraído. Ambos os índices podem ser negativos. Nesse caso, eles são retirados do final da string.
por exemplo:
string : " foobar "
end1 : (( substr(string,-2) ))
end2 : (( substr(string,3) ))
range : (( substr(string,1,-1) ))
avalia para
string : foobar
end1 : ar
end2 : bar
range : ooba
(( match("(f.*)(b.*)", "xxxfoobar") ))
Retorna a correspondência de uma expressão regular para um determinado valor da string. A correspondência é uma lista dos valores correspondentes para as sub -expressões contidas na expressão regular. Índice 0 refere -se à correspondência da expressão regular completa. Se o valor da string não corresponder a uma lista vazia for retornada.
por exemplo:
matches : (( match("(f.*)*(b.*)", "xxxfoobar") ))
rendimentos:
matches :
- foobar
- foo
- bar
Um terceiro argumento do tipo inteiro pode ser dado para solicitar uma correspondência múltipla de um máximo de N repetições. Se o valor for negativo, todas as repetições serão relatadas. O resultado é uma lista de todas as correspondências, cada uma no formato descrito acima.
(( keys(map) ))
Determine a lista classificada de chaves usadas em um mapa.
por exemplo:
map :
alice : 25
bob : 25
keys : (( keys(map) ))
rendimentos:
map :
alice : 25
bob : 25
keys :
- alice
- bob
(( length(list) ))
Determine o comprimento de uma lista, um mapa ou um valor de string.
por exemplo:
list :
- alice
- bob
length : (( length(list) ))
rendimentos:
list :
- alice
- bob
length : 2
(( base64(string) ))
A função base64
gera uma codificação base64 de uma determinada string. base64_decode
decodifica uma sequência codificada base64.
por exemplo:
base64 : (( base64("test") ))
test : (( base64_decode(base64)))
avalia para
base54 : dGVzdA==
test : test
Um segundo argumento opcional pode ser usado para especificar o comprimento máximo da linha. Nesse caso, o resultado será uma string de várias linhas.
(( hash(string) ))
O hash
da função gera vários tipos de hashes para a string fornecida. Por padrão, como o hash sha256
é gerado. Um segundo argumento opcional especifica o tipo de hash. Os tipos possíveis são md4
, md5
, sha1
, sha224
, sha256
, sha384
, sha2512
, sha512/224
ou sha512/256
.
Os hashes md5
ainda podem ser gerados pelo Finctio md5(string)
depreciado.
por exemplo:
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") ))
avalia para
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) ))
A função bcrypt
gera um hash de senha BCRYPT para a sequência fornecida usando o fator de custo especificado (padrão em 10, se ausente).
por exemplo:
hash : (( bcrypt("password", 10) ))
avalia para
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
(( bcrypt_check("password", hash) ))
A função bcrypt_check
valida uma senha em relação a um determinado hash bcrypt.
por exemplo:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
avalia para
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : true
(( md5crypt("password") ))
A função md5crypt
gera um hash de senha criptografada do Apache MD5 para a string fornecida.
por exemplo:
hash : (( md5crypt("password") ))
avalia para
hash : $apr1$3Qc1aanY$16Sb5h7U1QrcqwZbDJIYZ0
(( md5crypt_check("password", hash) ))
A função md5crypt_check
valida uma senha em relação a um determinado hash criptografado do Apache MD5.
por exemplo:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
avalia para
hash : $apr1$B77VuUUZ$NkNFhkvXHW8wERSRoi74O1
valid : true
(( decrypt("secret") ))
Esta função pode ser usada para armazenar segredos criptografados em um arquivo YAML SPIFF. O resultado processado conterá o valor descriptografado. Todos os tipos de nós podem ser criptografados e descriptografados, incluindo mapas e listas completos.
A senha para a descriptografia pode ser dada como segundo argumento ou (a maneira preferida) ela pode ser especificada pela variável de ambiente SPIFF_ENCRYPTION_KEY
.
Um último argumento opcional pode selecionar o método de criptografia. O único método suportado até agora é 3DES
. Outros métodos podem ser adicionados para versões SPIFF dedicadas usando o registro do método de criptografia oferecido pela Biblioteca Spiff.
Um valor pode ser criptografado usando a função encrypt("secret")
.
por exemplo:
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) ))
avaliado para 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) ))
A função rand
gera valores aleatórios. O primeiro argumento decide que tipo de valores são solicitados. Sem argumento, gera um número aleatório positivo no intervalo int64
.
tipo de argumento | resultado |
---|---|
interno | Valor inteiro no intervalo [0, n ) para n e ( n , 0] para N negativo n |
bool | valor booleano |
corda | Uma string run, onde a runa está no intervalo de caracteres, qualquer combinação de classes de caracteres ou faixas de caracteres utilizáveis para o Regexp pode ser usada. Se um argumento de comprimento adicional for especificado, a sequência resultante terá o comprimento fornecido. |
por exemplo:
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) ))
avalia para
int : 8037669378456096839
int10 : 7
neg10 : -5
bool : true
string : ghhjAYPMlD
upper : LBZQFRSURL
alnum : 0p6KS7EhAj
punct : ' &{;,^])"(# '
(( type(foobar) ))
O type
de função gera uma string que denota o tipo de expressão fornecida.
por exemplo:
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(~~) ))
Avalia tipos para
types :
- int : int
- float : float
- bool : bool
- string : string
- list : list
- map : map
- lambda : lambda
- template : template
(( defined(foobar) ))
A função defined
verifica se uma expressão pode ser avaliada com sucesso. Ele produz o valor booleano true
, se a expressão puder ser avaliada e false
de outra forma.
por exemplo:
zero : 0
div_ok : (( defined(1 / zero ) ))
zero_def : (( defined( zero ) ))
null_def : (( defined( null ) ))
avalia para
zero : 0
div_ok : false
zero_def : true
null_def : false
Esta função pode ser usada em combinação do operador condicional para avaliar expressões, dependendo da resolução de outra expressão.
(( valid(foobar) ))
A função valid
se uma expressão pode ser avaliada com sucesso e avalia um valor definido não é igual ao nil
. Ele produz o valor booleano true
, se a expressão puder ser avaliada e false
de outra forma.
por exemplo:
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 ) ))
avalia para
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) ))
A função require
um erro se o argumento fornecido for indefinido ou nil
, caso contrário, ele gera o valor fornecido.
por exemplo:
foo : ~
bob : (( foo || "default" ))
alice : (( require(foo) || "default" ))
avalia para
foo : ~
bob : ~
alice : default
(( stub(foo.bar) ))
O stub
de função gera o valor de um campo dedicado encontrado no primeiro stub a montante, definindo -o.
por exemplo:
template.yml
value : (( stub(foo.bar) ))
fundido com stub
stub.yml
foo :
bar : foobar
avalia para
value : foobar
O argumento passado para esta função deve ser uma referência literal ou uma expressão avaliada para uma string que denota uma referência ou uma lista de string que denote a lista de elementos do caminho para a referência. Se nenhum argumento ou um indefinido ( ~~
) for fornecido, o caminho de campo real será usado.
Observe que uma determinada referência exclusiva não será avaliada como expressão, se seu valor deve ser usado, ele deve ser transformado em uma expressão, por exemplo, denotando (ref)
ou [] ref
para uma expressão de lista.
Como alternativa, a operação merge
pode ser usada, por exemplo, merge foo.bar
. A diferença é que stub
não se fundem; portanto, o campo ainda será mesclado (com o caminho original no documento).
(( tagdef("tag", value) ))
A função tagdef
pode ser usada para definir tags dinâmicas (consulte Tags). Em contraste com o marcador de tags, essa função permite especificar o nome da tag e seu valor pretendido por uma expressão. Portanto, ele pode ser usado em elementos de composição como map
ou sum
para criar uma tag dinâmica com valores calculados.
Um terceiro argumento opcional pode ser usado para especificar o escopo pretendido ( local
ou global
). Por padrão, uma tag local é criada. As tags locais são visíveis apenas no nível de processamento real (modelo ou sub), enquanto as tags globais, uma vez definidas, podem ser usadas em todos os níveis de processamento adicionais (stub ou modelo).
Como alternativa, o nome da tag pode ser prefixado com um início ( *
) para declarar uma tag global.
O valor especificado da tag será usado como resultado da função.
por exemplo:
template.yml
value : (( tagdef("tag:alice", 25) ))
alice : (( tag:alice::. ))
avalia para
value : 25
alice : 25
(( eval(foo "." bar ) ))
Avalie o resultado da avaliação de uma expressão de string novamente como expressão de dinaml. Isso pode, por exemplo, ser usado para realizar indiretas.
por exemplo: a expressão em
alice :
bob : married
foo : alice
bar : bob
status : (( eval( foo "." bar ) ))
Calcula o caminho para um campo, que é avaliado novamente para produzir o valor deste campo composto:
alice :
bob : married
foo : alice
bar : bob
status : married
(( env("HOME" ) ))
Leia o valor de uma variável de ambiente cujo nome é dado como expressão de dinaml. Se a variável de ambiente não estiver definida, a avaliação falhará.
Em um segundo sabor, a função env
aceita vários argumentos e/ou listar argumentos, que são unidos a uma única lista. Cada entrada nesta lista é usada como nome de uma variável de ambiente e o resultado da função é um mapa das variáveis fornecidas como elemento YAML. Por meio deste, as variáveis de ambiente inexistentes são omitidas.
(( parse(yamlorjson) ))
Analise uma corda Yaml ou JSON e retorne o conteúdo como valor YAML. Portanto, pode ser usado para avaliação dinâmica adicional.
por exemplo:
json : |
{ "alice": 25 }
result : (( parse( json ).alice ))
produz o valor 25
para o result
do campo.
A parse
da função suporta um segundo argumento opcional, o modo Parse . Aqui, os mesmos modos são possíveis que a função de leitura. O modo de análise padrão é import
, o conteúdo é apenas analisado e não há mais avaliação durante esta etapa.
(( asjson(expr) ))
Esta função transforma um valor YAML dado por seu argumento em uma string json . A função correspondente asyaml
gera o valor YAML como string de documentos YAML .
por exemplo:
data :
alice : 25
mapped :
json : (( asjson(.data) ))
yaml : (( asyaml(.data) ))
resolve
data :
alice : 25
mapped :
json : ' {"alice":25} '
yaml : |+
alice: 25
(( catch(expr) ))
Esta função executa uma expressão e produz algum mapa de informações de avaliação. Sempre é bem -sucedido, mesmo que a expressão falhe. O mapa inclui os seguintes campos:
nome | tipo | significado |
---|---|---|
valid | bool | A expressão é válida |
error | corda | o texto da mensagem de erro da avaliação |
value | qualquer | o valor da expressão, se a avaliação foi bem -sucedida |
por exemplo:
data :
fail : (( catch(1 / 0) ))
valid : (( catch( 5 * 5) ))
resolve
data :
fail :
error : division by zero
valid : false
valid :
error : " "
valid : true
value : 25
(( static_ips(0, 1, 3) ))
Gere uma lista de IPs estáticos para um emprego.
por exemplo:
jobs :
- name : myjob
instances : 2
networks :
- name : mynetwork
static_ips : (( static_ips(0, 3, 4) ))
Isso criará 3 IPs da sub -rede do mynetwork
e retornará duas entradas, pois existem apenas duas instâncias. As duas entradas serão as 0ª e 3ª compensações das faixas de IP estática definidas pela rede.
Por exemplo, dado o arquivo tchau.yml :
networks : (( merge ))
jobs :
- name : myjob
instances : 3
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
e arquivo 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
retorna
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
.
Se tchau.yml era em vez disso
networks : (( merge ))
jobs :
- name : myjob
instances : 2
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
spiff merge bye.yml hi.yml
em vez disso, retorna
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
também aceita argumentos de lista, desde que todos os elementos contidos transitivos sejam novamente listas ou valores inteiros. Isso permite abreviar a lista de IPs da seguinte forma:
static_ips: (( static_ips([1..5]) ))
(( ipset(ranges, 3, 3,4,5,6) ))
Enquanto a função static_ips por razões históricas depende da estrutura de um manifesto do BOSH e funciona apenas em locais dedicados no manifesto, a função IPSet oferece um cálculo semelhante puramente baseado em seus argumentos. Portanto, os intervalos de IP disponíveis e os números necessários de IPs são passados como argumentos.
O primeiro argumento (intervalos) pode ser um único intervalo como uma corda simples ou uma lista de strings. Cada string pode ser
O segundo argumento especifica o número solicitado de endereços IP no conjunto de resultados.
Os argumentos adicionais especificam os índices dos IPs para escolher (a partir de 0) nos intervalos fornecidos. Aqui, novamente, listas de índices podem ser usadas.
por exemplo:
ranges :
- 10.0.0.0 - 10.0.0.255
- 10.0.2.0/24
ipset : (( ipset(ranges,3,[256..260]) ))
Resolve o IPSet para [ 10.0.2.0, 10.0.2.1, 10.0.2.2 ]
.
Se nenhum índices IP for especificado (apenas dois argumentos), os IPs serão escolhidos a partir do início do primeiro intervalo até o final do último intervalo dado, sem indireção.
(( list_to_map(list, "key") ))
Uma lista de entradas de mapa com nome explícito/campos de chave será mapeado para um mapa com as teclas dedicadas. Por padrão, o name
do campo principal é usado, que pode ser alterado pelo segundo argumento opcional. Um campo -chave explicitamente denotado na lista também será levado em consideração.
por exemplo:
list :
- key:foo : alice
age : 24
- foo : bob
age : 30
map : (( list_to_map(list) ))
será mapeado para
list :
- foo : alice
age : 24
- foo : bob
age : 30
map :
alice :
age : 24
bob :
age : 30
Em combinação com modelos e expressões de lambda, isso pode ser usado para gerar mapas com valores -chave denominados arbitrariamente, embora as expressões dinaml não sejam permitidas para valores -chave.
(( makemap(fieldlist) ))
Neste sabor, makemap
cria um mapa com entradas descritas pela lista de campo fornecida. Espera -se que a lista contenha mapas com a key
e value
das entradas, descrevendo entradas de mapa dedicadas.
por exemplo:
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map : (( makemap(list) ))
rendimentos
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map :
" 5 " : 25
alice : 24
bob : 25
Se o valor da chave for um booleano ou um número inteiro, ele será mapeado para uma string.
(( makemap(key, value) ))
Nesse sabor, makemap
cria um mapa com entradas descritas pelos pares de argumentos fornecidos. Os argumentos podem ser uma sequência de pares de chave/valores (fornecidos por argumentos separados).
por exemplo:
map : (( makemap("peter", 23, "paul", 22) ))
rendimentos
map :
paul : 22
peter : 23
Em contraste com o sabor anterior makemap
, este também pode ser tratado pelos literais do mapa.
(( merge(map1, map2) ))
Além da palavra -chave merge
há também uma função chamada merge
(ela deve sempre ser seguida por um suporte de abertura). Ele pode ser usado para mesclar mapas severais retirados do documento real análogo ao processo de mesclagem de stub. Se os mapas forem especificados por expressões de referência, eles não poderão conter expressões dinaml , porque sempre são avaliadas no contexto do documento real antes de avaliar os argumentos.
por exemplo:
map1 :
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
resolve result
em
result :
alice : 26
bob : 24 # <---- expression evaluated before mergeing
Alternativamente, os modelos de mapa podem ser passados (sem operador de avaliação!). Nesse caso, as expressões dinaml do modelo são avaliadas enquanto mesclava os documentos fornecidos como chamadas regulares da fusão do SPIFF .
por exemplo:
map1 :
<< : (( &template ))
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
resolve result
em
result :
alice : 26
bob : 26
Um mapa também pode ser dado por uma expressão de mapa. Aqui é possível especificar expressões dinaml usando a sintaxe usual:
por exemplo:
map1 :
alice : 24
bob : 25
map2 :
alice : 26
peter : 8
result : (( merge(map1, map2, { "bob"="(( carl ))", "carl"=100 }) ))
resolve result
em
result :
alice : 26
bob : 100
Em vez de vários argumentos, um argumento de uma única lista pode ser fornecida. A lista deve conter os mapas a serem mesclados.
As mescladas aninhadas têm acesso a todas as ligações externas. As referências relativas são pesquisadas pela primeira vez no documento real. Se eles não forem encontrados, todas as ligações externas são usadas para procurar a referência, das ligações internas para externas. Além disso, o contexto ( __ctx
) oferece um campo OUTER
, que é uma lista de todos os documentos externos das mescladas aninhadas, que podem ser usadas para procurar referências absolutas.
por exemplo:
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) ))
resolve merged
para
merged :
bob : 25
outer1 :
alice :
age : 24
outer2 : 24
sum : 49
(( intersect(list1, list2) ))
A função intersect
cruza várias listas. Uma lista pode conter entradas de qualquer tipo.
por exemplo:
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) ))
resolve intersect
intersect :
- - a
- a
- { a: b }
- 0
- " 0 "
(( reverse(list) ))
A função reverse
reverte a ordem de uma lista. A lista pode conter entradas de qualquer tipo.
por exemplo:
list :
- - a
- b
- { a: b }
- { b: c }
- 0
- 1
reverse : (( reverse(list) ))
resolve reverse
para
reverse :
- 1
- 0
- { b: c }
- { a: b }
- b
- - a
(( validate(value,"dnsdomain") ))
A função validate
valida uma expressão usando um conjunto de validadores. O primeiro argumento é o valor para validar e todos os outros argumentos são validadores que devem ter sucesso para aceitar o valor. Se pelo menos um validador falhar, é gerada uma mensagem de erro apropriada que explique o motivo de falha.
Um validador é indicado por uma string ou uma lista que contém o tipo de validador como string e seus argumentos. Um validador pode ser negado com um precedente !
em seu nome.
Os seguintes validadores estão disponíveis:
Tipo | Argumentos | Significado |
---|---|---|
empty | nenhum | Lista vazia, mapa ou string |
dnsdomain | nenhum | Nome do domínio DNS |
wildcarddnsdomain | nenhum | Nome de domínio DNS do Wild Cardard |
dnslabel | nenhum | rótulo DNS |
dnsname | nenhum | domínio DNS ou domínio curinga |
ip | nenhum | endereço IP |
cidr | nenhum | cidra |
publickey | nenhum | Chave pública no formato PEM |
privatekey | nenhum | Chave privada no formato PEM |
certificate | nenhum | Certificado em formato PEM |
ca | nenhum | Certificado para CA. |
semver | Lista opcional de restrições | Valide a versão semver contra restrições |
type | Lista de teclas de tipo aceitas | Pelo menos uma chave de tipo deve corresponder |
valueset | Liste o argumento com valores | valores possíveis |
value ou = | valor | Verifique o valor dedicado |
gt ou > | valor | maior que (número/string) |
lt ou < | valor | menor que (número/string) |
ge ou >= | valor | maior ou igual a (número/string) |
le ou <= | valor | Menos ou igual a (número/string) |
match ou ~= | expressão regular | Valor da string Combinagem de expressão regular |
list | Lista opcional de validadores de entrada | é a lista e as entradas correspondem aos validadores |
map | [[<Key Validator>,] <Validador de entrada>] | é mapa e chaves e entradas correspondem aos validadores dados |
mapfield | <nome do campo> [, <Dalidator>] | Entrada necessária no mapa |
optionalfield | <nome do campo> [, <Dalidator>] | entrada opcional no mapa |
and | Lista de validadores | Todos os validadores devem ter sucesso |
or | Lista de validadores | Pelo menos um validador deve ter sucesso |
not ou ! | validador | negar o (s) argumento (s) validador |
Se a validação for bem -sucedida, o valor será retornado.
por exemplo:
dnstarget : (( validate("192.168.42.42", [ "or", "ip", "dnsdomain" ]) ))
avalia para
dnstarget : 192.168.42.42
Se a validação falhar, um erro explicando o motivo de falha é gerado.
por exemplo:
dnstarget : (( validate("alice+bob", [ "or", "ip", "dnsdomain" ]) ))
produz o seguinte erro:
*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])?)*')])
Um validador também pode ser uma expressão de lambda, levando pelo menos um argumento e retornando um valor booleano. Dessa forma, é possível fornecer aos próprios validadores como parte do documento YAML.
por exemplo:
val : (( validate( 0, |x|-> x > 1 ) ))
Se mais de um parâmetro for declarado, os argumentos adicionais devem ser especificados como argumentos do validador. O primeiro argumento é sempre o valor a verificar.
por exemplo:
val : (( validate( 0, [|x,m|-> x > m, 5] ) ))
A função Lambda pode retornar uma lista com 1, 2 ou 3 elementos também. Isso pode ser usado para fornecer mensagens apropriadas.
Índice | Significado |
---|---|
0 | O primeiro índice sempre é o resultado da correspondência, ele deve ser avaliado como booleano |
1 | Se dois elementos forem dados, o segundo índice é a mensagem que descreve o resultado real |
2 | Aqui o índice 1 diminui a mensagem de sucesso e 2 a mensagem de falha |
por exemplo:
val : (( validate( 6, [|x,m|-> [x > m, "is larger than " m, "is less than or equal to " m], 5] ) ))
Só para mencionar, a especificação do validador pode ser fornecida em linha, conforme mostrado nos exemplos acima, mas também como expressões de referência. Os validadores not
and
/ or
aceitam as especificações do validador profundamente aninhadas.
por exemplo:
dnsrecords :
domain : 1.2.3.4
validator :
- map
- - or # key validator
- dnsdomain
- wildcarddnsdomain
- ip # entry validator
val : (( validate( map, validator) ))
(( check(value,"dnsdomain") ))
A check
da função pode ser usada para corresponder a uma estrutura YAML com um verificador de valor baseado em YAML. Por meio deste, a mesma descrição de verificação já descrita para validar pode ser usada. O resultado da chamada é um valor booleano que indica o resultado da correspondência. Não falha se a verificação falhar.
(( error("message") ))
O error
da função pode ser usado para causar falhas explícitas de avaliação com uma mensagem dedicada.
Isso pode ser usado, por exemplo, para reduzir um erro complexo de processamento para uma mensagem significativa, anexando a função de erro como padrão para a expressão com que potencialmente falha.
por exemplo:
value : (( <some complex potentially failing expression> || error("this was an error my friend") ))
Outro cenário pode estar omitindo uma mensagem descritiva para a falta de campos necessários usando um valor de expressão de erro como (padrão) para um campo destinado a ser definido em um stub a montante.
Dynaml suportam várias funções matemáticas:
Inteiros que retornam: ceil
, floor
, round
e roundtoeven
retornando carros alegóricos ou inteiros: abs
retornando carros alegóricos: sin
, cos
, sinh
, cosh
, asin
, acos
, asinh
, acosh
, sqrt
, exp
, log
, log10
,
O Dynaml suporta várias conversões de tipo entre valores integer
, float
, bool
e string
pelas funções apropriadas.
por exemplo:
value : (( integer("5") ))
converte uma string em um valor inteiro.
A conversão de um número inteiro em uma string aceita um argumento inteiro adicional opcional para especificar a base para conversão, por exemplo, string(55,2)
resultará em "110111"
. A base padrão é 10. A base deve estar entre 2 e 36.
O SPIFF suporta acesso ao conteúdo fora do modelo e sub -arquivos. É possível ler arquivos, executar comandos e pipelines. Todas essas funções existem em dois sabores.
sync
for usada, que se destina a sincronizar o processamento do modelo com um estado dedicado (fornecido por conteúdo externo). Aqui, as operações de cache não seriam úteis; portanto, há um segundo sabor não incapacitado. Cada função está disponível com o sufixo _uncached
(por exemplo, read_uncached()
) (( read("file.yml") ))
Leia um arquivo e retorne seu conteúdo. Há suporte para três tipos de conteúdo: arquivos yaml
, arquivos text
e arquivos binary
. A leitura no modo binário resultará em uma string multi-linha codificada Base64.
Se o sufixo do arquivo for .yml
, .yaml
ou .json
, por padrão, o tipo YAML será usado. Se o arquivo deve ser lido como text
, esse tipo deve ser especificado explicitamente. Em todos os outros casos, o padrão é text
, portanto, ler um arquivo binário (por exemplo, um arquivo) exige urgentemente especificar o modo binary
.
Um segundo parâmetro opcional pode ser usado para especificar explicitamente o tipo de retorno desejado: yaml
ou text
. Para documentos da YAML , alguns tipos adicionais são suportados: multiyaml
, template
, templates
, import
e importmulti
.
Um documento YAML será analisado e a árvore será devolvida. Os elementos da árvore podem ser acessados por expressões regulares de dinaml.
Além disso, o arquivo YAML pode novamente conter expressões dinaml. Todas as expressões de dinaml incluídas serão avaliadas no contexto da expressão de leitura. Isso significa que o mesmo arquivo incluído em locais diferentes em um documento YAML pode resultar em diferentes sub -árvores, dependendo das expressões dinaml usadas.
Se for possível ler também uma YAML de vários documentos. Se o tipo multiyaml
for fornecido, um nó de lista com os nós raiz do documento YAML será retornado.
O documento YAML ou JSON também pode ler como modelo especificando o template
de tipo. Aqui, o resultado será um valor de modelo, que pode ser usado como modelos em linha regulares. Se templates
forem especificados, um multi-documento será mapeado para uma lista de modelos.
Se o tipo de leitura estiver definido para import
, o conteúdo do arquivo será lido como documento YAML e o nó raiz será usado para substituir a expressão. As expressões dinaml potenciais contidas no documento não serão avaliadas com a ligação real da expressão juntamente com a chamada de leitura, mas como seria parte do arquivo original. Portanto, esse modo só pode ser usado, se não houver processamento adicional do resultado da leitura ou os valores entregues não são processados.
Isso pode ser usado em conjunto com uma referência acorrentada (para o exame (( read(...).selection ))
) para deletar um fragmento dedicado do documento importado. Em seguida, a avaliação será feita apenas para a parte selecionada, apenas. Expressões e referências nas outras partes não são avaliadas e não podem levar a erros.
por exemplo:
template.yaml
ages :
alice : 25
data : (( read("import.yaml", "import").first ))
Import.yaml
first :
age : (( ages.alice ))
second :
age : (( ages.bob ))
não falhará, porque a second
seção nunca é avaliada.
Esse modo deve ser tomado com cautela, porque geralmente leva a resultados inesperados.
O tipo de leitura importmulti
pode ser usado para importar arquivos YAML de vários documentos como uma lista de nós.
Um documento de texto será retornado como string única.
É possível ler também documentos binários. O conteúdo não pode ser usado como uma string (ou documento YAML), diretamente. Portanto, o binary
do modo de leitura deve ser especificado. O conteúdo é retornado como um valor de string múltiplo codificado BASE64.
(( exec("command", arg1, arg2) ))
Executar um comando. Os argumentos podem ser quaisquer expressões de dinaml, incluindo expressões de referência avaliadas em listas ou mapas. Listas ou mapas são passados como argumentos únicos que contêm um documento YAML com o fragmento fornecido.
O resultado é determinado analisando a saída padrão do comando. Pode ser um documento YAML ou uma única string ou valor inteiro de várias linhas. Um documento YAML deve começar com o prefixo do documento ---
. Se o comando falhar, a expressão é tratada como indefinida.
por exemplo
arg :
- a
- b
list : (( exec( "echo", arg ) ))
string : (( exec( "echo", arg.[0] ) ))
rendimentos
arg :
- a
- b
list :
- a
- b
string : a
exec
alternativo pode ser chamado com um único argumento de lista descrevendo completamente a linha de comando.
O mesmo comando será executado uma vez, apenas, mesmo que seja usado em várias expressões.
(( pipe(data, "command", arg1, arg2) ))
Execute um comando e alimente sua entrada padrão com dados dedicados. O argumento do comando deve ser uma string. Os argumentos para o comando podem ser quaisquer expressões de dinaml, incluindo expressões de referência avaliadas em listas ou mapas. Listas ou mapas são passados como argumentos únicos que contêm um documento YAML com o fragmento fornecido.
O fluxo de entrada é gerado a partir dos dados fornecidos. Se este é um tipo simples, sua representação de string será usada. Caso contrário, um documento YAML será gerado a partir dos dados de entrada. O resultado é determinado analisando a saída padrão do comando. Pode ser um documento YAML ou uma única string ou valor inteiro de várias linhas. Um documento YAML deve começar com o prefixo do documento ---
. Se o comando falhar, a expressão é tratada como indefinida.
por exemplo
data :
- a
- b
list : (( pipe( data, "tr", "a", "z") ))
rendimentos
arg :
- a
- b
list :
- z
- b
Alternativamente, pipe
pode ser chamado com dados e um argumento de lista descrevendo completamente a linha de comando.
O mesmo comando será executado uma vez, apenas, mesmo que seja usado em várias expressões.
(( write("file.yml", data) ))
Escreva um arquivo e retorne seu conteúdo. Se o resultado puder ser analisado como documento YAML, o documento será retornado. Um terceiro argumento opcional pode ser usado para passar nas opções de gravação. Os argumentos da opção podem ser um número inteiro denotando permissões de arquivo (o padrão é 0644
) ou uma string separada por vírgula com opções. As opções suportadas são
binary
: os dados são base64 decodificados antes de escrever0
está indicando um valor octal. (( tempfile("file.yml", data) ))
Escreva o arquivo temporário AA e retorne seu nome de caminho. Um terceiro argumento opcional pode ser usado para passar as opções de gravação. Basicamente se comporta como write
ATENÇÃO : Um arquivo temporário existe apenas durante o processamento de mesclagem. Ele será excluído depois.
Pode ser usado, por exemplo, para fornecer um argumento de arquivo temporário para a função exec
.
(( lookup_file("file.yml", list) ))
Pesquise um arquivo é uma lista de diretórios. O resultado é uma lista de arquivos existentes. Com lookup_dir
, é possível procurar um diretório.
Se nenhum arquivo existente puder ser encontrado, a lista vazia será retornada.
É possível passar em vários argumentos da lista ou string para compor o caminho de pesquisa.
(( mkdir("dir", 0755) ))
Crie um diretório e todos os seus diretórios intermediários, se ainda não existirem.
A peça de permissão é opcional (padrão 0755). O caminho do diretório pode ser dado por valor como valor ou como uma lista de componentes do caminho.
(( list_files(".") ))
Listar arquivos em um diretório. O resultado é uma lista de arquivos existentes. Com list_dirs
, é possível listar diretórios.
(( archive(files, "tar") ))
Crie um arquivo do tipo fornecido (padrão é tar
) contendo os arquivos listados. O resultado é o arquivo codificado base64.
Os tipos de arquivo suportados são tar
e targz
.
files
podem ser uma lista ou mapa de entradas de arquivo. No caso de um mapa, a chave do mapa é usada como padrão para o caminho do arquivo. Uma entrada de arquivo é um mapa com os seguintes campos:
campo | tipo | significado |
---|---|---|
path | corda | Opcional para mapas, o caminho do arquivo no arquivo, inadimplente pela chave do mapa |
mode | int ou int string | Modo de arquivo ou opções de gravação. Basicamente, se comporta como o argumento da opção para write . |
data | qualquer | Conteúdo do arquivo, a YAML será organizada como documento YAML. Se mode indicar o modo binário, um valor de sequência será decodificado de base64. |
base64 | corda | Base64 Dados binários codificados |
por exemplo:
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"))) ))
rendimentos:
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
O Spiff suporta o manuseio de nomes de versão semântica. Ele suporta todas as funcionalidades das versões do Pacote Semver Mindminds que aceitam versões com ou sem um v
líder.
(( semver("v1.2-beta.1") ))
Verifique se uma determinada string é uma versão semântica e retorne seu formulário normalizado (sem liderar a peça de liberação v
e completa com o número da versão maior, menor e de patch).
por exemplo:
normalized : (( semver("v1.2-beta.1") ))
resolve
normalized : 1.2.0-beta.1
(( semverrelease("v1.2.3-beta.1") ))
Retorne a parte de liberação de uma versão semântica omitindo metadados e informações de pré -lançamento.
por exemplo:
release : (( semverrelease("v1.2.3-beta.1") ))
resolve
release : v1.2.3
Se um argumento adicional de string receber, essa função substituir a versão pela versão da versão semântica fornecida, preservando os metadados e as informações de pré -lançamento.
por exemplo:
new : (( semverrelease("1.2.3-beta.1", "1.2.1) ))
resolve
new : 1.2.1-beta.1
(( semvermajor("1.2.3-beta.1") ))
Determine o número principal da versão da versão semântica fornecida. O resultado é um número inteiro.
por exemplo:
major : (( semvermajor("1.2.3-beta.1") ))
resolve
major : 1
A função semverincmajor
pode ser usada para incrementar o número da versão principal e redefinir a versão menor, a versão de patch e liberar sufixos.
por exemplo:
new : (( semverincmajor("1.2.3-beta.1") ))
resolve
new : 2.0.0
(( semverminor("1.2.3-beta.1") ))
Determine o número da versão menor da versão semântica fornecida. O resultado é um número inteiro.
por exemplo:
minor : (( semverminor("1.2.3-beta.1") ))
resolve
minor : 2
A função semverincminor
pode ser usada para incrementar o número da versão menor e redefinir a versão do patch e liberar sufixos.
por exemplo:
new : (( semverincmajor("v1.2.3-beta.1") ))
resolve
new : v1.3.0
(( semverpatch("1.2.3-beta.1") ))
Determine o número da versão do patch da versão semântica fornecida. O resultado é um número inteiro.
por exemplo:
patch : (( semverpatch("1.2.3-beta.1") ))
resolve
patch : 3
A função semverincpatch
pode ser usada para incrementar o número da versão do patch ou redefinir os sufixos de liberação. Se houver sufixos rlease, eles serão removidos e as informações de liberação serão mantidas inalteradas, caso contrário, o número da versão do patch será aumentado.
por exemplo:
final : (( semverincpatch("1.2.3-beta.1") ))
new : (( semverincpatch(final) ))
resolve
final : 1.2.3
new : 1.2.4
(( semverprerelease("1.2.3-beta.1") ))
Determine o pré -lançamento da versão semântica fornecida. O resultado é uma string.
por exemplo:
prerelease : (( semverprerelease("1.2.3-beta.1") ))
resolve
prerelease : beta.1
Se um argumento adicional de string for dado esta função definir, substituir ou limpar (se definido como esvaziar string) o pré -lançamento
por exemplo:
new : (( semverprerelease("1.2.3-beta.1", "beta.2) ))
resolve
new : 1.2.3-beta.2
(( semvermetadata("1.2.3+demo") ))
Determine os metadados da versão semântica fornecida. O resultado é uma string.
por exemplo:
metadata : (( semvermetadata("1.2.3+demo") ))
resolve
metadata : demo
Se um argumento de string adicional for fornecido, essa função definir, substituir ou limpar (se definido como esvaziar a string) os metadados.
por exemplo:
new : (( semvermetadata("1.2.3-test", "demo) ))
resolve
new : 1.2.3+demo
(( semvercmp("1.2.3", 1.2.3-beta.1") ))
Compare duas versões semânticas. Um pré -lançamento é sempre menor que o lançamento final. O resultado é um número inteiro com os seguintes valores:
resultado | significado |
---|---|
-1 | A primeira versão é antes da segunda versão |
0 | Ambas as versões são iguais |
1 | O primeiro versuon é depois do segundo |
por exemplo:
compare : (( semvercmp("1.2.3", "1.2.3-beta.1") ))
resolve
compare : 1
(( semvermatch("1.2.3", "~1.2") ))
Combine a versão semântica fornecida com uma lista de contragens. O resultado é um booleano. É possível especificar qualquer número de restrições de versão. Se nenhuma restrição for dada, a função apenas verifica se a string fornecida é uma versão semântica.
por exemplo:
match : (( semvermatch("1.2.3", "~1.2") ))
resolve
match : true
A lista completa de possíveis especificações de restrições pode ser encontrada aqui.
(( semversort("1.2.3", "1.2.1") ))
Classifique uma lista de versões em ordem crescente. Um v
líder é preservado.
por exemplo:
sorted : (( semversort("1.2.3", "1.2.1") ))
resolve
sorted :
- 1.2.1
- 1.2.3
A lista de versões a serem classificadas também pode ser especificada com um único argumento da lista.
O SPIFF suporta algumas funções úteis para trabalhar com certificados e chaves x509 . Consulte também a seção útil para encontrar algumas dicas para o fornecimento de estado.
(( x509genkey(spec) ))
Esta função pode ser usada gerar teclas privadas RSA ou ECDSA. O resultado será uma chave codificada PEM como valor de string de linha múltipla. Se um tamanho de chave (número inteiro ou string) for dado como argumento, uma chave RSA será gerada com o tamanho da chave fornecido (por exemplo 2048). Dado um dos valores da string
A função gerará uma chave ECDSA apropriada.
por exemplo:
keys :
key : (( x509genkey(2048) ))
resolve 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 uma determinada chave ou certificado no formato PEM (por exemplo, gerado com a função X509GenKey), esta função extrai a chave pública e a retorna novamente no formato PEM como uma string de várias linhas.
por exemplo:
keys :
key : (( x509genkey(2048) ))
public : (( x509publickey(key)
resolve 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 gerar uma chave pública SSH, um argumento de formato adicional opcional pode ser definido como ssh
. O resultado será um formato de chave pública regular utilizável para o SSH. O formato padrão é pem
fornecendo o formato de saída PEM mostrado acima.
As teclas RSA são, por padrão, o formato PKCS#1 ( RSA PUBLIC KEY
) no PEM. Se o formato PKIX genérico ( PUBLIC KEY
) for necessária, o argumento do formato pkix
deverá ser fornecido.
Usando o formato ssh
essa função também pode ser usada para converter uma chave pública formatada PEM em uma chave SSH,
(( x509cert(spec) ))
A função x509cert
cria certificados assinados localmente, um auto -assinado ou um certificado assinado por uma determinada ca. Ele retorna um certificado codificado do PEM como um valor de string de várias linhas.
O parâmetro de especificação única pega um mapa com alguns campos opcionais e não opcionais usados para especificar as informações do certificado. Pode ser uma expressão de mapa em linha ou qualquer referência de mapa no restante do documento YAML.
Os seguintes campos de mapa são observados:
Nome do campo | Tipo | Obrigatório | Significado |
---|---|---|---|
commonName | corda | opcional | Nome comum Campo do assunto |
organization | string ou string lista | opcional | Campo da organização do assunto |
country | string ou string lista | opcional | Campo de campo do assunto |
isCA | bool | opcional | Opção de certificado da CA |
usage | string ou string lista | obrigatório | Chaves de uso para o certificado (veja abaixo) |
validity | inteiro | opcional | intervalo de validade em horas |
validFrom | corda | opcional | Hora de início no formato "1 de janeiro 01:22:31 2019" |
hosts | string ou string lista | opcional | Lista de nomes de DNS ou endereços IP |
privateKey | corda | exigido ou publicKey | chave privada para gerar o certificado para |
publicKey | corda | exigido ou privateKey | chave pública para gerar o certificado para |
caCert | corda | opcional | certificado para assinar com |
caPrivateKey | corda | opcional | Chave Priavte para caCert |
Para certificados autoassinados, o campo privateKey
deve ser definido. publicKey
e os campos ca
devem ser omitidos. Se o campo caCert
for fornecido, o campo caKey
também será necessário. Se o campo privateKey
for fornecido junto com o caCert
, a chave pública para o certificado será extraída da chave privada.
Campos adicionais são silenciosamente ignorados.
As seguintes chaves de uso são suportadas (o caso é ignorado):
Chave | Significado |
---|---|
Signature | x509.KeyUsagedIGITalSignature |
Commitment | X509.KeyusageContentComitment |
KeyEncipherment | x509.KeyUSAGEKEYENCIPERMENT |
DataEncipherment | x509.KeyUsagedataEnciplement |
KeyAgreement | x509.KeyUsageKeyGreement |
CertSign | x509.KeyusageCertSign |
CRLSign | x509.KeyusageCrlSign |
EncipherOnly | x509.KeyUsageEncipleronly |
DecipherOnly | x509.KeySagedEcipheronly |
Any | x509.ExtKeyusageany |
ServerAuth | x509.ExtKeyUSageserverauth |
ClientAuth | x509.ExtKeyusageClientAuth |
codesigning | x509.ExtKeyusageCodesigning |
EmailProtection | X509.ExtKeyUSageEmailProtection |
IPSecEndSystem | x509.ExtKeyUSAGEIPECENDSYSTEM |
IPSecTunnel | x509.ExtKeyUSAGEIPSectUnnel |
IPSecUser | x509.ExtKeyUSAGEIPSECUSER |
TimeStamping | x509.ExtKeyusageTimestamping |
OCSPSigning | x509.ExtKeyUSAGEOCSPSIGNING |
MicrosoftServerGatedCrypto | x509.ExtKeyusagemicrosOftServergatedCrypto |
NetscapeServerGatedCrypto | x509.ExtKeyusageNetScapeServergatedCrypto |
MicrosoftCommercialCodeSigning | x509.ExtKeyusagemicrosoftComerCialCodesigning |
MicrosoftKernelCodeSigning | x509.ExtKeyusagemicrosoftkernelCodesigning |
por exemplo:
spec :
<< : (( &local ))
ca :
organization : Mandelsoft
commonName : Uwe Krueger
privateKey : (( data.cakey ))
isCA : true
usage :
- Signature
- KeyEncipherment
data :
cakey : (( x509genkey(2048) ))
cacert : (( x509cert(spec.ca) ))
gera um certificado de raiz autoassinado e resolve 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 função analisa um certificado fornecido no formato PEM e retorna um mapa de campos:
Nome do campo | Tipo | Obrigatório | Significado |
---|---|---|---|
commonName | corda | opcional | Nome comum Campo do assunto |
organization | Lista de string | opcional | Campo da organização do assunto |
country | Lista de string | opcional | Campo de campo do assunto |
isCA | bool | sempre | Opção de certificado da CA |
usage | Lista de string | sempre | Chaves de uso para o certificado (veja abaixo) |
validity | inteiro | sempre | intervalo de validade em horas |
validFrom | corda | sempre | Hora de início no formato "1 de janeiro 01:22:31 2019" |
validUntil | corda | sempre | start time in the format "Jan 1 01:22:31 2019" |
hosts | string list | opcional | List of DNS names or IP addresses |
dnsNames | string list | opcional | List of DNS names |
ipAddresses | string list | opcional | List of IP addresses |
publicKey | corda | sempre | public key to generate the certificate for |
eg:
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) ))
resolves to
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 supports some useful functions to work with wireguard keys. Please refer also to the Useful to Know section to find some tips for providing state.
(( wggenkey() ))
This function can be used generate private wireguard key. The result will base64 encoded.
eg:
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-
eg:
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. Enquanto
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) ))
rendimentos
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.
eg:
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.
eg:
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.
eg:
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. Portanto
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)
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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
.
eg:
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.
eg:
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
Atenção :
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.
eg:
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.
eg:
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.
eg:
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. Ouselect
. 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.
por exemplo
port : 4711
hosts :
- alice
- bob
mapped : (( map[hosts|x|->x ":" port] ))
rendimentos
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.
por exemplo
list :
- name : alice
age : 25
- name : bob
age : 24
ages : (( map[list|i,p|->i + 1 ". " p.name " is " p.age ] ))
rendimentos
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.
por exemplo
ages :
alice : 25
bob : 24
keys : (( map[ages|k,v|->k] ))
rendimentos
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
Observação
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[]
por exemplo
list :
- name : alice
age : 25
- name : bob
age : 26
selected : (( select[list|v|->v.age > 25 ] ))
evaluates selected to
selected :
- name : bob
age : 26
Observação
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 .
Observação
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.
por exemplo
list :
- 1
- 2
sum : (( sum[list|0|s,x|->s + x] ))
rendimentos
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.
por exemplo
list :
- 1
- 2
- 3
prod : (( sum[list|0|s,i,x|->s + i * x ] ))
rendimentos
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.
por exemplo
ages :
alice : 25
bob : 24
sum : (( map[ages|0|s,k,v|->s + v] ))
rendimentos
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.
eg:
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.
eg:
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.
eg:
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.
eg:
temp :
<< : (( &temporary ))
foo : bar
value : (( temp.foo ))
yields:
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
eg:
data :
alice : (( &temporary ( "bar" ) ))
foo : (( alice ))
yields:
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
.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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).)
eg:
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.
eg:
template.yaml
data :
<< : (( &tag:persons ))
alice : 25
tagref : (( persons::alice ))
resolves tagref
to 25
(( tag::. ))
Reference the whole (structured) value of tagged node.
eg:
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 exemplo:
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.
eg:
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
eg:
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:
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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 | corda | current version of spiff |
FILE | corda | name of actually processed template file |
DIR | corda | name of directory of actually processed template file |
RESOLVED_FILE | corda | name of actually processed template file with resolved symbolic links |
RESOLVED_DIR | corda | name of directory of actually processed template file with resolved symbolic links |
PATHNAME | corda | 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
.
eg:
template.yml
foo :
bar :
path : (( __ctx.PATH ))
str : (( __ctx.PATHNAME ))
file : (( __ctx.FILE ))
dir : (( __ctx.DIR ))
evaluates to
eg:
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 exemplo
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.
eg:
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.
eg:
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.
.eg:
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 (( ~~ ))
.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
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.
eg:
bob :
b1 : 1
b2 : 2
b3 : 3
filtered :
<<for :
key,bob : (( .bob ))
<<mapkey : (( key ))
<<do : (( bob == 2 ? ~~ :bob ))
ou
bob :
b1 : 1
b2 : 2
b3 : 3
filtered :
<<for :
key,bob : (( .bob ))
<<mapkey : (( bob == 2 ? ~~ :key ))
<<do : (( bob ))
resolve to
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.
eg:
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:
eg:
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
retorna
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
com
stub.yml
foo :
alice : 24
bob : 26
rendimentos
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"] ))
com
stub.yml
foo :
- peter
- paul
rendimentos
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 ))
com
stub.yml
people :
- alice : 13
rendimentos
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
rendimentos
meta :
properties :
alice : 24
bob : 42
Functions and mappings can freely be nested
eg:
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.
eg:
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.
eg:
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.
eg:
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
com
stub.yml
alice : (( config.alice * 2 || ~ ))
bob : (( config.bob * 3 || ~~ ))
rendimentos
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
com
mapping.yml
config :
alice : (( ~~ ))
bob : (( ~~ ))
alice : (( config.alice || ~~ ))
bob : (( config.bob || ~~ ))
peter : (( config.peter || ~~ ))
e
config.yml
config :
alice : 4711
peter : 0815
rendimentos
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.
eg:
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.
eg:
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 ))
e
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.
eg:
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):
Marcação | 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 ))
}
Ele suporta