The original example from Bas:
bundle agent main
{
methods:
"" usebundle => create_link();
"" usebundle => create_template();
}
bundle agent create_link
{
files:
"/tmp/link_file"
move_obstructions => "true",
link_from => ln_s( "/etc/resolv.conf" );
#move_obstructions => "true",
}
bundle agent create_template
{
vars:
"mustache" string => "this is a test";
files:
"/tmp/link_file"
move_obstructions => "true",
edit_template_string => "$(mustache)",
template_method => "inline_mustache";
}
body link_from ln_s(x)
# @brief Create a symbolink link to `x`
# The link is created even if the source of the link does not exist.
# @param x The source of the link
{
link_type => "symlink";
source => "$(x)";
when_no_source => "force";
}
info: Linked files '/tmp/link_file' -> '/etc/resolv.conf' error: Cannot follow symlink '/tmp/link_file'; it is not owned by root or the user running this process, and the target owner and/or group differs from that of the symlink itself. error: Unable to open destination file '/etc/../run/systemd/resolve/stub-resolv.conf.cf-after-edit' for writing. (fopen: Permission denied) error: Failed to update rendering of '/tmp/link_file' from mustache template 'inline' error: Errors encountered when actuating files promise '/tmp/link_file' error: Method 'create_template' failed in some repairs
Checking against the behavior of the content attribute:
bundle agent main
{
methods:
"" usebundle => create_link();
"" usebundle => create_template();
"" usebundle => content();
}
bundle agent create_link
{
files:
"/tmp/link_target"
content => "Link Target Content";
"/tmp/link_file"
move_obstructions => "true",
link_from => ln_s( "/tmp/link_target" );
#move_obstructions => "true",
}
bundle agent create_template
{
vars:
"mustache" string => "this is a test";
files:
"/tmp/link_file"
move_obstructions => "true",
edit_template_string => "$(mustache)",
template_method => "inline_mustache";
}
bundle agent content
{
files:
"/tmp/link_file"
move_obstructions => "true",
content => "Rendered via content attribute targeting symlink";
}
body link_from ln_s(x)
# @brief Create a symbolink link to `x`
# The link is created even if the source of the link does not exist.
# @param x The source of the link
{
link_type => "symlink";
source => "$(x)";
when_no_source => "force";
}
info: Created file '/tmp/link_target', mode 0600
info: Updated file '/tmp/link_target' with content 'Link Target Content'
info: Linked files '/tmp/link_file' -> '/tmp/link_target'
info: Updated rendering of '/tmp/link_file' from mustache template 'inline'
info: Updated file '/tmp/link_file' with content 'Rendered via content attribute targeting symlink'
Checking against the behavior of the copy_from attribute:
f
bundle agent main
{
methods:
"" usebundle => create_link();
"" usebundle => create_template();
"" usebundle => content();
"" usebundle => copy();
}
bundle agent create_link
{
files:
"/tmp/link_target"
content => "Link Target Content";
"/tmp/link_file"
move_obstructions => "true",
link_from => ln_s( "/tmp/link_target" );
#move_obstructions => "true",
}
bundle agent create_template
{
vars:
"mustache" string => "this is a test";
files:
"/tmp/link_file"
move_obstructions => "true",
edit_template_string => "$(mustache)",
template_method => "inline_mustache";
}
bundle agent content
{
files:
"/tmp/link_file"
move_obstructions => "true",
content => "Rendered via content attribute targeting symlink";
}
bundle agent copy
{
files:
"/tmp/link_file"
move_obstructions => "true",
copy_from => local_dcp( $(this.promise_filename) );
}
body copy_from local_dcp(from)
# @brief Copy a local file if the hash on the source file differs.
# @param from The path to the source file.
#
# **Example:**
#
# ```cf3
# bundle agent example
# {
# files:
# "/tmp/file.bak"
# copy_from => local_dcp("/tmp/file");
# }
# ```
#
# **See Also:** `local_cp()`, `remote_dcp()`
{
source => "$(from)";
compare => "digest";
}
body link_from ln_s(x)
# @brief Create a symbolink link to `x`
# The link is created even if the source of the link does not exist.
# @param x The source of the link
{
link_type => "symlink";
source => "$(x)";
when_no_source => "force";
}
info: Updated file '/tmp/link_target' with content 'Link Target Content'
info: Updated rendering of '/tmp/link_file' from mustache template 'inline'
info: Updated file '/tmp/link_file' with content 'Rendered via content attribute targeting symlink'
info: Removing old symbolic link '/tmp/link_file' to make way for copy
info: Copied file '/home/nickanderson/org/roam/daily/work/cfengine3-k1evev' to '/tmp/link_file.cfnew' (mode '600')
info: Moved '/tmp/link_file.cfnew' to '/tmp/link_file'
info: Updated file '/tmp/link_file' from 'localhost:/home/nickanderson/org/roam/daily/work/cfengine3-k1evev'
The content attribute appears to function in the same way as when template_method inline_mustache is used. That is to say that it operates on the final destination and does not convert the file into a plain file. However, copy_from does replace the symlink with a plain file.
Re-reading the documentation on move_obstructions (https://docs.cfengine.com/docs/3.24/reference-promise-types-files.html#move_obstructions), it does seem to me like both template_method inline_mustache ( probably all cases of template_method are affected ) and the content attribute have bugs in this regard and do not respect move_obstructions as they should.
As far as working around it, for now I think you will have to use additional promises and check to see if the file islink() or not before actuating the promise.