diff --git a/changelogs/unreleased/6056-docs-unknowns.yml b/changelogs/unreleased/6056-docs-unknowns.yml new file mode 100644 index 0000000000..06ed9adcd7 --- /dev/null +++ b/changelogs/unreleased/6056-docs-unknowns.yml @@ -0,0 +1,9 @@ +description: Added documentation about unknowns +change-type: patch +sections: + minor-improvement: "{{description}}" +issue-nr: 6056 +destination-branches: + - master + - iso8 + - iso7 diff --git a/docs/glossary.rst b/docs/glossary.rst index 10bc5cc9bf..e4186c110b 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -117,6 +117,10 @@ Glossary a cloud provider will not be known upfront. Inmanta marks this parameters as **unknown**. The state of any resource that uses such an unknown parameter becomes undefined. + For more context, see + :ref:`how unknowns propagate through the configuration model ` and + :ref:`how the exporter deals with them `. + entity Concepts in the infrastructure are modelled in the configuration with entities. An entity defines a new type in the configuration model. See :ref:`lang-entity`. diff --git a/docs/language.rst b/docs/language.rst index 2ae332109e..875d002fbc 100644 --- a/docs/language.rst +++ b/docs/language.rst @@ -923,3 +923,90 @@ Plug-ins For more complex operations, python plugins can be used. Plugins are exposed in the Inmanta language as function calls, such as the template function call. A template accepts parameters and returns a value that it computed out of the variables. Each module that is included can also provide plug-ins. These plug-ins are accessible within the namespace of the module. The :ref:`module-plugins` section of the module guide provides more details about how to write a plugin. + + +.. _language_unknowns: +Unknowns +======== + +Wherever the configuration model interacts with the outside world (e.g. to fetch external values) :term:`unknown` values +may be present. These unknowns represent values we don't know yet, such as the IP address of a machine that hasn't been created yet. Because the compiler can handle unknowns, developers can write models as if all information is present up front, even when this is not the case. These unknowns are propagated through the model to finally end up in the resources that require these unknowns. +values. This section describes how unknown values flow through the model, and perhaps equally importantly, where they do not +flow at all. + +.. note:: + Unknowns are a subtle concept. Luckily, for the majority of model development you don't really need to take them into + account. However, for some advanced scenarios it may be important to know how and where they may occur. + +For the most part, unknowns are simply propagated along the data flow: they're treated like any other value, except +when anything needs to be derived from them, the result simply becomes an unknown as well. More specifically: statements like +assignment statements, constructors +and lists simply include the unknown in their result like they would any other value. Any expression that can not produce +a definite result without knowing the value, will return another unknown. And finally, statements that expand the model +with new blocks based on some value, like the if statement and the for loop, simply do not expand the model with their +respective blocks for unknowns. + +The model below presents some examples of how an unknown propagates. + +.. code-block:: inmanta + + # std::env returns an unknown if the environment variable is not (yet) set + my_unknown = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST") + + a = my_unknown # a is unknown + b = [1, 2, my_unknown, 3] # b is a list with 1 unknown element + c = my_unknown is defined # we can not know if c is null, so c is also unknown + d = true or my_unknown # value of my_unknown is irrelevant -> d is true + e = my_unknown or true # lazy boolean operator can not compute result without knowing the value -> e is unknown + f = (e == my_unknown) # both e and my_unknown are unknown but they aren't necessarily the same value -> f is unknown + + if my_unknown: + # this block is never executed + std::print("This message is never printed!") + else: + # neither is this one + std::print("This message is never printed!") + end + + for x in my_unknown: + # neither is this one + std::print("This message is never printed!") + end + + for x in [1, 2, my_unknown]: + # this block is executed twice: x=1 and x=2 + std::print(f"This message is printed twice! x={x}") + end + + g = my_unknown ? true : false # condition is unknown -> neither branch is executed, result is unknown + + entity E: + int n + end + implement E using std::none + + h = [E(n=x) for x in [1, 2, my_unknown]] # the constructor is executed once with n=1 and once with n=2. Unknown is propagated as is -> h = [E(n=1), E(n=2), unknown] + i = [E(n=x) for x in [1, 2, my_unknown] if not std::is_unknown(x)] # the unknown is filtered out -> i = [E(n=1), E(n=2)] + +Now that we've covered how unknowns flow through the model, we can discuss what an unknown value actually means. In most cases +it simply represents an unknown value. But because of the propagation semantics outlined above, if it happens to occur in a +list, it may in fact represent any number of values: not only the value is unknown, also its size. + +For example, consider a list comprehension that filters a list on some condition. If the list contains an unknown, the compiler +can not know if the filter applies so it will propagate the unknown to the result. When the unknown eventually becomes known, +it might remain in the result, or it might be filtered out, depending on whether it matches the condition. + +.. code-block:: inmanta + + my_unknown = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST") + my_unknown2 = std::get_env("THIS_ENV_VAR_DOES_NOT_EXIST2") + + l = [1, my_unknown, 3, my_unknown2, 5] + a = [x for x in l if x > 2] # l = [unknown, 3, unknown, 5] + + # an unknown can even represent more than one unknown value + b = my_unknown == 0 ? [1, 2] : [3, 4] # b = unknown -> when it becomes known it will be either [1, 2] or [3, 4] + # or none at all + c = [x for x in l if x > 1000] # c = [unknown, unknown] -> would become [] if the env var values are <= 1000 + + d = std::len(l) # d = unknown (l contains unknowns, so its length is also unknown) diff --git a/docs/platform_developers/modelexportformat.rst b/docs/platform_developers/modelexportformat.rst index 0e4925f200..492ef16105 100644 --- a/docs/platform_developers/modelexportformat.rst +++ b/docs/platform_developers/modelexportformat.rst @@ -1,20 +1,19 @@ +.. _model_export_format: Model Export Format ======================== - - #. top level is a dict with one entry for each instance in the model #. the key in this dict is the object reference handle #. the value is the serialized instance #. the serialized instance is a dict with three fields: type, attributes and relation. #. type is the fully qualified name of the type #. attributes is a dict, with as keys the names of the attributes and as values a dict with one entry. -#. An attribute can have one or more of tree keys: unknows, nones and values. The "values" entry has as value a list with the attribute values. - If any of the values is Unknown or None, it is removed from the values array and the index at which it was removed is recorded in respective the unknowns or nones value -#. relations is like attributes, but the list of values contains the reference handles to which this relations points +#. An attribute can have one or more of tree keys: unknows, nones and values. The "values" entry has as value a list with the attribute values. + If any of the values is :term:`unknown` or None, it is removed from the values array and the index at which it was removed is recorded in respective the unknowns or nones value +#. relations is like attributes, but the list of values contains the reference handles to which this relations points -Basic structure as pseudo jinja template +Basic structure as pseudo jinja template .. code-block:: js+jinja @@ -22,23 +21,23 @@ Basic structure as pseudo jinja template {% for instance in instances %} '{{instance.handle}}':{ "type":"{{instance.type.fqn}}", - "attributes":[ + "attributes":[ {% for attribute in instance.attributes %} "{{attribute.name}}": [ {{ attribute.values | join(",") }} ] {% endfor %} ] "relations" : [ {% for relation in instance.relations %} - "{{relation.name}}": [ + "{{relation.name}}": [ {% for value in relation.values %} {{value.handle}} {% endfor %} ] {% endfor %} ] - + {% endif %} - } + } Type Export Format ======================== @@ -46,4 +45,4 @@ Type Export Format .. automodule:: inmanta.model :members: :private-members: - +