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