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