The vcs.cf library provides bundles for working with version control tools.

file bodies

control

Prototype: control

Description: Include policy files used by this policy file as part of inputs

Implementation:

body file control
{
      inputs => { @(vcs_common.inputs) };
}

common bodies

vcs_common

Prototype: vcs_common

Description: Enumerate policy files used by this policy file for inclusion to inputs

Implementation:

bundle common vcs_common
{
  vars:
      "inputs" slist => { "$(this.promise_dirname)/common.cf",
                          "$(this.promise_dirname)/paths.cf",
                          "$(this.promise_dirname)/commands.cf" };
}

agent bundles

git_init

Prototype: git_init(repo_path)

Description: initializes a new git repository if it does not already exist

Arguments:

  • repo_path: absolute path of where to initialize a git repository

Example:

bundle agent my_git_repositories
{
  vars:
    "basedir"  string => "/var/git";
    "repos"    slist  => { "myrepo", "myproject", "myPlugForMoreHaskell" };

  files:
    "$(basedir)/$(repos)/."
      create => "true";

  methods:
    "git_init" usebundle => git_init("$(basedir)/$(repos)");
}

Implementation:

bundle agent git_init(repo_path)
{
  classes:
    "ok_norepo" not => fileexists("$(repo_path)/.git");

  methods:
    ok_norepo::
      "git_init"  usebundle => git("$(repo_path)", "init", "");
}

git_add

Prototype: git_add(repo_path, file)

Description: adds files to the supplied repository's index

Arguments:

  • repo_path: absolute path to a git repository
  • file: a file to stage in the index

Example:

bundle agent add_files_to_git_index
{
  vars:
    "repo"  string => "/var/git/myrepo";
    "files" slist  => { "fileA", "fileB", "fileC" };

  methods:
    "git_add" usebundle => git_add("$(repo)", "$(files)");
}

Implementation:

bundle agent git_add(repo_path, file)
{
  classes:
    "ok_repo" expression => fileexists("$(repo_path)/.git");

  methods:
    ok_repo::
      "git_add" usebundle => git("$(repo_path)", "add", "$(file)");
}

git_checkout

Prototype: git_checkout(repo_path, branch)

Description: checks out an existing branch in the supplied git repository

Arguments:

  • repo_path: absolute path to a git repository
  • branch: the name of an existing git branch to checkout

Example:

bundle agent git_checkout_some_existing_branch
{
  vars:
    "repo"   string => "/var/git/myrepo";
    "branch" string => "dev/some-topic-branch";

  methods:
    "git_checkout" usebundle => git_checkout("$(repo)", "$(branch)");
}

Implementation:

bundle agent git_checkout(repo_path, branch)
{
  classes:
    "ok_repo" expression => fileexists("$(repo_path)/.git");

  methods:
    ok_repo::
      "git_checkout" usebundle => git("$(repo_path)", "checkout", "$(branch)");
}

git_checkout_new_branch

Prototype: git_checkout_new_branch(repo_path, new_branch)

Description: checks out and creates a new branch in the supplied git repository

Arguments:

  • repo_path: absolute path to a git repository
  • new_branch: the name of the git branch to create and checkout

Example:

bundle agent git_checkout_new_branches
{
  vars:
    "repo[myrepo]"    string => "/var/git/myrepo";
    "branch[myrepo]"  string => "dev/some-new-topic-branch";

    "repo[myproject]"   string => "/var/git/myproject";
    "branch[myproject]" string => "dev/another-new-topic-branch";

    "repo_names"        slist => getindices("repo");

  methods:
    "git_checkout_new_branch" usebundle => git_checkout_new_branch("$(repo[$(repo_names)])", "$(branch[$(repo_names)])");
}

Implementation:

bundle agent git_checkout_new_branch(repo_path, new_branch)
{
  classes:
    "ok_repo" expression => fileexists("$(repo_path)/.git");

  methods:
    ok_repo::
      "git_checkout" usebundle => git("$(repo_path)", "checkout -b", "$(branch)");
}

git_clean

Prototype: git_clean(repo_path)

Description: Ensure that a given git repo is clean

Arguments:

  • repo_path: Path to the clone

Example:

 methods:
   "test"
     usebundle => git_clean("/opt/cfengine/masterfiles_staging_tmp"),
     comment => "Ensure that the staging area is a clean clone";

Implementation:

bundle agent git_clean(repo_path)
{
  methods:
      "" usebundle => git("$(repo_path)", "clean", ' --force -d'),
      comment => "To have a clean clone we must remove any untracked files and
                  directories. These should have all been stashed, but in case
                  of error we go ahead and clean anyway.";
}

git_stash

Prototype: git_stash(repo_path, stash_name)

Description: Stash any changes (including untracked files if git is capable) in repo_path

Arguments:

  • repo_path: Path to the clone
  • stash_name: Stash name

Example:

 methods:
   "test"
     usebundle => git_stash("/opt/cfengine/masterfiles_staging_tmp", "temp"),
     comment => "Stash any changes, including untracked files";

Implementation:

bundle agent git_stash(repo_path, stash_name)
{
  classes:
    _stdlib_path_exists_git::
      "_git_stash_supports_including_untracked_files" -> { "CFE-3383" }
        expression => regcmp( ".*--include-untracked.*",
                              execresult( "$(paths.git) stash --help", noshell ) );

  vars:
      "_stash_options"
        string => concat( "save ",
                          "--quiet ",
                          ifelse( "_git_stash_supports_including_untracked_files",
                                  "--include-untracked", ""),
                          "$(stash_name)");

  methods:
      "" usebundle => git($(repo_path), "stash", $(_stash_options)),
      comment => "So that we don't lose any trail of what happened and so that
                    we don't accidentally delete something important we stash any
                    changes.
  Note:
                      1. This promise will fail if user.email is not set
                      2. We are respecting ignored files.";

    !_stdlib_path_exists_git::
      "Warning: bundle '$(this.bundle)' actuated, but git not found";
}

git_stash_and_clean

Prototype: git_stash_and_clean(repo_path)

Description: Ensure that a given git repo is clean and attempt to save any modifications

Arguments:

  • repo_path: Path to the clone

Example:

 methods:
   "test"
     usebundle => git_stash_and_clean("/opt/cfengine/masterfiles_staging_tmp"),
     comment => "Ensure that the staging area is a clean clone after attempting to stash any changes";

Implementation:

bundle agent git_stash_and_clean(repo_path)
{
  vars:
      "stash" string => "CFEngine AUTOSTASH: $(sys.date)";

  methods:
      "" usebundle => git_stash($(repo_path), $(stash)),
      classes => scoped_classes_generic("bundle", "git_stash");

    git_stash_ok::
      "" usebundle => git_clean($(repo_path));

  reports:
    git_stash_not_ok::
      "$(this.bundle):: Warning: Not saving changes or cleaning. Git stash failed. Perhaps 'user.email' or 'user.name' is not set.";
}

git_commit

Prototype: git_commit(repo_path, message)

Description: executes a commit to the specificed git repository

Arguments:

  • repo_path: absolute path to a git repository
  • message: the message to associate to the commmit

Example:

bundle agent make_git_commit
{
  vars:
    "repo"  string => "/var/git/myrepo";
    "msg"   string => "dituri added some bundles for common git operations";

  methods:
    "git_commit" usebundle => git_commit("$(repo)", "$(msg)");
}

Implementation:

bundle agent git_commit(repo_path, message)
{
  classes:
    "ok_repo" expression => fileexists("$(repo_path)/.git");

  methods:
    ok_repo::
      "git_commit" usebundle => git("$(repo_path)", "commit", '-m "$(message)"');
}

git

Prototype: git(repo_path, subcmd, args)

Description: generic interface to git

Arguments:

  • repo_path: absolute path to a new or existing git repository
  • subcmd: any valid git sub-command
  • args: a single string of arguments to pass

This bundle will drop privileges if running as root (uid 0) and the repository is owned by a different user. Use DEBUG or DEBUG_git (from the command line, -D DEBUG_git) to see every Git command it runs.

Example:

bundle agent git_rm_files_from_staging
{
  vars:
    "repo"        string => "/var/git/myrepo";
    "git_cmd"     string => "reset --soft";
    "files"       slist  => { "fileA", "fileB", "fileC" };

  methods:
    "git_reset" usebundle => git("$(repo)", "$(git_cmd)", "HEAD -- $(files)");
}

Implementation:

bundle agent git(repo_path, subcmd, args)
{
  vars:
      "oneliner" string => "$(paths.path[git])";

      "repo_uid"
      string  => filestat($(repo_path), "uid"),
      comment => "So that we don't mess up permissions, we will just execute
                    all commands as the current owner of .git";

      "repo_gid"
      string  => filestat($(repo_path), "gid"),
      comment => "So that we don't mess up permissions, we will just execute
                    all commands as the current group of .git";

      # We get the passwd entry from the user that owns the repo so
      # that we can extract the home directory for later use.
      "repo_uid_passwd_ent"
        string => execresult("$(paths.getent) passwd $(repo_uid)", noshell),
        comment => "We need to extract the home directory of the repo
                    owner so that it can be used to avoid errors from
                    unprivledged execution trying to access the root
                    users git config.";

  classes:
      "am_root" expression => strcmp($(this.promiser_uid), "0");

      # $(repo_uid) must be defined before we try to test this or we will end up
      # having at least one pass during evaluation the agent will not know it
      # needs to drop privileges, leading to some files like .git/index being
      # created with elevated privileges, and subsequently causing the agent to
      # not be able to commit as a normal user.
      "need_to_drop"
        not => strcmp($(this.promiser_uid), $(repo_uid)),
        if => isvariable( repo_uid );

    am_root.need_to_drop::
      # This regular expression could be tightened up
      # Extract the home directory from the owner of the repository
      # into $(repo_uid_passwd[1])
      "extracted_repo_uid_home"
        expression => regextract( ".*:.*:\d+:\d+:.*:(.*):.*",
                                  $(repo_uid_passwd_ent),
                                  "repo_uid_passwd" ),
        if => isvariable("repo_uid_passwd_ent");

  commands:
    am_root.need_to_drop::
      # Because cfengine does not inherit the shell environment when
      # executing commands, git will look for the root users git
      # config and error when the executing user does not have
      # access. So we need to set the home directory of the executing
      # user.
      "$(paths.env) HOME=$(repo_uid_passwd[1]) $(oneliner)"
        args => "$(subcmd) $(args)",
        classes => kept_successful_command,
        contain => setuidgid_dir( $(repo_uid), $(repo_gid), $(repo_path) );

    !am_root|!need_to_drop::
      "$(oneliner)"
      args => "$(subcmd) $(args)",
      classes => kept_successful_command,
      contain => in_dir( $(repo_path) );

  reports:
    "DEBUG|DEBUG_$(this.bundle).am_root.need_to_drop"::
      "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)'"
        if => isvariable("repo_uid_passwd[1]");

    "DEBUG|DEBUG_$(this.bundle).(!am_root|!need_to_drop)"::
      "DEBUG $(this.bundle): with current privileges, in directory '$(repo_path)', running Git command '$(oneliner) $(subcmd) $(args)'";
}