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