1# VCS Bundles
2
3bundle common vcs_common
4# @brief Enumerate policy files used by this policy file for inclusion to inputs
5{
6  vars:
7      "inputs" slist => { "$(this.promise_dirname)/common.cf",
8                          "$(this.promise_dirname)/paths.cf",
9                          "$(this.promise_dirname)/commands.cf" };
10}
11
12body file control
13# @brief Include policy files used by this policy file as part of inputs
14{
15      inputs => { @(vcs_common.inputs) };
16}
17
18bundle agent git_init(repo_path)
19# @brief initializes a new git repository if it does not already exist
20# @depends git
21# @param repo_path absolute path of where to initialize a git repository
22#
23# **Example:**
24#
25# ```cf3
26# bundle agent my_git_repositories
27# {
28#   vars:
29#     "basedir"  string => "/var/git";
30#     "repos"    slist  => { "myrepo", "myproject", "myPlugForMoreHaskell" };
31#
32#   files:
33#     "$(basedir)/$(repos)/."
34#       create => "true";
35#
36#   methods:
37#     "git_init" usebundle => git_init("$(basedir)/$(repos)");
38# }
39# ```
40{
41  classes:
42    "ok_norepo" not => fileexists("$(repo_path)/.git");
43
44  methods:
45    ok_norepo::
46      "git_init"  usebundle => git("$(repo_path)", "init", "");
47}
48
49bundle agent git_add(repo_path, file)
50# @brief adds files to the supplied repository's index
51# @depends git
52# @param repo_path absolute path to a git repository
53# @param file a file to stage in the index
54#
55# **Example:**
56#
57# ```cf3
58# bundle agent add_files_to_git_index
59# {
60#   vars:
61#     "repo"  string => "/var/git/myrepo";
62#     "files" slist  => { "fileA", "fileB", "fileC" };
63#
64#   methods:
65#     "git_add" usebundle => git_add("$(repo)", "$(files)");
66# }
67# ```
68{
69  classes:
70    "ok_repo" expression => fileexists("$(repo_path)/.git");
71
72  methods:
73    ok_repo::
74      "git_add" usebundle => git("$(repo_path)", "add", "$(file)");
75}
76
77bundle agent git_checkout(repo_path, branch)
78# @brief checks out an existing branch in the supplied git repository
79# @depends git
80# @param repo_path absolute path to a git repository
81# @param branch the name of an existing git branch to checkout
82#
83# **Example:**
84#
85# ```cf3
86# bundle agent git_checkout_some_existing_branch
87# {
88#   vars:
89#     "repo"   string => "/var/git/myrepo";
90#     "branch" string => "dev/some-topic-branch";
91#
92#   methods:
93#     "git_checkout" usebundle => git_checkout("$(repo)", "$(branch)");
94# }
95# ```
96{
97  classes:
98    "ok_repo" expression => fileexists("$(repo_path)/.git");
99
100  methods:
101    ok_repo::
102      "git_checkout" usebundle => git("$(repo_path)", "checkout", "$(branch)");
103}
104
105bundle agent git_checkout_new_branch(repo_path, new_branch)
106# @brief checks out and creates a new branch in the supplied git repository
107# @depends git
108# @param repo_path absolute path to a git repository
109# @param new_branch the name of the git branch to create and checkout
110#
111# **Example:**
112#
113# ```cf3
114# bundle agent git_checkout_new_branches
115# {
116#   vars:
117#     "repo[myrepo]"    string => "/var/git/myrepo";
118#     "branch[myrepo]"  string => "dev/some-new-topic-branch";
119#
120#     "repo[myproject]"   string => "/var/git/myproject";
121#     "branch[myproject]" string => "dev/another-new-topic-branch";
122#
123#     "repo_names"        slist => getindices("repo");
124#
125#   methods:
126#     "git_checkout_new_branch" usebundle => git_checkout_new_branch("$(repo[$(repo_names)])", "$(branch[$(repo_names)])");
127# }
128# ```
129{
130  classes:
131    "ok_repo" expression => fileexists("$(repo_path)/.git");
132
133  methods:
134    ok_repo::
135      "git_checkout" usebundle => git("$(repo_path)", "checkout -b", "$(branch)");
136}
137
138bundle agent git_clean(repo_path)
139# @brief Ensure that a given git repo is clean
140# @param repo_path Path to the clone
141#
142# **Example:**
143#
144# ```cf3
145#  methods:
146#    "test"
147#      usebundle => git_clean("/opt/cfengine/masterfiles_staging_tmp"),
148#      comment => "Ensure that the staging area is a clean clone";
149# ```
150{
151  methods:
152      "" usebundle => git("$(repo_path)", "clean", ' --force -d'),
153      comment => "To have a clean clone we must remove any untracked files and
154                  directories. These should have all been stashed, but in case
155                  of error we go ahead and clean anyway.";
156}
157
158bundle agent git_stash(repo_path, stash_name)
159# @brief Stash any changes (including untracked files if git is capable) in repo_path
160# @param repo_path Path to the clone
161# @param stash_name Stash name
162#
163# **Example:**
164#
165# ```cf3
166#  methods:
167#    "test"
168#      usebundle => git_stash("/opt/cfengine/masterfiles_staging_tmp", "temp"),
169#      comment => "Stash any changes, including untracked files";
170# ```
171{
172  classes:
173    _stdlib_path_exists_git::
174      "_git_stash_supports_including_untracked_files" -> { "CFE-3383" }
175        expression => regcmp( ".*--include-untracked.*",
176                              execresult( "$(paths.git) stash --help", noshell ) );
177
178  vars:
179      "_stash_options"
180        string => concat( "save ",
181                          "--quiet ",
182                          ifelse( "_git_stash_supports_including_untracked_files",
183                                  "--include-untracked", ""),
184                          "$(stash_name)");
185
186  methods:
187      "" usebundle => git($(repo_path), "stash", $(_stash_options)),
188      comment => "So that we don't lose any trail of what happened and so that
189                    we don't accidentally delete something important we stash any
190                    changes.
191  Note:
192                      1. This promise will fail if user.email is not set
193                      2. We are respecting ignored files.";
194
195    !_stdlib_path_exists_git::
196      "Warning: bundle '$(this.bundle)' actuated, but git not found";
197}
198
199bundle agent git_stash_and_clean(repo_path)
200# @brief Ensure that a given git repo is clean and attempt to save any modifications
201# @param repo_path Path to the clone
202#
203# **Example:**
204#
205# ```cf3
206#  methods:
207#    "test"
208#      usebundle => git_stash_and_clean("/opt/cfengine/masterfiles_staging_tmp"),
209#      comment => "Ensure that the staging area is a clean clone after attempting to stash any changes";
210# ```
211{
212  vars:
213      "stash" string => "CFEngine AUTOSTASH: $(sys.date)";
214
215  methods:
216      "" usebundle => git_stash($(repo_path), $(stash)),
217      classes => scoped_classes_generic("bundle", "git_stash");
218
219    git_stash_ok::
220      "" usebundle => git_clean($(repo_path));
221
222  reports:
223    git_stash_not_ok::
224      "$(this.bundle):: Warning: Not saving changes or cleaning. Git stash failed. Perhaps 'user.email' or 'user.name' is not set.";
225}
226
227bundle agent git_commit(repo_path, message)
228# @brief executes a commit to the specificed git repository
229# @depends git
230# @param repo_path absolute path to a git repository
231# @param message the message to associate to the commmit
232#
233# **Example:**
234#
235# ```cf3
236# bundle agent make_git_commit
237# {
238#   vars:
239#     "repo"  string => "/var/git/myrepo";
240#     "msg"   string => "dituri added some bundles for common git operations";
241#
242#   methods:
243#     "git_commit" usebundle => git_commit("$(repo)", "$(msg)");
244# }
245# ```
246{
247  classes:
248    "ok_repo" expression => fileexists("$(repo_path)/.git");
249
250  methods:
251    ok_repo::
252      "git_commit" usebundle => git("$(repo_path)", "commit", '-m "$(message)"');
253}
254
255bundle agent git(repo_path, subcmd, args)
256# @brief generic interface to git
257# @param repo_path absolute path to a new or existing git repository
258# @param subcmd any valid git sub-command
259# @param args a single string of arguments to pass
260#
261# This bundle will drop privileges if running as root (uid 0) and the
262# repository is owned by a different user. Use `DEBUG` or `DEBUG_git` (from the
263# command line, `-D DEBUG_git`) to see every Git command it runs.
264#
265# **Example:**
266#
267# ```cf3
268# bundle agent git_rm_files_from_staging
269# {
270#   vars:
271#     "repo"        string => "/var/git/myrepo";
272#     "git_cmd"     string => "reset --soft";
273#     "files"       slist  => { "fileA", "fileB", "fileC" };
274#
275#   methods:
276#     "git_reset" usebundle => git("$(repo)", "$(git_cmd)", "HEAD -- $(files)");
277# }
278# ```
279{
280  vars:
281      "oneliner" string => "$(paths.path[git])";
282
283      "repo_uid"
284      string  => filestat($(repo_path), "uid"),
285      comment => "So that we don't mess up permissions, we will just execute
286                    all commands as the current owner of .git";
287
288      "repo_gid"
289      string  => filestat($(repo_path), "gid"),
290      comment => "So that we don't mess up permissions, we will just execute
291                    all commands as the current group of .git";
292
293      # We get the passwd entry from the user that owns the repo so
294      # that we can extract the home directory for later use.
295      "repo_uid_passwd_ent"
296        string => execresult("$(paths.getent) passwd $(repo_uid)", noshell),
297        comment => "We need to extract the home directory of the repo
298                    owner so that it can be used to avoid errors from
299                    unprivledged execution trying to access the root
300                    users git config.";
301
302  classes:
303      "am_root" expression => strcmp($(this.promiser_uid), "0");
304
305      # $(repo_uid) must be defined before we try to test this or we will end up
306      # having at least one pass during evaluation the agent will not know it
307      # needs to drop privileges, leading to some files like .git/index being
308      # created with elevated privileges, and subsequently causing the agent to
309      # not be able to commit as a normal user.
310      "need_to_drop"
311        not => strcmp($(this.promiser_uid), $(repo_uid)),
312        if => isvariable( repo_uid );
313
314    am_root.need_to_drop::
315      # This regular expression could be tightened up
316      # Extract the home directory from the owner of the repository
317      # into $(repo_uid_passwd[1])
318      "extracted_repo_uid_home"
319        expression => regextract( ".*:.*:\d+:\d+:.*:(.*):.*",
320                                  $(repo_uid_passwd_ent),
321                                  "repo_uid_passwd" ),
322        if => isvariable("repo_uid_passwd_ent");
323
324  commands:
325    am_root.need_to_drop::
326      # Because cfengine does not inherit the shell environment when
327      # executing commands, git will look for the root users git
328      # config and error when the executing user does not have
329      # access. So we need to set the home directory of the executing
330      # user.
331      "$(paths.env) HOME=$(repo_uid_passwd[1]) $(oneliner)"
332        args => "$(subcmd) $(args)",
333        classes => kept_successful_command,
334        contain => setuidgid_dir( $(repo_uid), $(repo_gid), $(repo_path) );
335
336    !am_root|!need_to_drop::
337      "$(oneliner)"
338      args => "$(subcmd) $(args)",
339      classes => kept_successful_command,
340      contain => in_dir( $(repo_path) );
341
342  reports:
343    "DEBUG|DEBUG_$(this.bundle).am_root.need_to_drop"::
344      "DEBUG $(this.bundle): with dropped privileges to uid '$(repo_uid)' and gid '$(repo_gid)', in directory '$(repo_path)', running Git command '$(paths.env) HOME=\"$(repo_uid_passwd[1])\" $(oneliner) $(subcmd) $(args)'"
345        if => isvariable("repo_uid_passwd[1]");
346
347    "DEBUG|DEBUG_$(this.bundle).(!am_root|!need_to_drop)"::
348      "DEBUG $(this.bundle): with current privileges, in directory '$(repo_path)', running Git command '$(oneliner) $(subcmd) $(args)'";
349}
350