_ __ __
___ _ __ (_)/ _|/ _| _ _
/ __| '_ | | |_| |_ _| |_ _| |_
__ |_) | | _| _|_ _|_ _|
|___/ .__/|_|_| |_| |_| |_|
|_|
spiff++
est un fork de spiff qui fournit une extension compatible avec spiff basée sur la dernière version offrant un riche ensemble de nouvelles fonctionnalités non encore disponibles dans spiff. Tous les correctifs fournis par le projet spiff original seront également incorporés dans spiff++. Comme il n'y aura aucun moyen de revenir à la base source de spiff, un nouveau référentiel spiff++ indépendant a été créé pour poursuivre le développement de spiff++.spiff est un outil de ligne de commande et un système de modèles YAML hybride déclaratif dans le domaine. Alors que les systèmes de création de modèles classiques traitent un fichier modèle en remplaçant les expressions du modèle par des valeurs provenant de sources de données externes, dans le domaine signifie que le moteur de création de modèles connaît la syntaxe et la structure du modèle traité. Il peut donc prendre les valeurs des expressions de modèle directement à partir du document traité, y compris les parties désignées par les expressions de modèle elles-mêmes.
Par exemple:
resource :
name : bosh deployment
version : 25
url : (( "http://resource.location/bosh?version=" version ))
description : (( "This document describes a " name " located at " url ))
Au lieu d'utiliser uniquement des sources de valeur externes, spiff fournit un mécanisme de fusion pour fusionner un modèle avec un nombre quelconque de talons de fusion afin de produire un document final.
Il s'agit d'un outil de ligne de commande et d'un système de modèles YAML déclaratif, spécialement conçu pour générer des manifestes de déploiement (par exemple des manifestes BOSH, Kubernetes ou Landscaper).
Outre la CLI, il existe une bibliothèque golang permettant l'utilisation du traitement du modèle spiff dans n'importe quel programme GO (par exemple Landscaper).
Le moteur de création de modèles permet d'accéder au système de fichiers sur la base d'un système de fichiers virtuel configurable ou du système de processus pour exécuter des commandes et incorporer la sortie dans le traitement du modèle.
Contenu:
<<if:
<<switch:
<<type:
<<for:
<<merge:
Les binaires exécutables de la version officielle peuvent être téléchargés via les versions Github pour les machines Darwin, Linux et PowerPC (et les machines virtuelles).
Certaines dépendances de spiff ont changé depuis la dernière version officielle, et spiff ne sera pas mis à jour pour suivre ces dépendances. Ces dépendances sont soit corrigées, soit copiées dans la base de code locale.
spiff merge template.yml [template2.yml ...]
Fusionnez un tas de fichiers modèles en un seul manifeste et imprimez-le.
Voir « langage de modèles dynamiques » pour plus de détails sur le fichier modèle, ou examples/ subdir pour des exemples plus compliqués.
Exemple:
spiff merge cf-release/templates/cf-deployment.yml my-cloud-stub.yml
Il est possible de lire un fichier à partir de l'entrée standard en utilisant le nom de fichier -
. Il ne peut être utilisé qu’une seule fois. Cela permet d'utiliser spiff dans le cadre d'un pipeline pour traiter simplement un seul flux ou pour traiter un flux basé sur plusieurs modèles/stubs.
Le fichier modèle (premier argument) peut être un flux de plusieurs documents contenant plusieurs documents YAML séparés par une ligne contenant uniquement ---
. Chaque document YAML sera traité indépendamment avec les fichiers stub fournis. Le résultat est un flux de documents traités dans le même ordre. Si le nœud racine d'un document est marqué comme temporaire, le document est omis du flux de sortie. Par exemple, cela peut être utilisé pour générer des manifestes Kubernetes à utiliser par kubectl
.
La commande merge
propose plusieurs options :
L'option --partial
. Si cette option est donnée, spiff gère l'évaluation incomplète de l'expression. Toutes les erreurs sont ignorées et les parties insolubles du document yaml sont renvoyées sous forme de chaînes.
Avec l'option --json
la sortie sera au format JSON au lieu de YAML.
L'option --path <path>
peut être utilisée pour générer un chemin imbriqué, au lieu du document traité complet.
Si la sortie est une liste, l'option --split
génère chaque élément de la liste sous forme de document distinct. Le format yaml utilise comme d'habitude ---
comme ligne de séparation. Le format json génère une séquence de documents json , un par ligne.
Avec --select <field path>
il est possible de sélectionner un champ dédié du document traité pour la sortie
Avec --evaluate <dynaml expression>
il est possible d'évaluer une expression dynaml donnée sur le document traité pour la sortie. L'expression est évaluée avant que le chemin de sélection ne soit appliqué, qui fonctionnera ensuite sur le résultat de l'évaluation.
L'option --state <path>
active la prise en charge de l'état de spiff . Si le fichier donné existe, il est placé en haut de la liste des talons configurés pour le fichier donné, il est placé en haut de la liste des talons configurés pour le traitement de fusion. En plus de la sortie du document traité, il est filtré pour les nœuds marqués du marqueur &state
. Ce document filtré est ensuite stocké sous le fichier indiqué, enregistrant l'ancien fichier d'état avec le suffixe .bak
. Ceci peut être utilisé avec une fusion manuelle proposée par la bibliothèque des services publics de l'État.
Avec l'option --bindings <path>
un fichier yaml peut être spécifié, dont le contenu est utilisé pour créer des liaisons supplémentaires pour le traitement. Le document yaml doit être constitué d'une carte. Chaque clé est utilisée comme liaison supplémentaire. Le document de liaisons n'est pas traité, les valeurs sont utilisées telles que définies.
Avec l'option --tag <tag>:<path>
un fichier yaml peut être spécifié, dont le contenu est utilisé comme valeur pour une balise globale prédéfinie (voir Balises). Les balises sont accessibles par des expressions de référence de la forme <tag>::<ref>
. Contrairement aux liaisons, le contenu balisé n'entre pas en concurrence avec les nœuds du document, il utilise un autre espace de noms de référence.
Avec l'option --define <key>=<value>
(shorthand -D
) des valeurs de liaison supplémentaires peuvent être spécifiées sur la ligne de commande, remplaçant les valeurs de liaison du fichier de liaison. L'option peut se produire plusieurs fois.
Si la clé contient des points ( .
), elle sera interprétée comme une expression de chemin pour décrire les champs dans les valeurs de la carte profonde. Un point (et un avant un point) peuvent être échappés par
pour le conserver dans le nom du champ.
L'option --preserve-escapes
conservera l'échappement pour les expressions dynamiques et les directives de fusion liste/carte. Cette option peut être utilisée si d'autres étapes de traitement d'un résultat de traitement avec spiff sont prévues.
L'option --preserve-temporary
conservera les champs marqués comme temporaires dans le document final.
L'option --features=<featurelist>
activera ces fonctionnalités données. Les nouvelles fonctionnalités incompatibles avec l'ancien comportement doivent être explicitement activées. En règle générale, ces fonctionnalités ne rompent pas le comportement courant mais introduisent une interprétation dédiée pour les valeurs yaml qui étaient auparavant utilisées comme valeurs normales.
Les bibliothèques de dossiers proposent des bibliothèques d'utilitaires utiles. Ils peuvent également être utilisés comme exemple pour la puissance de ce moteur de création de modèles.
spiff diff manifest.yml other-manifest.yml
Afficher les différences structurelles entre deux manifestes de déploiement. Ici, les flux contenant plusieurs documents sont également pris en charge. Pour indiquer aucune différence, le nombre de documents dans les deux flux doit être identique et chaque document du premier flux ne doit présenter aucune différence par rapport au document avec le même index dans le deuxième flux. Les différences trouvées sont affichées pour chaque document séparément.
Contrairement aux outils de comparaison de base et même bosh diff
, cette commande possède une connaissance sémantique d'un manifeste de déploiement et n'est pas uniquement basée sur du texte. Par exemple, si deux manifestes sont identiques sauf qu'ils contiennent des tâches répertoriées dans des ordres différents, spiff diff
le détectera, car l'ordre des tâches est important dans un manifeste. D'un autre côté, si deux manifestes diffèrent uniquement par l'ordre de leurs pools de ressources, par exemple, cela générera une différence vide puisque l'ordre des pools de ressources n'a pas réellement d'importance pour un déploiement.
Contrairement à bosh diff
, cette commande ne modifie aucun des deux fichiers.
Il sert à vérifier les différences entre un déploiement et le suivant.
Flux typique :
$ 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
La sous-commande convert
peut être utilisée pour convertir les fichiers d'entrée en json ou simplement pour normaliser l'ordre des champs. Les options disponibles sont --json
, --path
, --split
ou --select
selon leur signification pour la sous-commande merge
.
spiff encrypt secret.yaml
La sous-commande encrypt
peut être utilisée pour crypter ou déchiffrer des données selon la fonction encrypt
dynaml. Le mot de passe peut être donné en deuxième argument ou il est extrait de la variable d'environnement SPIFF_ENCRYPTION_KEY
. Le dernier argument peut être utilisé pour passer la méthode de chiffrement (voir fonction encrypt
)
Les données sont extraites du fichier spécifié. Si -
est donné, il est lu depuis stdin.
Si l'option -d
est donnée, les données sont déchiffrées, sinon les données sont lues sous forme de document yaml et le résultat crypté est imprimé.
Les nouvelles fonctionnalités incompatibles avec l'ancien comportement doivent être explicitement activées. En règle générale, ces fonctionnalités ne rompent pas le comportement courant, mais introduisent une interprétation dédiée pour les valeurs yaml qui étaient auparavant utilisées comme valeurs normales et peuvent donc rompre les cas d'utilisation existants.
Les indicateurs de fonctionnalité suivants sont actuellement pris en charge :
Fonctionnalité | Depuis | État | Signification |
---|---|---|---|
interpolation | 1.7.0-bêta-1 | alpha | dynaml dans le cadre des chaînes yaml |
control | 1.7.0-bêta-4 | alpha | structures de contrôle basées sur yaml |
Les indicateurs de fonctionnalités actives peuvent être interrogés à l'aide de la fonction dynamique features()
comme liste de chaînes. Si cette fonction est appelée avec un argument de chaîne, elle renvoie si la fonctionnalité donnée est actuellement activée.
Les fonctionnalités peuvent être activées par ligne de commande à l'aide de l'option --features
, par la bibliothèque go à l'aide de la fonction WithFeatures
ou généralement en définissant la variable d'environnement SPIFF_FEATURES
sur une liste de fonctionnalités. Ce paramètre est toujours utilisé par défaut. En utilisant les paramètres spiff Plain()
de la bibliothèque go, toutes les variables d'environnement sont ignorées.
Une fonctionnalité peut être spécifiée par son nom ou par son nom précédé du préfixe no
pour la désactiver.
Le dossier des bibliothèques contient des bibliothèques de modèles spiff utiles. Il s'agit essentiellement de simples stubs qui sont ajoutés à la liste des fichiers de fusion pour offrir les fonctions utilitaires pour le traitement de la fusion.
Spiff utilise un langage de création de modèles déclaratif et sans logique appelé « dynaml » (yaml dynamique).
Chaque nœud dynaml est garanti pour se résoudre en un nœud YAML. Ce n'est pas une interpolation de chaîne. Cela évite aux développeurs d'avoir à réfléchir à la manière dont une valeur sera restituée dans le modèle résultant.
Un nœud dynaml apparaît dans le fichier .yml sous la forme d'une chaîne désignant une expression entourée de deux parenthèses (( <dynaml> ))
. Ils peuvent être utilisés comme valeur d’une carte ou comme entrée dans une liste. L'expression peut s'étendre sur plusieurs lignes. Dans tous les cas, la valeur de la chaîne yaml ne doit pas se terminer par une nouvelle ligne (par exemple en utilisant |-
)
Si une valeur entre parenthèses ne doit pas être interprétée comme une expression dynamique et conservée telle quelle dans la sortie, elle peut être échappée par un point d'exclamation directement après les crochets ouvrants.
Par exemple, ((! .field ))
correspond à la valeur de chaîne (( .field ))
et ((!! .field ))
correspond à la valeur de chaîne ((! .field ))
.
Voici une liste complète des expressions dynamiques :
(( foo ))
Recherchez la clé 'foo' la plus proche (c'est-à-dire la portée lexicale) dans le modèle actuel et introduisez-la.
par exemple :
fizz :
buzz :
foo : 1
bar : (( foo ))
bar : (( foo ))
foo : 3
bar : (( foo ))
Cet exemple se résoudra à :
fizz :
buzz :
foo : 1
bar : 1
bar : 3
foo : 3
bar : 3
Les problèmes suivants ne seront pas résolus car le nom de la clé est le même que la valeur à fusionner :
foo : 1
hi :
foo : (( foo ))
(( foo.bar.[1].baz ))
Recherchez la clé 'foo' la plus proche, et à partir de là, suivez jusqu'à .bar.[1].baz
.
Un chemin est une séquence d'étapes séparées par des points. Une étape est soit un mot pour les cartes, soit des chiffres entourés de parenthèses pour l'indexation des listes. L'index peut être négatif (un moins suivi de chiffres). Les indices négatifs sont extraits de la fin de la liste (index effectif = index + longueur (liste)).
Un chemin qui ne peut pas être résolu entraîne une erreur d’évaluation. Si une référence ne doit parfois pas être fournie, elle doit être utilisée en combinaison avec '||' (voir ci-dessous) pour garantir la résolution.
Remarque : Le grammateur dynamique a été retravaillé pour activer désormais la syntaxe d'index habituelle. Au lieu de foo.bar.[1]
il est désormais possible d'utiliser foo.bar[1]
.
Remarque : les références se trouvent toujours dans le modèle ou le stub, et l'ordre n'a pas d'importance. Vous pouvez faire référence à un autre nœud dynamique et supposer qu'il est résolu, et le nœud de référence finira par se résoudre une fois le nœud dépendant résolu.
par exemple :
properties :
foo : (( something.from.the.stub ))
something : (( merge ))
Cela sera résolu tant que « quelque chose » est résoluble, et tant que cela apporte quelque chose comme ceci :
from :
the :
stub : foo
Si le chemin commence par un point ( .
), le chemin est toujours évalué à partir de la racine du document. Si la racine du document est une liste, le premier niveau de mappe est utilisé pour résoudre l'expression de chemin si elle commence par .__map
. Cela peut être utilisé pour éviter d'avoir besoin d'utiliser le propre index de liste (comme .[1].path
), qui peut changer si des entrées de liste sont ajoutées.
Les entrées de liste constituées d'une carte avec un champ name
peuvent être directement adressées par leur valeur de nom en tant que composant de chemin.
Remarque : Cela fonctionne également pour les chemins absolus des documents de liste.
par exemple :
L'âge d'Alice dans
list :
- name : alice
age : 25
peut être référencé en utilisant le chemin list.alice.age
, au lieu de list[0].age
.
Par défaut, un champ portant le name
name est utilisé comme champ clé. Si un autre champ doit être utilisé comme champ clé, il peut être marqué dans une entrée de liste comme clé en préfixant le nom du champ avec le mot-clé clé key:
. Ce mot-clé est supprimé par le traitement et ne fera pas partie du résultat final du traitement.
par exemple :
list :
- key:person : alice
age : 25
alice : (( list.alice ))
sera résolu à
list :
- person : alice
age : 25
alice :
person : alice
age : 25
Ce nouveau champ clé sera également observé lors de la fusion de listes.
Si le champ clé sélectionné commence par un !
, la fonctionnalité clé est désactivée. Le point d'exclamation est également supprimé du nom de champ effectif.
Si les valeurs du champ clé ne sont pas uniques, il est également désactivé.
(( foo.[bar].baz ))
Recherchez la clé 'foo' la plus proche, et à partir de là, suivez le(s) champ(s) décrit(s) par la bar
d'expression, puis jusqu'à .baz.
L'index peut être une constante entière (sans espaces) comme décrit dans la dernière section. Mais il peut aussi s'agir d'une expression dynamique arbitraire (même un nombre entier, mais avec des espaces). Si l’expression renvoie une chaîne, elle recherche le champ dédié. Si l’expression donne un nombre entier, l’élément du tableau avec cet index est adressé. Le point ( .
) devant l’opérateur d’index est facultatif.
par exemple :
properties :
name : alice
foo : (( values.[name].bar ))
values :
alice :
bar : 42
Cela résoudra foo
à la valeur 42
. L'index dynamique peut également être à la fin de l'expression (sans .bar
).
Fondamentalement, c'est la manière la plus simple d'exprimer quelque chose comme eval("values." name ".bar")
Si l'expression est évaluée comme une liste, les éléments de la liste (chaînes ou entiers) sont utilisés comme éléments de chemin pour accéder à des champs plus profonds.
par exemple :
properties :
name :
- foo
- bar
foo : (( values.[name] ))
values :
foo :
bar : 42
résout foo
à nouveau à la valeur 42
.
Remarque : L'opérateur d'index est également utilisable sur l'élément racine ( .[index]
).
Il est possible de spécifier plusieurs indices séparés par des virgules dans des listes successives ( foo[0][1]
est équivalent à `foo[0,1]). Dans ce cas, les indices ne peuvent pas être à nouveau des listes.
(( list.[1..3] ))
L'expression slice peut être utilisée pour extraire une sous-liste dédiée à partir d'une expression de liste. La plage start ..
end extrait une liste de longueur end-start+1 avec les éléments de l'index start à end . Si l'index de départ est négatif, la tranche est prise à partir de la fin de la liste de length+start à length+end . Si l'index de fin est inférieur à l'index de début, le résultat est un tableau vide.
par exemple :
list :
- a
- b
- c
foo : (( list.[1..length(list) - 1] ))
L'index de début ou de fin peut être omis. Il est ensuite sélectionné en fonction de la taille réelle de la liste. Par conséquent list.[1..length(list)]
est équivalent à list.[1..]
.
évalue foo
à la liste [b,c]
.
(( 1.2e4 ))
Les lettres numériques sont prises en charge pour les entiers et les valeurs à virgule flottante.
(( "foo" ))
Chaîne littérale. Tous les encodages de chaînes JSON sont pris en charge (par exemple n
, "
ou uxxxx
).
(( [ 1, 2, 3 ] ))
Liste littérale. Les éléments de la liste pourraient à nouveau être des expressions. Il existe un littéral de liste spécial [1 .. -1]
, qui peut être utilisé pour résoudre une plage de nombres croissants ou décroissants en une liste.
par exemple :
list : (( [ 1 .. -1 ] ))
rendements
list :
- 1
- 0
- -1
(( { "alice" = 25 } ))
Le littéral map peut être utilisé pour décrire des cartes dans le cadre d’une expression dynamique. La clé et la valeur peuvent à nouveau être des expressions, dans lesquelles l'expression clé doit être évaluée comme une chaîne. De cette façon, il est possible de créer des cartes avec des clés non statiques. L'opérateur d'affectation =
a été choisi à la place du caractère habituel deux-points :
utilisé dans yaml, car cela entraînerait des conflits avec la syntaxe yaml.
Un littéral de carte peut être constitué de n'importe quel nombre d'affectations de champs séparées par une virgule ,
.
par exemple :
name : peter
age : 23
map : (( { "alice" = {}, name = age } ))
rendements
name : peter
age : 23
map :
alice : {}
peter : 23
Une autre façon de composer des listes basées sur des expressions consiste à utiliser les fonctions makemap
et list_to_map
.
(( ( "alice" = 25 ) alice ))
Toute expression peut être précédée d'un nombre quelconque de littéraux de portée explicites. Un littéral de portée décrit une carte dont les valeurs sont disponibles pour la résolution de référence relative de l'expression (portée statique). Il crée une liaison locale supplémentaire pour les prénoms.
Un littéral de portée peut être constitué de n'importe quel nombre d'affectations de champs séparées par une virgule ,
. La clé ainsi que la valeur sont données par des expressions, alors que l'expression clé doit être évaluée comme une chaîne. Toutes les expressions sont évaluées dans la portée externe suivante, ce qui signifie que les paramètres ultérieurs d'une portée ne peuvent pas utiliser les paramètres antérieurs du même littéral de portée.
par exemple :
scoped : (( ( "alice" = 25, "bob" = 26 ) alice + bob ))
rendements
scoped : 51
Un nom de champ peut également être désigné par un symbole ( $
name ).
(( foo bar ))
Expression de concaténation utilisée pour concaténer une séquence d'expressions dynamiques.
(( "foo" bar ))
Concaténation (où bar est une autre expression dynamique). Toutes les séquences de valeurs simples (chaîne, entier et booléen) peuvent être concaténées, données par n'importe quelle expression dynamique.
par exemple :
domain : example.com
uri : (( "https://" domain ))
Dans cet exemple, uri
sera résolu par la valeur "https://example.com"
.
(( [1,2] bar ))
Concaténation de listes sous forme d'expression (où bar est une autre expression dynamique). Toutes les séquences de listes peuvent être concaténées, données par n'importe quelle expression dynamique.
par exemple :
other_ips : [ 10.0.0.2, 10.0.0.3 ]
static_ips : (( ["10.0.1.2","10.0.1.3"] other_ips ))
Dans cet exemple, static_ips
sera résolu à la valeur [ 10.0.1.2, 10.0.1.3, 10.0.0.2, 10.0.0.3 ]
.
Si la deuxième expression donne une valeur autre qu'une liste (entier, booléen, chaîne ou carte), la valeur est ajoutée à la première liste.
par exemple :
foo : 3
bar : (( [1] 2 foo "alice" ))
donne la liste [ 1, 2, 3, "alice" ]
pour bar
.
(( map1 map2 ))
Concaténation de cartes comme expression. Toutes les séquences de cartes peuvent être concaténées, données par n'importe quelle expression dynamique. Ainsi, les entrées seront fusionnées. Les entrées avec la même clé sont écrasées de gauche à droite.
par exemple :
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat : (( foo bar ))
rendements
foo :
alice : 24
bob : 25
bar :
bob : 26
paul : 27
concat :
alice : 24
bob : 26
paul : 27
(( auto ))
Calcul automatique de la valeur contextuelle.
Dans l'attribut « taille » d'un pool de ressources, cela signifie effectuer un calcul basé sur le nombre total d'instances de tous les travaux qui se déclarent dans le pool de ressources actuel.
par exemple :
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
Dans ce cas, la taille du pool de ressources sera résolue à « 5 ».
(( merge ))
Introduisez le chemin actuel à partir des fichiers stub qui sont en cours de fusion.
par exemple :
foo :
bar :
baz : (( merge ))
J'essaierai d'introduire foo.bar.baz
à partir du premier stub, ou du second, etc., en renvoyant la valeur du dernier stub qui la fournit.
Si la valeur correspondante n'est pas définie, elle renverra zéro. Celle-ci a alors la même sémantique que les expressions de référence ; une fusion nulle est un modèle non résolu. Voir ||
.
<<: (( merge ))
Fusion de cartes ou de listes avec le contenu du même élément trouvé dans un stub.
** Attention ** Cette forme de merge
présente un problème de compatibilité. Dans les versions antérieures à 1.0.8, cette expression n'était jamais analysée, seule l'existence de la clé <<:
était pertinente. Par conséquent, il existe souvent des utilisations de <<: (( merge ))
où <<: (( merge || nil ))
est signifié. La première variante nécessiterait du contenu dans au moins un stub (comme toujours pour l'opérateur de fusion). Désormais, cette expression est évaluée correctement, mais cela briserait les ensembles de modèles de manifeste existants, qui utilisent la première variante, mais signifient la seconde. Par conséquent, ce cas est explicitement traité pour décrire une fusion facultative. Si une fusion est réellement requise, un qualificatif explicite supplémentaire doit être
Remarque : Au lieu d'utiliser un champ d'insertion <<:
pour placer des expressions de fusion, il est désormais possible d'utiliser <<<:
, également, ce qui permet d'utiliser des analyseurs yaml standards pour les documents yaml de type spiff. <<:
est conservé pour des raisons de compatibilité ascendante. être utilisé ( (( merge required ))
).
Si la clé de fusion ne doit pas être interprétée comme une clé normale au lieu d'une directive de fusion, elle peut être échappée par un point d'excalamtion ( !
).
Par exemple, une clé de carte <<<!
se traduira par une clé de chaîne <<<
et <<<!!
se traduira par une clé de chaîne <<<!
valeurs.yml
foo :
a : 1
b : 2
modèle.yml
foo :
<< : (( merge ))
b : 3
c : 4
spiff merge template.yml values.yml
donne :
foo :
a : 1
b : 2
c : 4
valeurs.yml
foo :
- 1
- 2
modèle.yml
foo :
- 3
- << : (( merge ))
- 4
spiff merge template.yml values.yml
donne :
foo :
- 3
- 1
- 2
- 4
- <<: (( merge on key ))
spiff
est capable de fusionner des listes de cartes avec un champ clé. Ces listes sont gérées comme des cartes avec la valeur du champ clé comme clé. Par défaut, le name
de la clé est utilisé. Mais avec le sélecteur on
une clé arbitraire, un nom peut être spécifié pour une expression de fusion de liste.
par exemple :
list :
- << : (( merge on key ))
- key : alice
age : 25
- key : bob
age : 24
fusionné avec
list :
- key : alice
age : 20
- key : peter
age : 13
rendements
list :
- key : peter
age : 13
- key : alice
age : 20
- key : bob
age : 24
Si aucune insertion de nouvelles entrées n'est souhaitée (comme le demande l'expression de fusion par insertion), mais uniquement le remplacement des entrées existantes, un champ clé existant peut être préfixé par la balise key:
pour indiquer un nom de clé non standard, par exemple - key:key: alice
.
<<: (( merge replace ))
Remplace le contenu complet d'un élément par le contenu trouvé dans un stub au lieu de procéder à une fusion approfondie du contenu existant.
valeurs.yml
foo :
a : 1
b : 2
modèle.yml
foo :
<< : (( merge replace ))
b : 3
c : 4
spiff merge template.yml values.yml
donne :
foo :
a : 1
b : 2
valeurs.yml
foo :
- 1
- 2
modèle.yml
foo :
- << : (( merge replace ))
- 3
- 4
spiff merge template.yml values.yml
donne :
foo :
- 1
- 2
<<: (( foo ))
Fusion de cartes et de listes trouvées dans le même modèle ou stub.
foo :
a : 1
b : 2
bar :
<< : (( foo )) # any dynaml expression
b : 3
donne :
foo :
a : 1
b : 2
bar :
a : 1
b : 3
Cette expression ajoute simplement de nouvelles entrées à la liste réelle. Il ne fusionne pas les entrées existantes avec le contenu décrit par l'expression de fusion.
bar :
- 1
- 2
foo :
- 3
- << : (( bar ))
- 4
donne :
bar :
- 1
- 2
foo :
- 3
- 1
- 2
- 4
Un cas d'utilisation courant consiste à fusionner des listes d'adresses IP ou de plages statiques en une liste d'adresses IP. Une autre possibilité consiste à utiliser une seule expression de concaténation.
<<: (( merge foo ))
Fusion de cartes ou de listes avec le contenu d'un élément arbitraire trouvé dans un stub (fusion de redirection). Il n'y aura plus de fusion (profonde) avec l'élément du même nom trouvé dans un stub. (La fusion approfondie des listes nécessite des cartes avec name
du champ)
Les fusions de redirection peuvent également être utilisées comme valeur de champ directe. Ils peuvent être combinés avec des fusions de remplacement comme (( merge replace foo ))
.
valeurs.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
modèle.yml
foo :
<< : (( merge bar))
b : 3
c : 4
spiff merge template.yml values.yml
donne :
foo :
a : 1
b : 2
c : 4
Une autre façon de fusionner avec un autre élément dans un stub pourrait également être effectuée de manière traditionnelle :
valeurs.yml
foo :
a : 10
b : 20
bar :
a : 1
b : 2
modèle.yml
bar :
<< : (( merge ))
b : 3
c : 4
foo : (( bar ))
Mais dans ce scénario, la fusion effectue toujours la fusion profonde avec le nom de l'élément d'origine. Par conséquent, spiff merge template.yml values.yml
donne :
bar :
a : 1
b : 2
c : 4
foo :
a : 10
b : 20
c : 4
valeurs.yml
foo :
- 10
- 20
bar :
- 1
- 2
modèle.yml
foo :
- 3
- << : (( merge bar ))
- 4
spiff merge template.yml values.yml
donne :
foo :
- 3
- 1
- 2
- 4
<<: (( merge none ))
Si la référence d'une fusion de redirection est définie sur la constante none
, aucune fusion n'est effectuée du tout. Cette expression donne toujours la valeur nulle.
par exemple : pour
modèle.yml
map :
<< : (( merge none ))
value : notmerged
valeurs.yml
map :
value : merged
spiff merge template.yml values.yml
donne :
map :
value : notmerged
Cela peut être utilisé pour la fusion explicite de champs à l'aide de la fonction stub
pour accéder aux parties dédiées des stubs en amont.
par exemple :
modèle.yml
map :
<< : (( merge none ))
value : (( "alice" "+" stub(map.value) ))
valeurs.yml
map :
value : bob
spiff merge template.yml values.yml
donne :
test :
value : alice+bob
Cela fonctionne également pour les champs dédiés :
modèle.yml
map :
value : (( merge none // "alice" "+" stub() ))
valeurs.yml
map :
value : bob
spiff merge template.yml values.yml
donne :
test :
value : alice+bob
(( a || b ))
Utilise a, ou b si a ne peut pas être résolu.
par exemple :
foo :
bar :
- name : some
- name : complicated
- name : structure
mything :
complicated_structure : (( merge || foo.bar ))
Cela tentera de fusionner dans mything.complicated_structure
, ou, s'il ne peut pas être fusionné, utilisera la valeur par défaut spécifiée dans foo.bar
.
L'opérateur //
vérifie en outre si a
peut être résolu en une valeur valide (différente de ~
).
(( 1 + 2 * foo ))
Les expressions Dynaml peuvent être utilisées pour exécuter des calculs arithmétiques entiers et à virgule flottante. Les opérations prises en charge sont +
, -
, *
et /
. L'opérateur modulo ( %
) ne prend en charge que les opérandes entiers.
par exemple :
valeurs.yml
foo : 3
bar : (( 1 + 2 * foo ))
spiff merge values.yml
donne 7
pour bar
. Ceci peut être combiné avec des concaténations (le calcul a une priorité plus élevée que la concaténation dans les expressions dynamiques) :
foo : 3
bar : (( foo " times 2 yields " 2 * foo ))
Le résultat est que la chaîne 3 times 2 yields 6
.
(( "10.10.10.10" - 11 ))
Outre l'arithmétique sur les entiers, il est également possible d'utiliser l'addition et la soustraction sur les adresses IP et les cidrs.
par exemple :
ip : 10.10.10.10
range : (( ip "-" ip + 247 + 256 * 256 ))
rendements
ip : 10.10.10.10
range : 10.10.10.10-10.11.11.1
La soustraction fonctionne également sur deux adresses IP ou cidrs pour calculer le nombre d'adresses IP entre deux adresses IP.
par exemple :
diff : (( 10.0.1.0 - 10.0.0.1 + 1 ))
donne la valeur 256. Les constantes d'adresse IP peuvent être directement utilisées dans les expressions dynamiques. Ils sont implicitement convertis en chaînes et reconvertis en adresses IP si une opération l'exige.
La multiplication et la division peuvent être utilisées pour gérer les changements de plage IP sur les CIDR. Avec la division, un réseau peut être divisé. La taille du réseau est augmentée pour permettre au moins un nombre dédié de sous-réseaux en dessous du CIDR d'origine. La multiplication peut alors être utilisée pour obtenir le n-ème sous-réseau suivant de la même taille.
par exemple :
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
rendements
subnet : 10.1.2.0/28
next : 10.1.2.32/28
De plus, certaines fonctions fonctionnent sur les CIDR IPv4 :
cidr : 192.168.0.1/24
range : (( min_ip(cidr) "-" max_ip(cidr) ))
next : (( max_ip(cidr) + 1 ))
num : (( min_ip(cidr) "+" num_ip(cidr) "=" min_ip(cidr) + num_ip(cidr) ))
contains : (( contains_ip(cidr, "192.168.0.2") ))
rendements
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 prend en charge les opérateurs de comparaison <
, <=
, ==
, !=
, >=
et >
. Les opérateurs de comparaison fonctionnent sur des valeurs entières. Les contrôles d'égalité fonctionnent également sur les listes et les cartes. Le résultat est toujours une valeur booléenne. Pour annuler une condition, l'opérateur unaire not ( !
) peut être utilisé.
De plus, il existe l'opérateur conditionnel ternaire ?:
, qui peut être utilisé pour évaluer des expressions en fonction d'une condition. Le premier opérande est utilisé comme condition. L'expression est évaluée au deuxième opérande si la condition est vraie, et au troisième dans le cas contraire.
par exemple :
foo : alice
bar : bob
age : 24
name : (( age > 24 ? foo :bar ))
donne la valeur bob
pour le name
de la propriété.
Une expression est considérée comme false
si son résultat est
false
Sinon, c'est considéré comme true
Remarque
L'utilisation du symbole :
peut entrer en conflit avec la syntaxe yaml, si l'expression complète n'est pas une valeur de chaîne entre guillemets.
Les opérateurs -or
et -and
peuvent être utilisés pour combiner des opérateurs de comparaison afin de composer des conditions plus complexes.
Remarque:
Le symbole d'opérateur plus traditionnel ||
(et &&
) ne peut pas être utilisé ici, car l'opérateur ||
existe déjà en dynamique avec une sémantique différente, qui ne vaut pas pour les opérations logiques. L'expression false || true
est évalué à false
, car il renvoie le premier opérande, s'il est défini, quelle que soit sa valeur. Pour être aussi compatible que possible, cela ne peut pas être modifié et les symboles nus or
et and
ne peuvent pas être utilisés, car cela invaliderait la concaténation des références avec de tels noms.
(( 5 -or 6 ))
Si les deux côtés d’un opérateur -or
ou -and
donnent des valeurs entières, une opération au niveau du bit est exécutée et le résultat est à nouveau un entier. Par conséquent, l’expression 5 -or 6
est évaluée à 7
.
Dynaml prend en charge un ensemble de fonctions prédéfinies. Une fonction est généralement appelée comme
result : (( functionname(arg, arg, ...) ))
Des fonctions supplémentaires peuvent être définies dans le cadre du document yaml à l'aide d'expressions lambda. Le nom de la fonction est alors soit une expression groupée, soit le chemin d'accès au nœud hébergeant l'expression lambda.
(( format( "%s %d", alice, 25) ))
Formatez une chaîne en fonction des arguments donnés par les expressions dynamiques. Il existe une deuxième version de cette fonction : error
formate un message d'erreur et définit l'évaluation comme ayant échoué.
(( join( ", ", list) ))
Joignez les entrées de listes ou de valeurs directes à une seule valeur de chaîne à l'aide d'une chaîne de séparation donnée. Les arguments à joindre peuvent être des expressions dynamiques évaluées en listes, dont les valeurs sont à nouveau des chaînes ou des entiers, ou des valeurs de chaîne ou entières.
par exemple :
alice : alice
list :
- foo
- bar
join : (( join(", ", "bob", list, alice, 10) ))
donne la valeur de chaîne bob, foo, bar, alice, 10
pour join
.
(( split( ",", string) ))
Divisez une chaîne pour un séparateur dédié. Le résultat est une liste. Au lieu d'une chaîne de séparation, une valeur entière peut être donnée, qui divise la chaîne donnée en une liste de chaînes de longueur limitée. La longueur est comptée en runes et non en octets.
par exemple :
list : (( split("," "alice, bob") ))
limited : (( split(4, "1234567890") ))
donne :
list :
- alice
- ' bob '
limited :
- " 1234 "
- " 5678 "
- " 90 "
Un troisième argument facultatif peut être spécifié. Il limite le nombre d'entrées de liste renvoyées. La valeur -1 conduit à une longueur de liste illimitée.
Si une expression régulière doit être utilisée comme chaîne de séparation, la fonction split_match
peut être utilisée.
(( trim(string) ))
Coupez une chaîne ou tous les éléments d'une liste de chaînes. Il existe un deuxième argument de chaîne facultatif. Il peut être utilisé pour spécifier un ensemble de caractères qui seront coupés. Le jeu de coupes par défaut se compose d'un espace et d'un caractère de tabulation.
par exemple :
list : (( trim(split("," "alice, bob")) ))
donne :
list :
- alice
- bob
(( element(list, index) ))
Renvoie un élément de liste dédié donné par son index.
par exemple :
list : (( trim(split("," "alice, bob")) ))
elem : (( element(list,1) ))
donne :
list :
- alice
- bob
elem : bob
(( element(map, key) ))
Renvoie un champ de carte dédié donné par sa clé.
map :
alice : 24
bob : 25
elem : (( element(map,"bob") ))
donne :
map :
alice : 24
bob : 25
elem : 25
Cette fonction est également capable de gérer les touches contenant des points (.).
(( compact(list) ))
Filtrez une liste en omettant les entrées vides.
par exemple :
list : (( compact(trim(split("," "alice, , bob"))) ))
donne :
list :
- alice
- bob
(( uniq(list) ))
Uniq fournit une liste sans doublons.
par exemple :
list :
- a
- b
- a
- c
- a
- b
- 0
- " 0 "
uniq : (( uniq(list) ))
rendements pour le champ uniq
:
uniq :
- a
- b
- c
- 0
(( contains(list, "foobar") ))
Vérifie si une liste contient une valeur dédiée. Les valeurs peuvent également être des listes ou des cartes.
par exemple :
list :
- foo
- bar
- foobar
contains : (( contains(list, "foobar") ))
donne :
list :
- foo
- bar
- foobar
contains : true
La fonction contains
également des travaux sur les chaînes pour rechercher des sous-chaînes ou des cartes pour rechercher une clé. Dans ces cas, l'élément doit être une chaîne.
par exemple :
contains : (( contains("foobar", "bar") ))
donne true
.
(( basename(path) ))
La fonction basename
renvoie le nom du dernier élément d'un chemin. L'argument peut être soit un nom de chemin normal, soit une URL.
par exemple :
pathbase : (( basename("alice/bob") ))
urlbase : (( basename("http://foobar/alice/bob?any=parameter") ))
donne :
pathbase : bob
urlbase : bob
(( dirname(path) ))
La fonction dirname
renvoie le répertoire parent d'un chemin. L'argument peut être soit un nom de chemin normal, soit une URL.
par exemple :
pathbase : (( dirname("alice/bob") ))
urlbase : (( dirname("http://foobar/alice/bob?any=parameter") ))
donne :
pathbase : alice
urlbase : /alice
(( parseurl("http://github.com") ))
Cette fonction analyse une URL et génère une carte avec tous les éléments d'une URL. Le port
Fields, userinfo
et password
sont facultatifs.
Par exemple:
url : (( parseurl("https://user:[email protected]:443/mandelsoft/spiff?branch=master&tag=v1#anchor") ))
donne:
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") ))
Vérifie si une liste contient une valeur dédiée et renvoie l'index de la première correspondance. Les valeurs peuvent également être des listes ou des cartes. Si aucune entrée ne pouvait être trouvée, -1
est retourné.
Par exemple:
list :
- foo
- bar
- foobar
index : (( index(list, "foobar") ))
donne:
list :
- foo
- bar
- foobar
index : 2
L' index
de fonction fonctionne également sur les chaînes pour rechercher des sous-chaînes.
Par exemple:
index : (( index("foobar", "bar") ))
donne 3
.
(( lastindex(list, "foobar") ))
La fonction lastindex
fonctionne comme index
mais l'index de la dernière occurrence est renvoyé.
Le sort
de fonction peut être utilisé pour trier des listes entières ou de chaînes. L'opération de tri est stable.
Par exemple:
list :
- alice
- foobar
- bob
sorted : (( sort(list) ))
rendements pour sorted
- alice
- bob
- foobar
Si d'autres types doivent être triés, en particulier les types complexes comme les listes ou les cartes, ou une règle de comparaison différente est requise, une fonction de comparaison peut être spécifiée comme un deuxième argument facultatif. La fonction de comparaison doit être une expression de lambda prenant deux arguments. Le type de résultat doit être integer
ou bool
indiquant si A est inférieur à B. Si un entier est retourné, il devrait être
Par exemple:
list :
- alice
- foobar
- bob
sorted : (( sort(list, |a,b|->length(a) < length(b)) ))
rendements pour sorted
- bob
- alice
- foobar
(( replace(string, "foo", "bar") ))
Remplacez toutes les occurrence d'une sous-chaîne dans une chaîne par une chaîne de remplacement. Avec un quatrième argument entier en option, le nombre de substitutions peut être limité (-1 moyen illimité).
Par exemple:
string : (( replace("foobar", "o", "u") ))
donne fuubar
.
Si une expression régulière doit être utilisée comme chaîne de recherche, la fonction replace_match
peut être utilisée. Ici, la chaîne de recherche est évaluée comme une expression régulière. Il peut conatriner les sous-expressions. Ces correspondances peuvent être utilisées dans la chaîne de remplacement
Par exemple:
string : (( replace_match("foobar", "(o*)b", "b${1}") ))
donne fbooar
.
L'argument de remplacement pourrait également être une fonction lambda. Dans ce cas, pour chaque correspondance, la fonction est appelée pour déterminer la valeur de remplacement. L'argument d'entrée unique est une liste des correspondances de sous-expression réelles.
Par exemple:
string : (( replace_match("foobar-barfoo", "(o*)b", |m|->upper(m.[1]) "b" ) ))
donne fOObar-barfoo
.
(( substr(string, 1, 2) ))
Extraire une chaîne de stub à partir d'une chaîne, à partir d'un index de démarrage donné jusqu'à un index de fin facultatif (exclusif). Si aucun indice de fin n'est donné, le sous-struvt jusqu'à la fin de la chaîne est extrait. Les deux indices peuvent être négatifs. Dans ce cas, ils sont prélevés à la fin de la chaîne.
Par exemple:
string : " foobar "
end1 : (( substr(string,-2) ))
end2 : (( substr(string,3) ))
range : (( substr(string,1,-1) ))
évalue à
string : foobar
end1 : ar
end2 : bar
range : ooba
(( match("(f.*)(b.*)", "xxxfoobar") ))
Renvoie la correspondance d'une expression régulière pour une valeur de chaîne donnée. La correspondance est une liste des valeurs correspondantes pour les sous-expressions contenues dans l'expression régulière. L'index 0 fait référence à la correspondance de l'expression régulière complète. Si la valeur de chaîne ne correspond pas à une liste vide est renvoyée.
Par exemple:
matches : (( match("(f.*)*(b.*)", "xxxfoobar") ))
donne:
matches :
- foobar
- foo
- bar
Un troisième argument de type entier peut être donné pour demander une correspondance multiple d'un maximum de n répétitions. Si la valeur est négative, toutes les répétitions sont signalées. Le résultat est une liste de toutes les correspondances, chacune dans le format décrit ci-dessus.
(( keys(map) ))
Déterminez la liste triée des clés utilisées dans une carte.
Par exemple:
map :
alice : 25
bob : 25
keys : (( keys(map) ))
donne:
map :
alice : 25
bob : 25
keys :
- alice
- bob
(( length(list) ))
Déterminez la longueur d'une liste, une carte ou une valeur de chaîne.
Par exemple:
list :
- alice
- bob
length : (( length(list) ))
donne:
list :
- alice
- bob
length : 2
(( base64(string) ))
La fonction base64
génère un codage Base64 d'une chaîne donnée. base64_decode
décode une chaîne codée Base64.
Par exemple:
base64 : (( base64("test") ))
test : (( base64_decode(base64)))
évalue à
base54 : dGVzdA==
test : test
Un deuxième argument facultatif peut être utilisé pour spécifier la longueur de ligne maximale. Dans ce cas, le résultat sera une chaîne multi-lignes.
(( hash(string) ))
Le hash
de fonction génère plusieurs types de hachages pour la chaîne donnée. Par défaut, lorsque le hachage sha256
est généré. Un deuxième argument facultatif spécifie le type de hachage. Les types possibles sont md4
, md5
, sha1
, sha224
, sha256
, sha384
, sha2512
, sha512/224
ou sha512/256
.
Les hachages md5
peuvent toujours être générés par le Finctio md5(string)
déprécié.
Par exemple:
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") ))
évalue à
data : alice
hash :
deprecated : 6384e2b2184bcbf58eccf10ca7a6563c
md4 : 616c69636531d6cfe0d16ae931b73c59d7e0c089c0
md5 : 6384e2b2184bcbf58eccf10ca7a6563c
sha1 : 522b276a356bdf39013dfabea2cd43e141ecc9e8
sha224 : 38b7e5d5651aaf85694a7a7c6d5db1275af86a6df93a36b8a4a2e771
sha256 : 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90
sha384 : 96a5353e625adc003a01bdcd9b21b21189bdd9806851829f45b81d3dfc6721ee21f6e0e98c4dd63bc559f66c7a74233a
sha512 : 408b27d3097eea5a46bf2ab6433a7234a33d5e49957b13ec7acc2ca08e1a13c75272c90c8d3385d47ede5420a7a9623aad817d9f8a70bd100a0acea7400daa59
sha512_224 : c3b8cfaa37ae15922adf3d21606e3a9836ba2a9d7838b040b7c96fd7
sha512_256 : ad0a339b08dc090fe3b16eae376f7e162836e8728da9c45466842e19508d7627
(( bcrypt("password", 10) ))
La fonction bcrypt
génère un hachage de mot de passe BCrypt pour la chaîne donnée en utilisant le facteur de coût spécifié (par défaut à 10, en cas de manque).
Par exemple:
hash : (( bcrypt("password", 10) ))
évalue à
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
(( bcrypt_check("password", hash) ))
La fonction bcrypt_check
valide un mot de passe par rapport à un hachage Bcrypt donné.
Par exemple:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
évalue à
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : true
(( md5crypt("password") ))
La fonction md5crypt
génère un hachage de mot de passe crypté Apache MD5 pour la chaîne donnée.
Par exemple:
hash : (( md5crypt("password") ))
évalue à
hash : $apr1$3Qc1aanY$16Sb5h7U1QrcqwZbDJIYZ0
(( md5crypt_check("password", hash) ))
La fonction md5crypt_check
valide un mot de passe contre un hachage crypté Apache MD5 donné.
Par exemple:
hash : $2a$10$b9RKb8NLuHB.tM9haPD3N.qrCsWrZy8iaCD4/.cCFFCRmWO4h.koe
valid : (( bcrypt_check("password", hash) ))
évalue à
hash : $apr1$B77VuUUZ$NkNFhkvXHW8wERSRoi74O1
valid : true
(( decrypt("secret") ))
Cette fonction peut être utilisée pour stocker des secrets cryptés dans un fichier SPIFF YAML. Le résultat traité contiendra alors la valeur décryptée. Tous les types de nœuds peuvent être cryptés et déchiffrés, y compris des cartes et des listes complètes.
Le mot de passe du déchiffrement peut être donné comme deuxième argument, soit (la manière préférée), il peut être spécifié par la variable d'environnement SPIFF_ENCRYPTION_KEY
.
Un dernier argument facultatif peut sélectionner la méthode de cryptage. La seule méthode prise en charge jusqu'à présent est 3DES
. D'autres méthodes peuvent être ajoutées pour les versions SPIFF dédiées en utilisant l'enregistrement des méthodes de chiffrement offerte par la bibliothèque SPIFF.
Une valeur peut être cryptée à l'aide de la fonction encrypt("secret")
.
Par exemple:
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) ))
évalué à quelque chose comme
decrypted : spiff is a cool tool
encrypted : d889f9e4cc7ae13effcbc8bb8cd0c38d1fb2197738444f753c48796d7946083e6639e5a1bf8f77648f2a1ddf37023c65ff57d52d0519d1d92cbcf87d3e263cba
password : this a very secret secret and may never be exposed to unauthorized people
(( rand("[:alnum:]", 10) ))
La fonction rand
génère des valeurs aléatoires. Le premier argument décide quel type de valeurs sont demandées. Sans argument, il génère un nombre aléatoire positif dans la gamme int64
.
Type d'argument | résultat |
---|---|
int | valeur entière dans la plage [0, n ) pour n et ( n , n, 0] positifs pour n négatif |
bouffon | valeur booléenne |
chaîne | Une chaîne de rune, où la rune se trouve dans la plage de caractères donnée, toute combinaison de classes de caractères ou de plages de caractères utilisables pour regexp peut être utilisée. Si un argument de longueur supplémentaire est spécifié, la chaîne résultante aura la longueur donnée. |
Par exemple:
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) ))
évalue à
int : 8037669378456096839
int10 : 7
neg10 : -5
bool : true
string : ghhjAYPMlD
upper : LBZQFRSURL
alnum : 0p6KS7EhAj
punct : ' &{;,^])"(# '
(( type(foobar) ))
Le type
de fonction donne une chaîne dénotant le type de l'expression donnée.
Par exemple:
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(~~) ))
évalue les types à
types :
- int : int
- float : float
- bool : bool
- string : string
- list : list
- map : map
- lambda : lambda
- template : template
(( defined(foobar) ))
La fonction defined
vérifie si une expression peut être évaluée avec succès. Il donne la valeur booléenne true
, si l'expression peut être évaluée et false
autrement.
Par exemple:
zero : 0
div_ok : (( defined(1 / zero ) ))
zero_def : (( defined( zero ) ))
null_def : (( defined( null ) ))
évalue à
zero : 0
div_ok : false
zero_def : true
null_def : false
Cette fonction peut être utilisée en combinaison de l'opérateur conditionnel pour évaluer les expressions en fonction de la résolution d'une autre expression.
(( valid(foobar) ))
La fonction valid
vérifie si une expression peut être évaluée avec succès et évalue à une valeur définie qui n'est pas égale à nil
. Il donne la valeur booléenne true
, si l'expression peut être évaluée et false
autrement.
Par exemple:
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 ) ))
évalue à
zero : 0
empty : null
map : {}
list : []
div_ok : false
zero_def : true
null_def : false
empty_def : false
map_def : true
list_def : true
(( require(foobar) ))
La fonction require
une erreur si l'argument donné n'est pas défini ou nil
, sinon il donne la valeur donnée.
Par exemple:
foo : ~
bob : (( foo || "default" ))
alice : (( require(foo) || "default" ))
évalue à
foo : ~
bob : ~
alice : default
(( stub(foo.bar) ))
Le stub
fonction donne la valeur d'un champ dédié trouvé dans le premier stub en amont qui le définit.
Par exemple:
template.yml
value : (( stub(foo.bar) ))
fusionné avec Stub
Stub.yml
foo :
bar : foobar
évalue à
value : foobar
L'argument transmis à cette fonction doit être un littéral de référence ou une expression évaluant à une chaîne désignant une référence ou une liste de chaînes dénotant la liste des éléments de chemin pour la référence. Si aucun argument ou un non défini ( ~~
) n'est donné, le chemin de champ réel est utilisé.
Veuillez noter qu'une référence unique donnée ne sera pas évaluée comme expression, si sa valeur doit être utilisée, elle doit être transformée en une expression, par exemple en désignant (ref)
ou [] ref
pour une expression de liste.
Alternativement, l'opération merge
pourrait être utilisée, par exemple merge foo.bar
. La différence est que stub
ne fusionne pas, par conséquent, le champ sera toujours fusionné (avec le chemin d'origine dans le document).
(( tagdef("tag", value) ))
La fonction tagdef
peut être utilisée pour définir des balises dynamiques (voir les balises). Contrairement au marqueur de balise, cette fonction permet de spécifier le nom de balise et sa valeur prévue par une expression. Par conséquent, il peut être utilisé pour composer des éléments comme map
ou sum
pour créer une balise dynamique avec des valeurs calculées.
Un troisième argument facultatif peut être utilisé pour spécifier la portée prévue ( local
ou global
). Par défaut, une balise locale est créée. Les balises locales sont visibles uniquement au niveau de traitement réel (modèle ou sous), tandis que les balises globales, une fois définies, peuvent être utilisées dans tous les niveaux de traitement supplémentaires (talon ou modèle).
Alternativement, le nom de balise peut être préfixé avec un début ( *
) pour déclarer une balise globale.
La valeur de balise spécifiée sera utilisée comme résultat pour la fonction.
Par exemple:
template.yml
value : (( tagdef("tag:alice", 25) ))
alice : (( tag:alice::. ))
évalue à
value : 25
alice : 25
(( eval(foo "." bar ) ))
Évaluez à nouveau le résultat de l'évaluation d'une expression de chaîne en tant qu'expression de dynaml. Cela peut, par exemple, être utilisé pour réaliser des indirections.
par exemple: l'expression dans
alice :
bob : married
foo : alice
bar : bob
status : (( eval( foo "." bar ) ))
calcule le chemin vers un champ, qui est ensuite évalué à nouveau pour donner la valeur de ce champ composé:
alice :
bob : married
foo : alice
bar : bob
status : married
(( env("HOME" ) ))
Lisez la valeur d'une variable d'environnement dont le nom est donné comme une expression de dynaml. Si la variable d'environnement n'est pas définie, l'évaluation échoue.
Dans une deuxième saveur, la fonction env
accepte plusieurs arguments et / ou arguments de liste, qui sont joints à une seule liste. Chaque entrée de cette liste est utilisée comme nom d'une variable d'environnement et le résultat de la fonction est une carte des variables données données en tant qu'élément YAML. Par la présente, les variables d'environnement inexistantes sont omises.
(( parse(yamlorjson) ))
Analyser une chaîne YAML ou JSON et renvoyez le contenu en valeur YAML. Il peut donc être utilisé pour une évaluation dynamale supplémentaire.
Par exemple:
json : |
{ "alice": 25 }
result : (( parse( json ).alice ))
donne la valeur 25
pour le result
du champ.
La fonction parse
prend en charge un deuxième argument facultatif, le mode Parse . Ici, les mêmes modes sont possibles comme pour la fonction de lecture. Le mode d'analyse par défaut est import
, le contenu est juste analysé et il n'y a pas d'autres évaluations au cours de cette étape.
(( asjson(expr) ))
Cette fonction transforme une valeur YAML donnée par son argument en une chaîne JSON . La fonction correspondante asyaml
donne la valeur YAML sous forme de chaîne de document YAML .
Par exemple:
data :
alice : 25
mapped :
json : (( asjson(.data) ))
yaml : (( asyaml(.data) ))
se résout à
data :
alice : 25
mapped :
json : ' {"alice":25} '
yaml : |+
alice: 25
(( catch(expr) ))
Cette fonction exécute une expression et donne une carte d'informations d'évaluation. Il réussit toujours, même si l'expression échoue. La carte comprend les champs suivants:
nom | taper | signification |
---|---|---|
valid | bouffon | L'expression est valide |
error | chaîne | Le texte du message d'erreur de l'évaluation |
value | n'importe lequel | la valeur de l'expression, si l'évaluation a réussi |
Par exemple:
data :
fail : (( catch(1 / 0) ))
valid : (( catch( 5 * 5) ))
se résout à
data :
fail :
error : division by zero
valid : false
valid :
error : " "
valid : true
value : 25
(( static_ips(0, 1, 3) ))
Générez une liste d'IPS statiques pour un emploi.
Par exemple:
jobs :
- name : myjob
instances : 2
networks :
- name : mynetwork
static_ips : (( static_ips(0, 3, 4) ))
Cela créera 3 IPS à partir du sous-réseau mynetwork
et renverra deux entrées, car il n'y a que deux instances. Les deux entrées seront les 0e et 3e décalages des gammes IP statiques définies par le réseau.
Par exemple, étant donné le fichier bye.yml :
networks : (( merge ))
jobs :
- name : myjob
instances : 3
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
et fichier 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
retours
jobs :
- instances : 3
name : myjob
networks :
- name : cf1
static_ips :
- 10.60.3.10
- 10.60.3.13
- 10.60.3.70
networks :
- name : cf1
subnets :
- cloud_properties :
security_groups :
- cf-0-vpc-c461c7a1
subnet : subnet-e845bab1
dns :
- 10.60.3.2
gateway : 10.60.3.1
name : default_unused
range : 10.60.3.0/24
reserved :
- 10.60.3.2 - 10.60.3.9
static :
- 10.60.3.10 - 10.60.3.70
type : manual
.
Si bye.yml était à la place
networks : (( merge ))
jobs :
- name : myjob
instances : 2
networks :
- name : cf1
static_ips : (( static_ips(0,3,60) ))
spiff merge bye.yml hi.yml
Renvoie plutôt
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
accepte également les arguments de la liste, tant que tous les éléments contenus transitivants sont à nouveau des listes ou des valeurs entières. Cela permet d'abréger la liste des IP comme suit:
static_ips: (( static_ips([1..5]) ))
(( ipset(ranges, 3, 3,4,5,6) ))
Alors que la fonction static_ips pour des raisons historiques repose sur la structure d'un manifeste Bosh et ne fonctionne que dans des emplacements dédiés dans le manifeste, la fonction IPSET offre un calcul similaire purement basé sur ses arguments. Ainsi, les gammes IP disponibles et les numéros requis d'IPS sont passés sous forme d'arguments.
Le premier argument (Ranges) peut être une seule plage en tant que simple chaîne ou une liste de chaînes. Chaque chaîne peut être
Le deuxième argument spécifie le nombre demandé d'adresses IP dans l'ensemble de résultats.
Les arguments supplémentaires spécifient les indices des IP à choisir (à partir de 0) dans les plages données. Ici encore, des listes d'indices peuvent être utilisées.
Par exemple:
ranges :
- 10.0.0.0 - 10.0.0.255
- 10.0.2.0/24
ipset : (( ipset(ranges,3,[256..260]) ))
résout ipset en [ 10.0.2.0, 10.0.2.1, 10.0.2.2 ]
.
Si aucun indice IP n'est spécifié (seulement deux arguments), les IP sont choisis à partir du début de la première plage jusqu'à la fin de la dernière plage donnée, sans indirection.
(( list_to_map(list, "key") ))
Une liste des entrées de carte avec des champs de nom / clés explicites sera mappée sur une carte avec les clés dédiées. Par défaut, le name
de champ de clé est utilisé, qui peut changer par le deuxième argument facultatif. Un champ clé explicitement indiqué dans la liste sera également pris en compte.
Par exemple:
list :
- key:foo : alice
age : 24
- foo : bob
age : 30
map : (( list_to_map(list) ))
sera mappé à
list :
- foo : alice
age : 24
- foo : bob
age : 30
map :
alice :
age : 24
bob :
age : 30
En combinaison avec des modèles et des expressions de lambda, il peut être utilisé pour générer des cartes avec des valeurs clés nommées arbitrairement, bien que les expressions de dynamls ne soient pas autorisées pour les valeurs clés.
(( makemap(fieldlist) ))
Dans cette saveur, makemap
crée une carte avec des entrées décrites par la liste de champs donnée. La liste devrait contenir des cartes avec la key
et value
des entrées, décrivant les entrées de carte dédiées.
Par exemple:
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map : (( makemap(list) ))
rendements
list :
- key : alice
value : 24
- key : bob
value : 25
- key : 5
value : 25
map :
" 5 " : 25
alice : 24
bob : 25
Si la valeur clé est un booléen ou un entier, il sera mappé à une chaîne.
(( makemap(key, value) ))
Dans cette saveur, makemap
crée une carte avec des entrées décrites par les paires d'arguments donnés. Les arguments peuvent être une séquence de paires de clés / valeurs (données par des arguments distincts).
Par exemple:
map : (( makemap("peter", 23, "paul", 22) ))
rendements
map :
paul : 22
peter : 23
Contrairement à la saveur makemap
précédente, celle-ci pourrait également être gérée par les littéraux de la carte.
(( merge(map1, map2) ))
À côté du mot-clé merge
il existe également une fonction appelée merge
(elle doit toujours être suivie d'un support d'ouverture). Il peut être utilisé pour fusionner les cartes sévères tirées du document réel analogue au processus de fusion de talons. Si les cartes sont spécifiées par des expressions de référence, elles ne peuvent contenir aucune expression de dynaml , car elles sont toujours évaluées dans le contexte du document réel avant d'évaluer les arguments.
Par exemple:
map1 :
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
résout result
de
result :
alice : 26
bob : 24 # <---- expression evaluated before mergeing
Alternativement, les modèles de cartographie peuvent être passés (sans opérateur d'évaluation!). Dans ce cas, les expressions Dynaml du modèle sont évaluées lors de la fusion des documents donnés comme pour les appels réguliers de SPIFF Merge .
Par exemple:
map1 :
<< : (( &template ))
alice : 24
bob : (( alice ))
map2 :
alice : 26
peter : 8
result : (( merge(map1,map2) ))
résout result
de
result :
alice : 26
bob : 26
Une carte peut également être donnée par une expression de carte. Ici, il est possible de spécifier des expressions de dynamls à l'aide de la syntaxe habituelle:
Par exemple:
map1 :
alice : 24
bob : 25
map2 :
alice : 26
peter : 8
result : (( merge(map1, map2, { "bob"="(( carl ))", "carl"=100 }) ))
résout result
de
result :
alice : 26
bob : 100
Au lieu de plusieurs arguments, un argument de liste unique peut être donné. La liste doit contenir les cartes à fusionner.
Les fusions imbriquées ont accès à toutes les liaisons extérieures. Les références relatives sont d'abord recherchées dans le document réel. S'ils ne sont pas trouvés, toutes les liaisons externes sont utilisées pour rechercher la référence, des liaisons intérieures aux liaisons extérieures. De plus, le contexte ( __ctx
) propose un champ OUTER
, qui est une liste de tous les documents extérieurs des fusions imbriquées, qui peuvent être utilisées pour rechercher des références absolues.
Par exemple:
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) ))
résout merged
merged :
bob : 25
outer1 :
alice :
age : 24
outer2 : 24
sum : 49
(( intersect(list1, list2) ))
La fonction intersect
croit plusieurs listes. Une liste peut contenir des entrées de tout type.
Par exemple:
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) ))
résout intersect
intersect :
- - a
- a
- { a: b }
- 0
- " 0 "
(( reverse(list) ))
La fonction reverse
inverse l'ordre d'une liste. La liste peut contenir des entrées de tout type.
Par exemple:
list :
- - a
- b
- { a: b }
- { b: c }
- 0
- 1
reverse : (( reverse(list) ))
se résout reverse
de
reverse :
- 1
- 0
- { b: c }
- { a: b }
- b
- - a
(( validate(value,"dnsdomain") ))
La fonction validate
valide une expression à l'aide d'un ensemble de validateurs. Le premier argument est la valeur à valider et tous les autres arguments sont des validateurs qui doivent réussir à accepter la valeur. Si au moins un validateur échoue, un message d'erreur approprié est généré, cela explique la raison de l'échec.
Un validateur est indiqué par une chaîne ou une liste contenant le type de validateur sous forme de chaîne et ses arguments. Un validateur peut être annulé avec un précédent !
en son nom.
Les validateurs suivants sont disponibles:
Taper | Arguments | Signification |
---|---|---|
empty | aucun | Liste, carte ou chaîne vide |
dnsdomain | aucun | Nom de domaine DNS |
wildcarddnsdomain | aucun | Nom de domaine DNS |
dnslabel | aucun | étiquette DNS |
dnsname | aucun | domaine DNS ou domaine générique |
ip | aucun | adresse IP |
cidr | aucun | cière |
publickey | aucun | clé publique au format PEM |
privatekey | aucun | clé privée au format PEM |
certificate | aucun | certificat au format PEM |
ca | aucun | certificat pour ca |
semver | Liste facultative des contraintes | Valider la version Semver contre les contraintes |
type | Liste des touches de type acceptées | Au moins une clé de type doit correspondre |
valueset | Listez un argument avec des valeurs | valeurs possibles |
value ou = | valeur | Vérifier la valeur dédiée |
gt ou > | valeur | supérieur à (nombre / chaîne) |
lt ou < | valeur | moins de (nombre / chaîne) |
ge ou >= | valeur | plus grand ou égal à (nombre / chaîne) |
le ou <= | valeur | moins ou égal à (nombre / chaîne) |
match ou ~= | expression régulière | Valeur de la chaîne correspondant à l'expression régulière |
list | Liste facultative des validateurs d'entrée | La liste et les entrées correspondent à des validateurs |
map | [[<Key Validator>,] <Validator d'entrée>] | Est-ce que la carte et les clés et les entrées correspondent aux validateurs |
mapfield | <nom de champ> [, <validator>] | Entrée requise dans la carte |
optionalfield | <nom de champ> [, <validator>] | Entrée facultative dans la carte |
and | liste des validateurs | Tous les validateurs doivent réussir |
or | liste des validateurs | Au moins un validateur doit réussir |
not ou ! | validateur | nier le ou les arguments validateurs |
Si la validation réussit, la valeur est renvoyée.
Par exemple:
dnstarget : (( validate("192.168.42.42", [ "or", "ip", "dnsdomain" ]) ))
évalue à
dnstarget : 192.168.42.42
Si la validation échoue, une erreur expliquant la raison de l'échec est générée.
Par exemple:
dnstarget : (( validate("alice+bob", [ "or", "ip", "dnsdomain" ]) ))
donne l'erreur suivante:
*condition 1 failed: (is no ip address: alice+bob and is no dns domain: [a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')])
Un validateur peut également être une expression de lambda prenant au moins un argument et renvoyant une valeur booléenne. De cette façon, il est possible de fournir les propres validateurs dans le cadre du document YAML.
Par exemple:
val : (( validate( 0, |x|-> x > 1 ) ))
Si plusieurs paramètres sont déclarés, les arguments supplémentaires doivent être spécifiés sous forme d'arguments de validateur. Le premier argument est toujours la valeur à vérifier.
Par exemple:
val : (( validate( 0, [|x,m|-> x > m, 5] ) ))
La fonction lambda peut également renvoyer une liste avec 1, 2 ou 3 éléments. Cela peut être utilisé pour fournir des messages appropriés.
Indice | Signification |
---|---|
0 | Le premier index est toujours le résultat du match, il doit être évaluable en tant que booléen |
1 | Si deux éléments sont donnés, le deuxième index est le message décrivant le résultat réel |
2 | Ici, l'index 1 décrit le message de réussite et 2 le message de défaillance |
Par exemple:
val : (( validate( 6, [|x,m|-> [x > m, "is larger than " m, "is less than or equal to " m], 5] ) ))
Pour mentionner, la spécification du validateur peut être donnée en ligne comme indiqué dans les exemples ci-dessus, mais comme des expressions de référence, également. Les validateurs not
, and
/ or
acceptent les spécifications de validateur profondément imbriquées.
Par exemple:
dnsrecords :
domain : 1.2.3.4
validator :
- map
- - or # key validator
- dnsdomain
- wildcarddnsdomain
- ip # entry validator
val : (( validate( map, validator) ))
(( check(value,"dnsdomain") ))
La check
de la fonction peut être utilisée pour correspondre à une structure YAML contre un vérificateur de valeur basé sur YAML. Par la présente, la même description de chèque déjà décrite pour valider peut être utilisée. Le résultat de l'appel est une valeur booléenne indiquant le résultat de la correspondance. Il n'échoue pas si le chèque échoue.
(( error("message") ))
L' error
de fonction peut être utilisée pour provoquer des défaillances d'évaluation explicites avec un message dédié.
Cela peut être utilisé, par exemple, pour réduire une erreur de traitement complexe à un message significatif en ajoutant la fonction d'erreur par défaut pour l'expression de Comples potentiellement défaillante.
Par exemple:
value : (( <some complex potentially failing expression> || error("this was an error my friend") ))
Un autre scénario pourrait être d'omettre un message descriptif pour les champs requis manquants en utilisant une valeur d'expression d'erreur comme (par défaut) pour un champ destiné à être défini dans un stub en amont.
Dynaml Prise en charge de diverses fonctions mathématiques:
Retour des entiers: ceil
, floor
, tournée round
et roundtoeven
Retour des flotteurs ou entiers: abs
Retour des flotteurs: sin
, cos
, sinh
, cosh
, asin
, acos
, asinh
, acosh
, sqrt
, exp
, log
, log10
,
Dynaml prend en charge diverses conversions de types entre les valeurs integer
, float
, bool
et string
par des fonctions appropriées.
Par exemple:
value : (( integer("5") ))
convertit une chaîne en valeur entière.
La conversion d'un entier en une chaîne accepte un argument entier supplémentaire facultatif pour spécifier la base pour la conversion, par exemple string(55,2)
entraînera "110111"
. La base par défaut est 10. La base doit être comprise entre 2 et 36.
SPIFF prend en charge l'accès au contenu en dehors du modèle et des fichiers sous. Il est possible de lire des fichiers, d'exécuter des commandes et des pipelines. Toutes ces fonctions existent en deux saveurs.
sync
est utilisée, qui est destinée à synchroniser le traitement du modèle avec un état de dédicace (fourni par le contenu externe). Ici, les opérations de mise en cache ne seraient pas utiles, il y a donc une deuxième saveur non-achetée. Chaque fonction est disponible avec le suffixe _uncached
(par exemple read_uncached()
) (( read("file.yml") ))
Lisez un fichier et renvoyez son contenu. Il existe une prise en charge de trois types de contenu: fichiers yaml
, fichiers text
et fichiers binary
. La lecture en mode binaire se traduira par une chaîne multi-ligne codée Base64.
Si le suffixe de fichier est .yml
, .yaml
ou .json
, par défaut, le type YAML est utilisé. Si le fichier doit être lu en text
, ce type doit être explicitement spécifié. Dans tous les autres cas, la valeur par défaut est text
, la lecture d'un fichier binaire (par exemple une archive) nécessite urgemment de spécifier le mode binary
.
Un deuxième paramètre facultatif peut être utilisé pour spécifier explicitement le type de retour souhaité: yaml
ou text
. Pour les documents YAML , certains types d'addtion sont pris en charge: multiyaml
, template
, templates
, import
et importmulti
.
Un document YAML sera analysé et l'arbre est retourné. Les éléments de l'arbre sont accessibles par des expressions de dynamls régulières.
De plus, le fichier YAML peut à nouveau contenir des expressions Dynaml. Toutes les expressions de dynamaux incluses seront évaluées dans le contexte de l'expression de lecture. Cela signifie que le même fichier inclus à différents endroits dans un document YAML peut entraîner différents sous-arbres, selon les expressions de dynamls utilisées.
Si cela est également possible de lire un YAML à plusieurs documents. Si le type multiyaml
est donné, un nœud de liste avec les nœuds racine du document YAML est renvoyé.
Le document YAML ou JSON peut également lire comme modèle en spécifiant le template
de type. Ici, le résultat sera une valeur de modèle, qui peut être utilisée comme des modèles en ligne réguliers. Si templates
sont spécifiés, un multi-documents est mappé sur une liste de modèles.
Si le type de lecture est défini sur import
, le contenu du fichier est lu en tant que document YAML et le nœud racine est utilisé pour remplacer l'expression. Les expressions de dynamls potentielles contenues dans le document ne seront pas évaluées avec la liaison réelle de l'expression avec l'appel de lecture, mais comme il aurait fait partie du fichier d'origine. Par conséquent, ce mode ne peut être utilisé que s'il n'y a pas de traitement supplémentaire du résultat de lecture ou si les valeurs livrées ne sont pas transformées.
Ceci peut être utilisé avec une référence enchaînée (pour Examle (( read(...).selection ))
) pour délire un fragment dédié du document importé. Ensuite, l'évaluation sera effectuée pour la partie sélectionnée, seulement. Les expressions et les références dans les autres parties ne sont pas évaluées et du tout et ne peuvent pas entraîner une erreur.
Par exemple:
template.yaml
ages :
alice : 25
data : (( read("import.yaml", "import").first ))
import.yaml
first :
age : (( ages.alice ))
second :
age : (( ages.bob ))
n'échouera pas, car la second
section n'est jamais évaluée.
Ce mode doit être pris avec prudence, car il conduit souvent à des résultats inattendus.
Le type de lecture importmulti
peut être utilisé pour importer des fichiers YAML multi-documents comme une liste de nœuds.
Un document texte sera renvoyé en tant que chaîne unique.
Il est également possible de lire des documents binaires. Le contenu ne peut pas être utilisé directement comme une chaîne (ou un document YAML). Par conséquent, le binary
en mode de lecture doit être spécifié. Le contenu est renvoyé en tant que valeur de chaîne multi-ligne codée Base64.
(( exec("command", arg1, arg2) ))
Exécuter une commande. Les arguments peuvent être des expressions de dynamls, y compris les expressions de référence évaluées aux listes ou aux cartes. Les listes ou cartes sont passées sous forme d'arguments uniques contenant un document YAML avec le fragment donné.
Le résultat est déterminé en analysant la sortie standard de la commande. Il peut s'agir d'un document YAML ou d'une seule chaîne multi-lignes ou d'une valeur entière. Un document YAML doit commencer par le préfixe de document ---
. Si la commande échoue, l'expression est gérée comme non définie.
par exemple
arg :
- a
- b
list : (( exec( "echo", arg ) ))
string : (( exec( "echo", arg.[0] ) ))
rendements
arg :
- a
- b
list :
- a
- b
string : a
Alternativement, exec
peut être appelé avec un argument de liste unique décrivant complètement la ligne de commande.
La même commande sera exécutée une fois, même si elle est utilisée dans plusieurs expressions.
(( pipe(data, "command", arg1, arg2) ))
Exécutez une commande et alimentez son entrée standard avec des données dédiées. L'argument de commande doit être une chaîne. Les arguments pour la commande peuvent être toutes les expressions de dynamls, y compris les expressions de référence évaluées aux listes ou aux cartes. Les listes ou cartes sont passées sous forme d'arguments uniques contenant un document YAML avec le fragment donné.
Le flux d'entrée est généré à partir des données données. S'il s'agit d'un type simple, sa représentation de chaîne est utilisée. Sinon, un document YAML est généré à partir des données d'entrée. Le résultat est déterminé en analysant la sortie standard de la commande. Il peut s'agir d'un document YAML ou d'une seule chaîne multi-lignes ou d'une valeur entière. Un document YAML doit commencer par le préfixe de document ---
. Si la commande échoue, l'expression est gérée comme non définie.
par exemple
data :
- a
- b
list : (( pipe( data, "tr", "a", "z") ))
rendements
arg :
- a
- b
list :
- z
- b
Alternativement, pipe
peut être appelé avec des données et un argument de liste décrivant complètement la ligne de commande.
La même commande sera exécutée une fois, même si elle est utilisée dans plusieurs expressions.
(( write("file.yml", data) ))
Écrivez un fichier et renvoyez son contenu. Si le résultat peut être analysé en tant que document YAML, le document est renvoyé. Un argument 3ème en option peut être utilisé pour transmettre les options d'écriture. Les arguments d'option peuvent être un entier indiquant les autorisations de fichiers (par défaut est 0644
) ou une chaîne séparée par des virgules avec des options. Les options prises en charge sont
binary
: les données sont décodées de la base64 avant d'écrire0
leader 0 indique une valeur octale. (( tempfile("file.yml", data) ))
Écrivez un fichier temporaire AA et renvoyez son nom de chemin. Un argument 3ème en option peut être utilisé pour transmettre des options d'écriture. Il se comporte essentiellement comme write
Attention : un fichier temporaire n'existe que pendant le traitement de la fusion. Il sera supprimé par la suite.
Il peut être utilisé, par exemple, pour fournir un argument de fichier temporaire pour la fonction exec
.
(( lookup_file("file.yml", list) ))
Recherche Un fichier est une liste de répertoires. Le résultat est une liste des fichiers existants. Avec lookup_dir
il est possible de rechercher un répertoire.
Si aucun fichier existant ne peut être trouvé, la liste vide est renvoyée.
Il est possible de transmettre plusieurs arguments de liste ou de chaîne pour composer le chemin de recherche.
(( mkdir("dir", 0755) ))
Créez un répertoire et tous ses répertoires intermédiaires s'ils n'existent pas encore.
La pièce d'autorisation est facultative (par défaut 0755). Le chemin du répertoire peut être donné par ATRING comme la valeur ou comme liste de composants de chemin.
(( list_files(".") ))
Liste les fichiers dans un répertoire. Le résultat est une liste des fichiers existants. Avec list_dirs
il est possible de répertorier les répertoires.
(( archive(files, "tar") ))
Créez une archive du type donné (par défaut est tar
) contenant les fichiers répertoriés. Le résultat est l'archive codée Base64.
Les types d'archives pris en charge sont tar
et targz
.
files
peuvent être une liste ou une carte des entrées de fichiers. En cas de carte, la clé de carte est utilisée par défaut pour le chemin du fichier. Une entrée de fichier est une carte avec les champs suivants:
champ | taper | signification |
---|---|---|
path | chaîne | Facultatif pour les cartes, le chemin du fichier dans l'archive, par défaut par la touche de carte |
mode | String int ou int | Mode de fichier ou options d'écriture. Il se comporte essentiellement comme l'argument de l'option pour write . |
data | n'importe lequel | Contenu de fichier, YAML sera rassemblé en tant que document YAML. Si mode indique le mode binaire, une valeur de chaîne sera décodée Base64. |
base64 | chaîne | Base64 Données binaires codées |
Par exemple:
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"))) ))
donne:
archive : |-
H4sIAAAAAAAA/+zVsQqDMBAG4Mx5igO3gHqJSQS3go7tUHyBqIEKitDEoW9f
dLRDh6KlJd/yb8ll+HOd8SY1qbfOJw8zDmQHiIhayjURcZuIQhOeSZlphVwL
glwsAXvM8mJ23twJ4qfnbB/3I+I4pmboW1uA0LSZmgJETr89VXCUtf9Neq1O
5blKxm6PO972X/FN/7nKVej/EaIogto6D+XUzpQydpm8ZayA+tY76B0YWHYD
DV9CEATBf3kGAAD//5NlAmIADAAA
content :
- -rw-r--r-- 0/0 22 2019-03-18 09:01 data/a/test.yaml
- -rw-r--r-- 0/0 41 2019-03-18 09:01 data/b/README.md
files :
data/a/test.yaml :
data :
alice : 26
bob : 27
data/b/README.md :
data : |+
### Test Docu
**Note**: This is a test
yaml :
alice : 26
bob : 27
SPIFF prend en charge la gestion des noms de version sémantique. Il prend en charge toutes les fonctionnalités du package Masterminds Semver acceptant les versions avec ou sans v
(( semver("v1.2-beta.1") ))
Vérifiez si une chaîne donnée est une version sémantique et renvoyez sa forme normalisée (sans leader v
et partie de version complète avec le numéro de version majeure, mineure et et du correctif).
Par exemple:
normalized : (( semver("v1.2-beta.1") ))
se résout à
normalized : 1.2.0-beta.1
(( semverrelease("v1.2.3-beta.1") ))
Renvoyez la partie de la version d'une version sémantique omettant des métadonnées et des informations de prérélease.
Par exemple:
release : (( semverrelease("v1.2.3-beta.1") ))
se résout à
release : v1.2.3
Si un argument de chaîne supplémentaire est donné, cette fonction remplace la version par la version de la version sémantique donnée de préserver les métadonnées et les informations de prérélease.
Par exemple:
new : (( semverrelease("1.2.3-beta.1", "1.2.1) ))
se résout à
new : 1.2.1-beta.1
(( semvermajor("1.2.3-beta.1") ))
Déterminez le principal numéro de version de la version sémantique donnée. Le résultat est un entier.
Par exemple:
major : (( semvermajor("1.2.3-beta.1") ))
se résout à
major : 1
La fonction semverincmajor
peut être utilisée pour incrémenter le numéro de version majeur et réinitialiser la version mineure, la version patch et la publication des suffixes.
Par exemple:
new : (( semverincmajor("1.2.3-beta.1") ))
se résout à
new : 2.0.0
(( semverminor("1.2.3-beta.1") ))
Déterminez le numéro de version mineure de la version sémantique donnée. Le résultat est un entier.
Par exemple:
minor : (( semverminor("1.2.3-beta.1") ))
se résout à
minor : 2
La fonction semverincminor
peut être utilisée pour incrémenter le numéro de version mineure et réinitialiser la version de patch et publier les suffixes.
Par exemple:
new : (( semverincmajor("v1.2.3-beta.1") ))
se résout à
new : v1.3.0
(( semverpatch("1.2.3-beta.1") ))
Déterminez le numéro de version du patch de la version sémantique donnée. Le résultat est un entier.
Par exemple:
patch : (( semverpatch("1.2.3-beta.1") ))
se résout à
patch : 3
La fonction semverincpatch
peut être utilisée pour incrémenter le numéro de version du patch ou réinitialiser les suffixes de version. S'il y a des suffixes Rlease, ils sont supprimés et les informations de version sont maintenues inchangées, sinon le numéro de version du patch est augmenté.
Par exemple:
final : (( semverincpatch("1.2.3-beta.1") ))
new : (( semverincpatch(final) ))
se résout à
final : 1.2.3
new : 1.2.4
(( semverprerelease("1.2.3-beta.1") ))
Déterminez la présélection de la version sémantique donnée. Le résultat est une chaîne.
Par exemple:
prerelease : (( semverprerelease("1.2.3-beta.1") ))
se résout à
prerelease : beta.1
Si un argument de chaîne supplémentaire est donné cette fonction définit, remplace ou efface (si défini sur la chaîne vide), la pré-alimentation
Par exemple:
new : (( semverprerelease("1.2.3-beta.1", "beta.2) ))
se résout à
new : 1.2.3-beta.2
(( semvermetadata("1.2.3+demo") ))
Déterminez les métadonnées de la version sémantique donnée. Le résultat est une chaîne.
Par exemple:
metadata : (( semvermetadata("1.2.3+demo") ))
se résout à
metadata : demo
Si un argument de chaîne supplémentaire est donné cette fonction définit, remplace ou efface (si défini sur la chaîne vide) les métadonnées.
Par exemple:
new : (( semvermetadata("1.2.3-test", "demo) ))
se résout à
new : 1.2.3+demo
(( semvercmp("1.2.3", 1.2.3-beta.1") ))
Comparez deux versions sémantiques. Une prérémité est toujours plus petite que la version finale. Le résultat est un entier avec les valeurs suivantes:
résultat | signification |
---|---|
-1 | La première version est avant la deuxième version |
0 | Les deux versions sont égales |
1 | Le premier Versuon est après le deuxième |
Par exemple:
compare : (( semvercmp("1.2.3", "1.2.3-beta.1") ))
se résout à
compare : 1
(( semvermatch("1.2.3", "~1.2") ))
Faites correspondre la version sémantique donnée par rapport à une liste de contre-contraintes. Le résultat est un booléen. Il est possible de spécifier n'importe quel nombre de contraintes de version. Si aucune contrainte n'est donnée, la fonction vérifie simplement si la chaîne donnée est une version sémantique.
Par exemple:
match : (( semvermatch("1.2.3", "~1.2") ))
se résout à
match : true
La liste complète des spécifications de contraintes possibles peut être trouvée ici.
(( semversort("1.2.3", "1.2.1") ))
Triez une liste des versions dans l'ordre croissant. Un v
leader est conservé.
Par exemple:
sorted : (( semversort("1.2.3", "1.2.1") ))
se résout à
sorted :
- 1.2.1
- 1.2.3
La liste des versions à tri peut également être spécifiée avec un seul argument de liste.
SPIFF prend en charge quelques fonctions utiles pour travailler avec les certificats et les clés X509 . Veuillez vous référer également à la section utile à savoir pour trouver quelques conseils pour fournir un état.
(( x509genkey(spec) ))
Cette fonction peut être utilisée générer des touches RSA ou ECDSA privées. Le résultat sera une touche codée PEM en tant que valeur de chaîne multi-lignes. Si une taille de clé (entier ou chaîne) est donnée comme argument, une touche RSA sera générée avec la taille de la clé donnée (par exemple 2048). Compte tenu de l'une des valeurs de chaîne
La fonction générera une clé ECDSA appropriée.
Par exemple:
keys :
key : (( x509genkey(2048) ))
se résout à quelque chose comme
key : |+
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwxdZDfzxqz4hlRwTL060pm1J12mkJlXF0VnqpQjpnRTq0rns
CxMxvSfb4crmWg6BRaI1cEN/zmNcT2sO+RZ4jIOZ2Vi8ujqcbzxqyoBQuMNwdb32
...
oqMC9QKBgQDEVP7FDuJEnCpzqddiXTC+8NsC+1+2/fk+ypj2qXMxcNiNG1Az95YE
gRXbnghNU7RUajILoimAHPItqeeskd69oB77gig4bWwrzkijFXv0dOjDhQlmKY6c
pNWsImF7CNhjTP7L27LKk49a+IGutyYLnXmrlarcNYeCQBin1meydA==
-----END RSA PRIVATE KEY-----
(( x509publickey(key) ))
Pour une clé ou un certificat donné au format PEM (par exemple généré avec la fonction X509Genkey), cette fonction extrait la clé publique et la renvoie à nouveau au format PEM en tant que chaîne multi-lignes.
Par exemple:
keys :
key : (( x509genkey(2048) ))
public : (( x509publickey(key)
se résout à quelque chose comme
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-----
Pour générer une clé publique SSH, un argument de format supplémentaire facultatif peut être défini sur ssh
. Le résultat sera alors un format clé public régulier utilisable pour SSH. Le format par défaut est pem
fournissant le format de sortie PEM indiqué ci-dessus.
Les clés RSA sont par défaut assemblées au format PKCS # 1 ( RSA PUBLIC KEY
) dans PEM. Si le format générique PKIX ( PUBLIC KEY
) est requis, l'argument du format pkix
doit être donné.
En utilisant le format ssh
cette fonction peut également être utilisée pour convertir une clé publique formatée PEM en une clé SSH,
(( x509cert(spec) ))
La fonction x509cert
crée des certificats signés localement, soit un certificat auto-signé ou un certificat signé par un ca. Il renvoie un certificat codé PEM en tant que valeur de chaîne multi-lignes.
Le paramètre à un seul spécification prend une carte avec certains champs facultatifs et non facultatifs utilisés pour spécifier les informations de certificat. Il peut s'agir d'une expression de carte en ligne ou de toute référence de carte dans le reste du document YAML.
Les champs de carte suivants sont observés:
Nom de champ | Taper | Requis | Signification |
---|---|---|---|
commonName | chaîne | facultatif | Champ de nom commun du sujet |
organization | Liste de chaîne ou de chaîne | facultatif | Champ d'organisation du sujet |
country | Liste de chaîne ou de chaîne | facultatif | Champ de pays du sujet |
isCA | bouffon | facultatif | CA Option de certificat |
usage | Liste de chaîne ou de chaîne | requis | Clés d'utilisation pour le certificat (voir ci-dessous) |
validity | entier | facultatif | Intervalle de validité en heures |
validFrom | chaîne | facultatif | Heure de début dans le format "1 janvier 01:22:31 2019" |
hosts | Liste de chaîne ou de chaîne | facultatif | Liste des noms DNS ou des adresses IP |
privateKey | chaîne | requis ou publickey | clé privée pour générer le certificat pour |
publicKey | chaîne | requis ou privé | clé publique pour générer le certificat pour |
caCert | chaîne | facultatif | certificat pour signer avec |
caPrivateKey | chaîne | facultatif | Priavte Key pour caCert |
Pour les certificats auto-signés, le champ privateKey
doit être défini. publicKey
et les champs ca
devraient être omis. Si le champ caCert
est donné, le champ caKey
est également nécessaire. Si le champ privateKey
est donné avec le caCert
, la clé publique du certificat est extraite de la clé privée.
Les champs supplémentaires sont ignorés silencieusement.
Les clés d'utilisation suivantes sont prises en charge (le cas est ignoré):
Clé | Signification |
---|---|
Signature | x509.KeyusageGitalsignature |
Commitment | x509.KeyUsageContent Commmitment |
KeyEncipherment | x509.KeyUsageKeyEnpicement |
DataEncipherment | x509.KeyUsagedataenchementment |
KeyAgreement | x509.KeyUsageKeyagrement |
CertSign | x509.KeyUsageCertSign |
CRLSign | x509.KeyUsageCrlSign |
EncipherOnly | x509 |
DecipherOnly | x509 |
Any | x509.ExtKeyUsageany |
ServerAuth | x509.ExtKeySageServerAuth |
ClientAuth | x509.ExtKeyUsageClientAuth |
codesigning | x509 |
EmailProtection | x509.ExtKeyUsageEmailProtection |
IPSecEndSystem | x509.ExtKeyUsageIpSenendSystem |
IPSecTunnel | x509.ExtKeyUsageIpsectunnel |
IPSecUser | x509.ExtKeyUsageIpseCuser |
TimeStamping | x509.ExtKeyUsageTimestamping |
OCSPSigning | x509.ExtKeyUsageOcSpsigning |
MicrosoftServerGatedCrypto | x509 |
NetscapeServerGatedCrypto | X509 |
MicrosoftCommercialCodeSigning | X509 |
MicrosoftKernelCodeSigning | X509 |
Par exemple:
spec :
<< : (( &local ))
ca :
organization : Mandelsoft
commonName : Uwe Krueger
privateKey : (( data.cakey ))
isCA : true
usage :
- Signature
- KeyEncipherment
data :
cakey : (( x509genkey(2048) ))
cacert : (( x509cert(spec.ca) ))
génère un certificat racine auto-signé et se résout en quelque chose comme
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) ))
Cette fonction analyse un certificat donné au format PEM et renvoie une carte des champs:
Nom de champ | Taper | Requis | Signification |
---|---|---|---|
commonName | chaîne | facultatif | Champ de nom commun du sujet |
organization | Liste des cordes | facultatif | Champ d'organisation du sujet |
country | Liste des cordes | facultatif | Champ de pays du sujet |
isCA | bouffon | toujours | CA Option de certificat |
usage | Liste des cordes | toujours | Clés d'utilisation pour le certificat (voir ci-dessous) |
validity | entier | toujours | Intervalle de validité en heures |
validFrom | chaîne | toujours | start time in the format "Jan 1 01:22:31 2019" |
validUntil | chaîne | toujours | start time in the format "Jan 1 01:22:31 2019" |
hosts | string list | facultatif | List of DNS names or IP addresses |
dnsNames | string list | facultatif | List of DNS names |
ipAddresses | string list | facultatif | List of IP addresses |
publicKey | chaîne | toujours | 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. Alors que
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) ))
rendements
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. Donc
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
Attention :
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.
par exemple
port : 4711
hosts :
- alice
- bob
mapped : (( map[hosts|x|->x ":" port] ))
rendements
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.
par exemple
list :
- name : alice
age : 25
- name : bob
age : 24
ages : (( map[list|i,p|->i + 1 ". " p.name " is " p.age ] ))
rendements
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.
par exemple
ages :
alice : 25
bob : 24
keys : (( map[ages|k,v|->k] ))
rendements
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
Remarque
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[]
par exemple
list :
- name : alice
age : 25
- name : bob
age : 26
selected : (( select[list|v|->v.age > 25 ] ))
evaluates selected to
selected :
- name : bob
age : 26
Remarque
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 .
Remarque
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.
par exemple
list :
- 1
- 2
sum : (( sum[list|0|s,x|->s + x] ))
rendements
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.
par exemple
list :
- 1
- 2
- 3
prod : (( sum[list|0|s,i,x|->s + i * x ] ))
rendements
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.
par exemple
ages :
alice : 25
bob : 24
sum : (( map[ages|0|s,k,v|->s + v] ))
rendements
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.
Par exemple:
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 | Taper | Signification |
---|---|---|
VERSION | chaîne | current version of spiff |
FILE | chaîne | name of actually processed template file |
DIR | chaîne | name of directory of actually processed template file |
RESOLVED_FILE | chaîne | name of actually processed template file with resolved symbolic links |
RESOLVED_DIR | chaîne | name of directory of actually processed template file with resolved symbolic links |
PATHNAME | chaîne | 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.
Par exemple
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
retours
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
avec
stub.yml
foo :
alice : 24
bob : 26
rendements
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"] ))
avec
stub.yml
foo :
- peter
- paul
rendements
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 ))
avec
stub.yml
people :
- alice : 13
rendements
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
rendements
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
avec
stub.yml
alice : (( config.alice * 2 || ~ ))
bob : (( config.bob * 3 || ~~ ))
rendements
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
avec
mapping.yml
config :
alice : (( ~~ ))
bob : (( ~~ ))
alice : (( config.alice || ~~ ))
bob : (( config.bob || ~~ ))
peter : (( config.peter || ~~ ))
et
config.yml
config :
alice : 4711
peter : 0815
rendements
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 ))
et
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):
Étiqueter | Signification |
---|---|
* | 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 ))
}
It supports