Many modules manage something that is a collection of things: group members, tags, DNS records, repositories, SSH keys, interfaces. The usual interface is a type: list option, and sooner or later every author of such a module has to answer the same question:
With
state: presentandthings: [a, b, c], should the target end up with exactly[a, b, c], or shoulda,b, andcsimply be added to whatever is already there?
There is no single right answer: both behaviors are legitimate and both are needed. What is missing, as far as I can tell, is a shared vocabulary for them. Each module answers the question in its own way, with its own option name, its own polarity, and its own default. Users have to read the docs of every module to find out which of the possible behaviors they get (and sometimes the docs do not say).
This post is not a proposal for a “right way” to handle lists. It is an invitation to compare notes and, if there is interest, to agree on names for the different strategies that already exist, so that modules choosing the same strategy can describe it with the same words.
More than one question
“Add or set” is the most visible part of the problem, but there are a few related questions hiding behind it:
- Items not listed: are they kept, or removed?
- Option omitted vs. empty list: does leaving
thingsout mean “do not touch”, and doesthings: []mean “remove everything”? Or are they the same? - Duplicates and order: is the target a set, an ordered sequence, or a sequence that allows repetitions? Is
[a, b]the same as[b, a]? What happens with[a, a]? - Scope: when “removing what is not listed”, removing it from where? Everything on the target, or only within some subset (an access level, a group of interfaces)?
- Item identity: when the list contains dictionaries (an interface with its settings, a member with its access level), is an existing item updated, replaced wholesale, or left alone?
What community.general does today
A survey of community.general modules found at least these strategies. Module names are examples, not an exhaustive list.
| Strategy | How it is expressed | Examples (default) |
|---|---|---|
| Always exact, no switch | Not expressed: passing [] clears the list |
Most ipa_* modules (ipa_role, ipa_sudorule, ipa_hbacrule, …), consul_token, consul_role, influxdb_user |
Boolean, true = add |
append, array_add, namespace_append |
ipa_group, ipa_hostgroup (false, so exact by default), osx_defaults, pmem |
Boolean, true = exact |
exclusive |
ini_file (true), ipa_dnsrecord (true) |
solo |
cloudflare_dns, dnsimple, netcup_dns (false) |
|
purge |
gitlab_label, gitlab_milestone, gitlab_*_variable, rhsm_repository (false) |
|
purge_tags |
ali_instance (false) |
|
remove_others |
apt_repo (false) |
|
remove_extra_pvs |
lvg (true) |
|
| Choice option | add / replace-all |
git_config (replace-all) |
merge / replace |
manageiq_group (replace) |
|
replace / add |
osx_defaults for dictionaries (replace) |
|
| Exact, limited to a scope | purge_users: [<access levels>] |
gitlab_project_members, gitlab_group_members (no purge when omitted) |
A value of state |
state: present / absent / exact |
ldap_attrs (present) |
| Add/remove only | state: present / absent applied to the listed items, no exact mode |
manageiq_tags, manageiq_policies, package managers |
| Not documented | Plain type: list without saying what happens to existing items |
Several modules that handle tags, labels, profiles, DNS server lists |
A few things stand out:
- The same concept has many names. “Make the target match the list exactly” is spelled
exclusive,solo,purge,purge_tags,remove_others,remove_extra_pvs,replace,replace-all,exact, andappend: false. - The same name means different things.
purgeis “remove unlisted labels” ingitlab_label, but “delete untracked files” inhg.replaceis “replace all values” ingit_config, but “recreate the vault” inipa_vault. - Polarity is not consistent. For
appendandarray_add,truemeans “add”. Forexclusive,solo, andpurge,truemeans “exact”.ipa_groupwithappend: falseandini_filewithexclusive: truedo the same thing. - Defaults are not consistent. Some modules default to the exact behavior (
lvg,ini_file,ipa_dnsrecord,ipa_group), others to the additive one (apt_repo,rhsm_repository,cloudflare_dns). - Duplicates and order are rarely documented. Some modules compare as sets, some deduplicate the input, some deliberately allow repeated values (
git_configwithadd_mode: add,ini_filewithexclusive: false). - Loops do not combine well with the exact strategies.
rhsm_repositoryandansible.posix.authorized_keyboth warn that their exact mode is applied per loop iteration.
Elsewhere in the ecosystem
Other projects have faced the same question, with different answers:
ansible.builtin.userhasappend(defaultfalse) forgroups. The documentation of the development branch states that from ansible-core 2.21,appendis required whenevergroupsis specified. In other words, the user must always state which behavior they want.ansible.posix.authorized_keyhasexclusive(defaultfalse).amazon.awsusespurge_<thing>booleans across the collection, for examplepurge_tags(defaulttrue). The docs make the “omitted vs. empty” distinction explicit: iftagsis not set, tags are not modified, even withpurge_tags: true.- Network resource modules (for example
cisco.ios.ios_vlans) express the strategy throughstate, with a vocabulary shared across many collections:merged(add or update the listed items),replaced(replace the configuration of the listed items, leave the others alone),overridden(the listed items become the entire configuration), anddeleted. This vocabulary also separates two things that are often mixed up: replacing an item versus replacing the whole collection. community.docker.docker_containerhascomparisons, which can be set per option tostrict,ignore, orallow_more_present. This is about comparing rather than changing, but it names the same three ideas: exact, not managed, and additive.
A different shape: each item carries its own state
When the list contains dictionaries, a module could take yet another route. Consider a hypothetical module that manages network interfaces:
- some_interface_module:
state: present # default for items that do not set their own
interfaces:
- name: link1 # created or updated
listen_port: 51821
- name: old_link
state: absent # removed
- Each item has its own
state, and the module-widestateis the default for the items that do not set one. - Interfaces that are not listed are never touched. Removing one means listing it with
state: absent. - A single task can add some items and remove others.
- A separate option, say
managed_interfaces, could declare the full set of interfaces the module considers its own, independently of which ones are written in this run. That would make the exact strategy safer: unlisted items would only be removed within that declared scope, similar to whatpurge_usersdoes in the GitLab member modules.
Lists nested inside each item (say, addresses or dns) would still need their own answer to all the questions above, and it is not necessarily the same answer as for the outer list.
Concepts that seem to need names
Without picking any of them, these are the distinct behaviors that show up above, with the words already in use for each:
| Behavior | Words already in use |
|---|---|
| Listed items are ensured, other items are kept | append, add, merge, merged, allow_more_present, exclusive: false, solo: false, purge: false |
| The target ends up with exactly the listed items | exact, exclusive, solo, purge, purge_*, remove_others, remove_extra_*, replace, replace-all, overridden, strict, append: false |
| Listed items are removed, other items are kept | state: absent, deleted |
| Exact, but only within a declared subset | purge_users: [<levels>] |
| Each listed item declares its own desired state | no common name (the hypothetical <list>[].state above) |
| An existing item is replaced as a whole, not merged | replaced (network resource modules) |
| The list is not managed at all | option omitted, ignore |
Separately from those, it might help to have standard phrasing for how values are compared: as a set, as an ordered sequence, or as a sequence that allows repetitions.
Dictionaries have the same problem
This post focuses on lists, but type: dict options raise very similar questions: are keys that are not specified kept or removed, does an omitted option differ from {}, and are nested values merged or replaced? The same variety shows up there too, for example merge_profile in community.general.lxd_profile, dict_mode in community.general.osx_defaults, purge_tags for the tags dictionaries in amazon.aws, and allow_more_present for dictionaries in community.docker. Dictionaries also add a question of their own: how deep a merge goes. Whatever terminology comes out of this discussion would probably need to work for dictionaries too, but it seems easier to start with lists.
Questions for discussion
- Is a shared terminology for these strategies useful, or is the variety a feature, given how different the managed resources are?
- If it is useful, should it cover only names (so that, say, “exact” means the same thing everywhere it appears), or also how the choice is exposed (boolean vs. choice option vs.
statevalue)? - Is the network resource vocabulary (
merged,replaced,overridden,deleted) a good base to build on outside network modules, or is it too tied tostateand to device configuration? - Should “option omitted means not managed” and “empty list means clear it” be the expected default meanings, as
amazon.awsdocuments fortags? - Should the expected way to handle duplicates and ordering be stated in the docs every time?
- Where should such a convention live: in the Ansible developer guide, or in each collection’s contributor docs?
- Are there other strategies or names in your collections that are not covered here?
Any experience from maintaining modules like these, including what users found confusing, would be very welcome.
AI was used to survey the modules and to draft this post.