diff --git a/.github/workflows/pr_glci.yml b/.github/workflows/pr_glci.yml new file mode 100644 index 0000000..db72790 --- /dev/null +++ b/.github/workflows/pr_glci.yml @@ -0,0 +1,190 @@ +# Push/Trigger a GitLab CI pipeline for the PR HEAD, **ONLY IF:** +# +# 1. The .gitlab-ci.yaml file exists and validates +# 2. The PR submitter has write access to the target repository. +# +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# GitHub Action Secrets variables available for this pipeline: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# GITLAB_API_PRIVATE_TOKEN Secure Should have `api` scope +# GITLAB_API_URL Optional +# +# The secure vars will be filtered in GitHub Actions log output, and aren't +# provided to untrusted builds (i.e, triggered by PR from another repository) +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!V!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# DO NOT MODIFY this workflow, unless you **REALLY** know what you are doing. +# +# This workflow bypasses some of the built-in protections of the +# `pull_request_target` event by explicitly checking out the PR's **HEAD**. +# Without being VERY CAREFUL, this could easily allow a malcious PR +# contributor the chance to access secrets or a GITHUB_TOKEN with write scope!! +# +# The jobs in this workflow are designed to handle this safely -- but DO NOT +# assume any alterations will also be safe. +# +# For general information, see: +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target +# +# For further information, or if ANY of this seems confusing or unecessary: +# +# ASK FOR ASSISTANCE **BEFORE** ATTEMPTING TO MODIFY THIS WORKFLOW. +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!V!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows +# +--- +name: PR GLCI +on: + pull_request_target: + types: [opened, reopened, synchronize] + +jobs: + + # The ONLY reason we can validate the PR HEAD's content safely here is that + # we restrict ourselves to sending data elsewhere. + glci-syntax: + name: '.gitlab-ci.yml Syntax' + runs-on: ubuntu-16.04 + outputs: + valid: ${{ steps.validate-glci-file.outputs.valid }} + steps: + - uses: actions/checkout@v2 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + - name: 'Validate GLCI file syntax' + id: validate-glci-file + uses: simp/github-action-gitlab-ci-syntax-check@main + with: + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_api_url: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 + + contributor-permissions: + name: 'PR contributor check' + runs-on: ubuntu-18.04 + outputs: + permitted: ${{ steps.user-repo-permissions.outputs.permitted }} + steps: + - uses: actions/github-script@v3 + id: user-repo-permissions + with: + github-token: ${{secrets.GITHUB_TOKEN}} + # See: + # - https://octokit.github.io/rest.js/ + # - https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-repository-permissions-for-a-user + script: | + const project_permission = await github.request('GET /repos/{owner}/{repo}/collaborators/{username}/permission', { + headers: { + accept: 'application/vnd.github.v3+json' + }, + owner: context.repo.owner, + repo: context.repo.repo, + username: context.payload.sender.login, + }) + const has_write_access = perm_lvl => (perm_lvl == "admin" || perm_lvl == "write" ) + const write_access_desc = perm_bool => (perm_bool ? "PERMISSION OK" : "PERMISSION DENIED" ) + if( has_write_access(project_permission.data.permission )){ + core.setOutput( 'permitted', 'true' ) + } else { + core.setOutput( 'permitted', 'false' ) + console.log(`::error ::payload user '${context.payload.sender.login}' does not have CI trigger permission for '${context.repository}; not triggering external CI'`) + } + console.log(`== payload user '${context.payload.sender.login}' CI trigger permission for '${context.repo.owner}': ${write_access_desc(has_write_access(project_permission.data.permission))}`) + + + trigger-when-user-has-repo-permissions: + name: 'Trigger CI [trusted users only]' + needs: [ glci-syntax, contributor-permissions ] + # This conditional provides an extra safety control, in case the workflow's + # `on` section is inadventently modified without considering the security + # implications. + # + # This job will ONLY trigger on: + # + # - [x] pull_request_target event: github.event_name == 'pull_request_target' + # AND: + # - [x] Newly-opened PRs: github.event.action == 'opened' + # - [x] Re-opened PRs: github.event.action == 'reopened' + # - [x] Commits are added to PR: github.event.action == 'synchronize' + # AND: + # - [x] .gitlab-ci.yml exists/ok: needs.glci-syntax.outputs.valid == 'true' + # + # [Not implemented] It should NEVER trigger on: + # + # - [ ] Merged PRs: github.event.pull_request.merged == 'false' + # - (the downstream GitLab mirror will take care of that) + # - Not implemented: For some reason, this conditional always fails + # - Unnecessary if on>pull_request_target>types doesn't include 'closed' + if: github.event_name == 'pull_request_target' && ( github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' ) && github.event.pull_request.merged != 'true' && needs.glci-syntax.outputs.valid == 'true' && needs.contributor-permissions.outputs.permitted == 'true' + runs-on: ubuntu-18.04 + steps: + # Things we'd like to do: + # - [ ] if there's no GitLab mirror, make one + # - [ ] if there's no GitLab <-> GitHub integration, make one + # - [ ] if there's no PR check on the main GitHub branch, make one (?) + # - [x] Cancel any GLCI pipelines already pending/running for this branch + # - "created|waiting_for_resource|preparing|pending|running" + # - Exception: don't cancel existing pipeline for our own commit + # - [x] if PR: force-push branch to GitLab + - uses: actions/checkout@v2 + if: needs.contributor-permissions.outputs.permitted == 'true' + with: + clean: true + fetch-depth: 0 # Need full checkout to push to gitlab mirror + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Trigger CI when user has Repo Permissions + if: needs.contributor-permissions.outputs.permitted == 'true' + uses: simp/github-action-gitlab-ci-pipeline-trigger@v1 + with: + git_branch: ${{ github.event.pull_request.head.ref }} # TODO check for/avoid protected branches? + git_hashref: ${{ github.event.pull_request.head.sha }} + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_group: ${{ github.event.organization.login }} + github_repository: ${{ github.repository }} + github_repository_owner: ${{ github.repository_owner }} + + - name: When user does NOT have Repo Permissions + if: needs.contributor-permissions.outputs.permitted == 'false' + continue-on-error: true + run: | + echo "Ending gracefully; Contributor $GITHUB_ACTOR does not have permission to trigger CI" + false + +### examine_contexts: +### name: 'Examine Context contents' +### if: always() +### runs-on: ubuntu-16.04 +### needs: [ glci-syntax, contributor-permissions ] +### steps: +### - name: Dump contexts +### env: +### GITHUB_CONTEXT: ${{ toJson(github) }} +### run: echo "$GITHUB_CONTEXT" +### - name: Dump needs context +### env: +### ENV_CONTEXT: ${{ toJson(needs) }} +### run: echo "$ENV_CONTEXT" +### - name: Dump env vars +### run: env | sort + diff --git a/.github/workflows/pr_glci_cleanup.yml b/.github/workflows/pr_glci_cleanup.yml new file mode 100644 index 0000000..70d1658 --- /dev/null +++ b/.github/workflows/pr_glci_cleanup.yml @@ -0,0 +1,105 @@ +# When a PR is closed, clean up any associated GitLab CI pipelines & branch +# +# * Cancels all GLCI pipelines associated with the PR HEAD ref (branch) +# * Removes the PR HEAD branch from the corresponding gitlab.com/org/ project +# +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# GitHub Action Secrets variables available for this pipeline: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# GITLAB_API_PRIVATE_TOKEN Secure Should have `api` scope +# GITLAB_API_URL Optional +# +# The secure vars will be filtered in GitHub Actions log output, and aren't +# provided to untrusted builds (i.e, triggered by PR from another repository) +# +# ------------------------------------------------------------------------------ +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows +# +--- +name: PR GLCI Cleanup +on: + pull_request_target: + types: [closed] + +jobs: + cleanup-glci-branch: + name: 'Clean up GLCI' + # This conditional provides an extra safety control, in case the workflow's + # `on` section is inadventently modified without considering the security + # implications. + if: github.event_name == 'pull_request_target' && github.event.action == 'closed' + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + - name: Trigger CI when user has Repo Permissions + env: + GITLAB_SERVER_URL: ${{ secrets.GITLAB_SERVER_URL }} # https://gitlab.com + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 + GITLAB_ORG: ${{ github.event.organization.login }} + GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + run: | + GITLAB_SERVER_URL="${GITLAB_SERVER_URL:-https://gitlab.com}" + GITLAB_API_URL="${GITLAB_API_URL:-${GITLAB_SERVER_URL}/api/v4}" + GIT_BRANCH="${GIT_BRANCH:-GITHUB_HEAD_REF}" + GITXXB_REPO_NAME="${GITHUB_REPOSITORY/$GITHUB_REPOSITORY_OWNER\//}" + GITLAB_PROJECT_ID="${GITLAB_ORG}%2F${GITXXB_REPO_NAME}" + # --http1.0 avoids an HTTP/2 load balancing issue when run from GA + CURL_CMD=(curl --http1.0 --fail --silent --show-error \ + --header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN" \ + --header "Content-Type: application/json" \ + --header "Accept: application/json" \ + ) + + # Cancel any active/pending GitLab CI pipelines for the same project+branch + active_pipeline_ids=() + for pipe_status in created waiting_for_resource preparing pending running; do + echo " ---- checking for CI pipelines with status '$pipe_status' for project '$GITLAB_PROJECT_ID', branch '$GIT_BRANCH'" + url="${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines?ref=${GIT_BRANCH}&status=${pipe_status}" + active_pipelines="$("${CURL_CMD[@]}" "$url" | jq -r '.[] | .id , .web_url')" + active_pipeline_ids+=($(echo "$active_pipelines" | grep -E '^[0-9]*$')) + printf "$active_pipelines\n\n" + done + if [ "${#active_pipeline_ids[@]}" -gt 0 ]; then + printf "\nFound %s active pipeline ids:\n" "${#active_pipeline_ids[@]}" + echo "${active_pipeline_ids[@]}" + for pipe_id in "${active_pipeline_ids[@]}"; do + printf "\n ------ Cancelling pipeline ID %s...\n" "$pipe_id" + "${CURL_CMD[@]}" --request POST "${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines/${pipe_id}/cancel" + done + else + echo No active pipelines found + fi + + echo "== Removing $GIT_BRANCH from gitlab" + git remote add gitlab "https://oauth2:${GITLAB_API_PRIVATE_TOKEN}@${GITLAB_SERVER_URL#*://}/${GITLAB_ORG}/${GITXXB_REPO_NAME}.git" + git push gitlab ":${GIT_BRANCH}" -f || : # attempt to un-weird GLCI's `changed` tracking + +### examine_contexts: +### name: 'Examine Context contents' +### if: always() +### runs-on: ubuntu-16.04 +### steps: +### - name: Dump contexts +### env: +### GITHUB_CONTEXT: ${{ toJson(github) }} +### run: echo "$GITHUB_CONTEXT" +### run: echo "$ENV_CONTEXT" +### - name: Dump env vars +### run: env | sort + diff --git a/.github/workflows/pr_glci_manual.yml b/.github/workflows/pr_glci_manual.yml new file mode 100644 index 0000000..cc04ffc --- /dev/null +++ b/.github/workflows/pr_glci_manual.yml @@ -0,0 +1,143 @@ +# Manually trigger GLCI pipelines for a PR +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# GITLAB_API_PRIVATE_TOKEN Required GitLab token (should have `api` scope) +# NO_SCOPE_GITHUB_TOKEN Required GitHub token (should have no scopes) +# GITLAB_SERVER_URL Optional Specify a GL server other than gitlab.com +# The secure vars will be filtered in GitHub Actions log output, and aren't +# provided to untrusted builds (i.e, triggered by PR from another repository) +# +# ------------------------------------------------------------------------------ +# +# NOTES: +# It is necessary to provide NO_SCOPE_GITHUB_TOKEN because $secrets.GITHUB_AUTO +# is NOT provide to manually-triggered (`workflow_dispatch`) events, in order +# to prevent recursive triggers between workflows +# +# Reference: +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token +--- +name: 'Manual: PR GLCI' + +on: + workflow_dispatch: + inputs: + pr_number: + description: "PR number to trigger GLCI" + required: true + +jobs: + glci-syntax: + name: '.gitlab-ci.yml Syntax' + runs-on: ubuntu-18.04 + outputs: + valid: ${{ steps.validate-glci-file.outputs.valid }} + pr_head_ref: ${{ steps.get-pr.outputs.pr_head_ref }} + pr_head_sha: ${{ steps.get-pr.outputs.pr_head_sha }} + pr_head_label: ${{ steps.get-pr.outputs.pr_head_label }} + pr_head_full_name: ${{ steps.get-pr.outputs.pr_full_name }} + steps: + - uses: actions/github-script@v3 + id: get-pr + with: + github-token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} + # See: + # - https://octokit.github.io/rest.js/ + script: | + console.log(`== pr number: ${context.payload.inputs.pr_number}`) + const pr = await github.request('get /repos/{owner}/{repo}/pulls/{pull_number}', { + headers: { + accept: 'application/vnd.github.v3+json' + }, + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.inputs.pr_number + }); + + console.log("\n\n== pr\n"); + console.log(pr); + console.log("\n\n== pr.data.head\n"); + console.log(pr.data.head); + console.log(pr.status); + + // PR must have been returned + if ( pr.status != 200 ) { + //#console.log(`::error ::Error looking up PR \#${context.payload.inputs.pr_number}: HTTP Response ${pr.status}`) + return(false) + } + + // TODO: should either of these conditions really prevent a GLCI trigger? + if ( pr.data.state != 'open' ) { + console.log(`::error ::PR# ${context.payload.inputs.pr_number} is not open`) + } + if ( pr.data.merged ) { + console.log(`::error ::PR# ${context.payload.inputs.pr_number} is already merged`) + } + core.setOutput( 'pr_head_sha', pr.data.head.sha ) + core.setOutput( 'pr_head_ref', pr.data.head.ref ) + core.setOutput( 'pr_head_label', pr.data.head.label ) + core.setOutput( 'pr_head_full_name', pr.data.head.full_name ) + - uses: actions/checkout@v2 + with: + repository: ${{ steps.get-pr.outputs.pr_head_full_name }} + ref: ${{ steps.get-pr.outputs.pr_head_sha }} + token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} + clean: true + - name: 'Validate GLCI file syntax' + id: validate-glci-file + uses: simp/github-action-gitlab-ci-syntax-check@main + with: + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_api_url: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 + + trigger-when-user-has-repo-permissions: + name: 'Trigger CI' + needs: [ glci-syntax ] + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + repository: ${{ needs.glci-syntax.outputs.pr_head_full_name }} + ref: ${{ needs.glci-syntax.outputs.pr_head_sha }} + token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} + fetch-depth: 0 # Need full checkout to push to gitlab mirror + clean: true + - name: Trigger CI when user has Repo Permissions + uses: simp/github-action-gitlab-ci-pipeline-trigger@v1 + with: + git_hashref: ${{ needs.glci-syntax.outputs.pr_head_sha }} + git_branch: ${{ needs.glci-syntax.outputs.pr_head_ref }} + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_group: ${{ github.event.organization.login }} + github_repository: ${{ github.repository }} + github_repository_owner: ${{ github.repository_owner }} + +### examine_contexts: +### needs: [ glci-syntax ] +### name: 'Examine Context contents' +### if: always() +### runs-on: ubuntu-18.04 +### steps: +### - name: Dump contexts +### env: +### GITHUB_CONTEXT: ${{ toJson(github) }} +### run: echo "$GITHUB_CONTEXT" +### - name: Dump 'needs' context +### env: +### ENV_CONTEXT: ${{ toJson(needs) }} +### run: echo "$ENV_CONTEXT" +### - name: Dump env vars +### run: env | sort diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml new file mode 100644 index 0000000..3ea01ee --- /dev/null +++ b/.github/workflows/pr_tests.yml @@ -0,0 +1,143 @@ +# Run Puppet checks and test matrix on Pull Requests +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# The testing matrix considers ruby/puppet versions supported by SIMP and PE: +# ------------------------------------------------------------------------------ +# Release Puppet Ruby EOL +# SIMP 6.4 5.5 2.40 TBD +# PE 2018.1 5.5 2.40 2021-01 (LTS overlap) +# PE 2019.8 6.18 2.5 2022-12 (LTS) +# +# https://puppet.com/docs/pe/2018.1/component_versions_in_recent_pe_releases.html +# https://puppet.com/misc/puppet-enterprise-lifecycle +# https://puppet.com/docs/pe/2018.1/overview/getting_support_for_pe.html +# ============================================================================== +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows +# + +name: PR Tests +on: + pull_request: + types: [opened, reopened, synchronize] + +env: + PUPPET_VERSION: '~> 6' + +jobs: + puppet-syntax: + name: 'Puppet Syntax' + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 # ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + with: + ruby-version: 2.5 + bundler-cache: true + - run: "bundle exec rake syntax" + + puppet-style: + name: 'Puppet Style' + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - run: "bundle exec rake lint" + - run: "bundle exec rake metadata_lint" + + ruby-style: + if: false # TODO Modules will need: rubocop in Gemfile, .rubocop.yml + name: 'Ruby Style (experimental)' + runs-on: ubuntu-18.04 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - run: | + bundle show + bundle exec rake rubocop + + file-checks: + name: 'File checks' + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: 'Install Ruby 2.5' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - run: bundle exec rake check:dot_underscore + - run: bundle exec rake check:test_file + + releng-checks: + name: 'RELENG checks' + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - name: 'Tags and changelogs' + run: | + bundle exec rake pkg:check_version + bundle exec rake pkg:compare_latest_tag + bundle exec rake pkg:create_tag_changelog + - name: 'Test-build the Puppet module' + run: 'bundle exec pdk build --force' + + spec-tests: + name: 'Puppet Spec' + needs: [puppet-syntax] + runs-on: ubuntu-18.04 + strategy: + matrix: + puppet: + - label: 'Puppet 6.18 [SIMP 6.5/PE 2019.8]' + puppet_version: '~> 6.18.0' + ruby_version: '2.5' + - label: 'Puppet 5.5 [SIMP 6.4/PE 2018.1]' + puppet_version: '~> 5.5.22' + ruby_version: '2.4' + - label: 'Puppet 7.x' + puppet_version: '~> 7.0' + ruby_version: '2.7' + env: + PUPPET_VERSION: '${{matrix.puppet.puppet_version}}' + steps: + - uses: actions/checkout@v2 + - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.puppet.ruby_version}} + bundler-cache: true + - run: 'command -v rpm || if command -v apt-get; then apt-get update; apt-get install -y rpm; fi ||:' + - run: 'bundle exec rake spec' + +# dump_contexts: +# name: 'Examine Context contents' +# runs-on: ubuntu-16.04 +# steps: +# - name: Dump contexts +# env: +# GITHUB_CONTEXT: ${{ toJson(github) }} +# run: echo "$GITHUB_CONTEXT" +# diff --git a/.github/workflows/tag_deploy.yml b/.github/workflows/tag_deploy.yml new file mode 100644 index 0000000..dfcef6b --- /dev/null +++ b/.github/workflows/tag_deploy.yml @@ -0,0 +1,126 @@ +# Build & Deploy Puppet module & GitHub release when a SemVer tag is pushed +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# PUPPETFORGE_API_TOKEN Required +# +# ------------------------------------------------------------------------------ +# +# NOTES: +# +# * The CHANGLOG text is altered to remove RPM-style date headers, which don't +# render well as markdown on the GitHub release pages +--- +name: 'Tag: Release to GitHub & Puppet Forge' + +on: + push: + tags: + - '[0-9]+\.[0-9]+\.[0-9]+' + +env: + PUPPET_VERSION: '~> 6' + +jobs: + releng-checks: + name: "RELENG checks" + runs-on: ubuntu-18.04 + steps: + - name: "Assert '${{ github.ref }}' is a tag" + run: '[[ "$GITHUB_REF" =~ ^refs/tags/ ]] || { echo "::error ::GITHUB_REF is not a tag: ${GITHUB_REF}"; exit 1 ; }' + - uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + clean: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - run: bundle exec rake pkg:check_version + - run: bundle exec rake pkg:compare_latest_tag + - run: bundle exec rake pkg:create_tag_changelog + - run: bundle exec rake metadata_lint + - name: "Test that Puppet module can build" + run: "bundle exec pdk build --force" + + create-github-release: + name: Deploy GitHub Release + needs: [ releng-checks ] + runs-on: ubuntu-18.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + clean: true + fetch-depth: 0 + - name: Get tag & annotation info (${{github.ref}}) + id: tag-check + run: | + tag="${GITHUB_REF/refs\/tags\//}" + annotation="$(git for-each-ref "$GITHUB_REF" --format='%(contents)' --count=1)" + annotation_title="$(echo "$annotation" | head -1)" + + echo "::set-output name=tag::${tag}" + echo "::set-output name=annotation_title::${annotation_title}" + + # Prepare annotation body as a file for the next step + # + # * The GitHub Release render the text in this file as markdown + # * The file is needed because :set-output only supports single lines + # * The `perl -pe` removes RPM-style date headers from the CHANGELOG, + # because they don't render well as markdown on the Release page + # + echo "$annotation" | tail -n +2 | \ + perl -pe 'BEGIN{undef $/;} s/\n\* (Mon|Tue|Wed|Thu|Fri|Sat|Sun) .*?\n//smg;' > /tmp/annotation.body + + - name: Create Release + uses: actions/create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ steps.tag-check.outputs.annotation_title }} + body_path: /tmp/annotation.body + draft: false + prerelease: false + + deploy-to-puppet-forge: + name: Deploy PuppetForge Release + needs: [ releng-checks ] + runs-on: ubuntu-18.04 + env: + PUPPETFORGE_API_TOKEN: ${{ secrets.PUPPETFORGE_API_TOKEN }} + FORGE_USER_AGENT: GitHubActions-ForgeReleng-Workflow/0.4.0 (Purpose/forge-ops-for-${{ github.event.repository.name }}) + FORGE_API_URL: https://forgeapi.puppet.com/v3/releases + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + clean: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.5 + bundler-cache: true + - name: Build Puppet module (PDK) + run: bundle exec pdk build --force + - name: Deploy to Puppet Forge + run: | + curl -X POST --silent --show-error --fail \ + --user-agent "$FORGE_USER_AGENT" \ + --header "Authorization: Bearer ${PUPPETFORGE_API_TOKEN}" \ + --form "file=@$(find $PWD/pkg -name ''*.tar.gz'')" \ + "$FORGE_API_URL" diff --git a/.github/workflows/validate_tokens.yml b/.github/workflows/validate_tokens.yml new file mode 100644 index 0000000..0e98981 --- /dev/null +++ b/.github/workflows/validate_tokens.yml @@ -0,0 +1,86 @@ +# Validate API tokens in GitHub Secrets against their respective services +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# PUPPETFORGE_API_TOKEN Required +# GITLAB_API_PRIVATE_TOKEN Required GitLab token (should have `api` scope) +# NO_SCOPE_GITHUB_TOKEN Required GitHub token (should have no scopes) +# GITLAB_SERVER_URL Optional Specify a GL server other than gitlab.com +# The secure vars will be filtered in GitHub Actions log output, and aren't +# provided to untrusted builds (i.e, triggered by PR from another repository) +# +--- +name: 'Manual: Validate API tokens' + +on: + - workflow_dispatch + +jobs: + puppetforge: + name: 'Puppet Forge token authenticates with API' + runs-on: ubuntu-16.04 + env: + PUPPETFORGE_API_TOKEN: ${{ secrets.PUPPETFORGE_API_TOKEN }} + FORGE_USER_AGENT: GitHubActions-ForgeReleng-Workflow/0.4.0 (Purpose/forge-ops-for-${{ github.event.repository.name }}) + steps: + - run: | + curl -sS --fail --silent --show-error \ + --user-agent "$FORGE_USER_AGENT" \ + --header "Authorization: Bearer ${PUPPETFORGE_API_TOKEN:-default_content_to_cause_401_response}" \ + https://forgeapi.puppet.com/v3/users > /dev/null + + gitlab: + name: 'GitLab token has scope for developer' + runs-on: ubuntu-16.04 + env: + GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} + GITLAB_ORG: ${{ github.event.organization.login }} + steps: + - run: | + GITLAB_API_URL="${GITLAB_API_URL:-https://gitlab.com/api/v4}" + curl -I --http1.1 --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN" \ + "${CURL_CMD[@]}" "${GITLAB_API_URL}/groups/$GITLAB_ORG/audit_events" + + github-no-scope: + name: 'No-scope GitHub token has NO scopes' + runs-on: ubuntu-16.04 + env: + GITHUB_ORG: ${{ github.event.organization.login }} + NO_SCOPE_GITHUB_TOKEN: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} + steps: + - name: Test token scopes with curl (expect no scopes) + run: | + if ! response="$(curl -I --http1.0 --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --header "Authorization: token ${NO_SCOPE_GITHUB_TOKEN:-default_content_to_cause_error}" \ + "https://api.github.com/users/${GITHUB_ORG}")" 2>/tmp/x.$$.err; then + echo "::error ::$(cat /tmp/x.$$.err)" + exit 1 + fi + + if ! scopes="$(echo "$response" | grep '^X-OAuth-Scopes:' )"; then + echo "::error ::No X-OAuth-Scopes in response headers!" + echo "::debug ::$response" + exit 1 + fi + scopes="$( echo "$scopes" | strings )" + if echo "$scopes" | awk -F: '{print $2}' | grep -E '\w' ; then + echo "::error ::The NO_SCOPE_GITHUB_TOKEN token has scopes! (${scopes})" + echo "::debug ::${scopes}" + exit 1 + fi + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a836363..4e7059d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,26 +1,40 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to everything above +# the line "# Repo-specific content" +# ------------------------------------------------------------------------------ # The testing matrix considers ruby/puppet versions supported by SIMP and PE: # -# https://puppet.com/docs/pe/2019.0/component_versions_in_recent_pe_releases.html +# https://puppet.com/docs/pe/2019.8/component_versions_in_recent_pe_releases.html # https://puppet.com/misc/puppet-enterprise-lifecycle # https://puppet.com/docs/pe/2018.1/overview/getting_support_for_pe.html # ------------------------------------------------------------------------------ -# Release Puppet Ruby EOL -# SIMP 6.3 5.5.16 2.4.5 TBD*** -# PE 2018.1.7 5.5.16 2.4.5 2020-05 (LTS)*** -# PE 2019.0 6.0 2.5.1 2019-08-31^^^ -# -# *** = Modules created for SIMP 6.3+ are not required to support Puppet < 5.5 +# Release Puppet Ruby EOL +# SIMP 6.4 5.5 2.4.10 TBD +# PE 2018.1 5.5 2.4.10 2021-01 (LTS overlap) +# PE 2019.8 6.18 2.5.7 2022-12 (LTS) --- + stages: - - 'sanity' - 'validation' - 'acceptance' - 'compliance' - 'deployment' variables: + # PUPPET_VERSION is a canary variable! + # + # The value `UNDEFINED` will (intentionally) cause `bundler install|update` to + # fail. The intended value for PUPPET_VERSION is provided by the `pup_#` YAML + # anchors. If it is still `UNDEFINED`, all the other setting from the job's + # anchor are also missing. PUPPET_VERSION: 'UNDEFINED' # <- Matrixed jobs MUST override this (or fail) BUNDLER_VERSION: '1.17.1' + SIMP_MATRIX_LEVEL: '1' + SIMP_FORCE_RUN_MATRIX: 'no' # Force dependencies into a path the gitlab-runner user can write to. # (This avoids some failures on Runners with misconfigured ruby environments.) @@ -38,51 +52,214 @@ variables: # -------------------------------------- .setup_bundler_env: &setup_bundler_env cache: - untracked: true key: "${CI_PROJECT_NAMESPACE}_ruby-${MATRIX_RUBY_VERSION}_bundler" paths: - '.vendor' before_script: - - 'ruby -e "puts %(\n\n), %q(=)*80, %(\nSIMP-relevant Environment Variables:\n\n#{e=ENV.keys.grep(/^PUPPET|^SIMP|^BEAKER|MATRIX/); pad=e.map{|x| x.size}.max+1; e.map{|v| %( * #{%(#{v}:).ljust(pad)} #{39.chr + ENV[v] + 39.chr}\n)}.join}\n), %q(=)*80, %(\n\n)"' - - 'declare GEM_BUNDLER_VER=(-v "~> ${BUNDLER_VERSION:-1.17.1}")' + # Print important environment variables that may affect this job + - 'ruby -e "puts %(\n\n), %q(=)*80, %(\nSIMP-relevant Environment Variables:\n\n#{e=ENV.keys.grep(/^PUPPET|^SIMP|^BEAKER|MATRIX/); pad=((e.map{|x| x.size}.max||0)+1); e.map{|v| %( * #{%(#{v}:).ljust(pad)} #{39.chr + ENV[v] + 39.chr}\n)}.join}\n), %q(=)*80, %(\n\n)"' + + - echo -e "\e[0Ksection_start:`date +%s`:before_script10[collapsed=true]\r\e[0KDiagnostic ruby & gem information" + # Diagnostic ruby & gem information + - 'which ruby && ruby --version || :' + - "[[ $- == *i* ]] && echo 'Interactive shell session' || echo 'Non-interactive shell session'" + - "shopt -q login_shell && echo 'Login shell' || echo 'Not a login shell'" + - 'rvm ls || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script10\r\e[0K" + + # If RVM is available, make SURE it's using the right Ruby: + # * Source rvm (to run in non-login shells) + # * If any $MATRIX_RUBY_VERSION rubies are available, use the latest + # * Otherwise: install & use ${MATRIX_RUBY_VERSION}-head (e.g., latest) + # * ^^ This could be wonky and introduce variations across runners + # * ^^ maybe it should just fail if there is no $MATRIX_RUBY_VERSION installed? + - echo -e "\e[0Ksection_start:`date +%s`:before_script20[collapsed=true]\r\e[0KEnsure RVM & ruby is installed" + - "command -v rvm && { if declare -p rvm_path &> /dev/null; then source \"${rvm_path}/scripts/rvm\"; else source \"$HOME/.rvm/scripts/rvm\" || source /etc/profile.d/rvm.sh; fi; }" + - "command -v rvm && { LATEST_RVM_RUBY_XY=\"$(rvm ls | grep \"$MATRIX_RUBY_VERSION\" | tail -1 | sed -e 's/^.*\\([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\).*$/\\1/g')\"; if [ -z \"$LATEST_RVM_RUBY_XY\" ]; then LATEST_RVM_RUBY_XY=\"${MATRIX_RUBY_VERSION}-head\"; rvm install \"$LATEST_RVM_RUBY\" --no-docs; else echo \"Found RVM Ruby: '${LATEST_RVM_RUBY_XY}'\"; fi; rvm use \"$LATEST_RVM_RUBY_XY\" ; }" + - 'ruby --version || :' + - 'gem list sync || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script20\r\e[0K" + + # Bundle gems (preferring cached > local > downloaded resources) + # * Try to use cached and local resources before downloading dependencies + - echo -e "\e[0Ksection_start:`date +%s`:before_script30[collapsed=true]\r\e[0KBundle gems (preferring cached > local > downloaded resources)" + - 'declare GEM_BUNDLER_VER=(-v "~> ${BUNDLER_VERSION:-2.2.6}")' - 'declare GEM_INSTALL_CMD=(gem install --no-document)' - 'declare BUNDLER_INSTALL_CMD=(bundle install --no-binstubs --jobs $(nproc) "${FLAGS[@]}")' - 'mkdir -p ${GEM_HOME} ${BUNDLER_BIN}' - 'gem list -ie "${GEM_BUNDLER_VER[@]}" --silent bundler || "${GEM_INSTALL_CMD[@]}" --local "${GEM_BUNDLER_VER[@]}" bundler || "${GEM_INSTALL_CMD[@]}" "${GEM_BUNDLER_VER[@]}" bundler' - 'rm -rf pkg/ || :' - 'bundle check || rm -f Gemfile.lock && ("${BUNDLER_INSTALL_CMD[@]}" --local || "${BUNDLER_INSTALL_CMD[@]}" || bundle pristine || "${BUNDLER_INSTALL_CMD[@]}") || { echo "PIPELINE: Bundler could not install everything (see log output above)" && exit 99 ; }' + - echo -e "\e[0Ksection_end:`date +%s`:before_script30\r\e[0K" + + # Diagnostic bundler, ruby, and gem checks: + - echo -e "\e[0Ksection_start:`date +%s`:before_script40[collapsed=true]\r\e[0KDiagnostic bundler, ruby, and gem checks" + - 'bundle exec rvm ls || :' + - 'bundle exec which ruby || :' + - 'bundle show sync || :' + - 'bundle exec gem list sync || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script40\r\e[0K" + + +# Assign a matrix level when your test will run. Heavier jobs get higher numbers +# NOTE: To skip all jobs with a SIMP_MATRIX_LEVEL, set SIMP_MATRIX_LEVEL=0 + +.relevant_file_conditions_trigger_spec_tests: &relevant_file_conditions_trigger_spec_tests + changes: + - .gitlab-ci.yml + - .fixtures.yml + - "spec/spec_helper.rb" + - "spec/{classes,unit,defines,type_aliases,types,hosts,lib}/**/*.rb" + - "{SIMP,data,manifests,files,types,lib,functions}/**/*" + - "templates/**/*.{erb,epp}" + - "Gemfile" + exists: + - "spec/{classes,unit,defines,type_aliases,types,hosts}/**/*_spec.rb" + +.relevant_file_conditions_trigger_acceptance_tests: &relevant_file_conditions_trigger_acceptance_tests + changes: + - .gitlab-ci.yml + - .fixtures.yml + - "spec/spec_helper_acceptance.rb" + - "spec/{helpers,acceptance}/**/*" + - "spec/inspec_*/**/*" + - "{SIMP,data,manifests,files,types,lib,functions}/**/*" + - "templates/**/*.{erb,epp}" + - "Gemfile" + exists: + - "spec/acceptance/**/*_spec.rb" + +# For some reason, the rule regexes stopped matching line starts inside +# $CI_COMMIT_MESSAGE with carets /^/, so we're using /\n?/ as a workaround. +.skip_job_when_commit_message_says_to: &skip_job_when_commit_message_says_to + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: (SKIP MATRIX|MATRIX LEVEL 0)/' + when: never + +.force_run_job_when_commit_message_lvl_1_or_above: &force_run_job_when_commit_mssage_lvl_1_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [123]/' + when: on_success + +.force_run_job_when_commit_message_lvl_2_or_above: &force_run_job_when_commit_mssage_lvl_2_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [23]/' + when: on_success + +.force_run_job_when_commit_message_lvl_3_or_above: &force_run_job_when_commit_mssage_lvl_3_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [3]/' + when: on_success + +# check for $CI_PIPELINE_SOURCE needed because this is combined w/when:changes +.run_job_when_level_1_or_above_w_changes: &run_job_when_level_1_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[123]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.run_job_when_level_2_or_above_w_changes: &run_job_when_level_2_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[23]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.run_job_when_level_3_or_above_w_changes: &run_job_when_level_3_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[3]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.force_run_job_when_var_and_lvl_1_or_above: &force_run_job_when_var_and_lvl_1_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[123]$/' + when: on_success + +.force_run_job_when_var_and_lvl_2_or_above: &force_run_job_when_var_and_lvl_2_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[23]$/' + when: on_success + +.force_run_job_when_var_and_lvl_3_or_above: &force_run_job_when_var_and_lvl_3_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[3]$/' + when: on_success + + + +# SIMP_MATRIX_LEVEL=1: Intended to run every commit +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_1_or_above + - <<: *force_run_job_when_commit_mssage_lvl_1_or_above + - <<: *run_job_when_level_1_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never + +.with_SIMP_SPEC_MATRIX_LEVEL_1: &with_SIMP_SPEC_MATRIX_LEVEL_1 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_commit_mssage_lvl_1_or_above + - <<: *force_run_job_when_var_and_lvl_1_or_above + - <<: *run_job_when_level_1_or_above_w_changes + <<: *relevant_file_conditions_trigger_spec_tests + - when: never + +# SIMP_MATRIX_LEVEL=2: Resource-heavy or redundant jobs +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_2: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_2 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_2_or_above + - <<: *force_run_job_when_commit_mssage_lvl_2_or_above + - <<: *run_job_when_level_2_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never + +.with_SIMP_SPEC_MATRIX_LEVEL_2: &with_SIMP_SPEC_MATRIX_LEVEL_2 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_commit_mssage_lvl_2_or_above + - <<: *force_run_job_when_var_and_lvl_2_or_above + - <<: *run_job_when_level_2_or_above_w_changes + <<: *relevant_file_conditions_trigger_spec_tests + - when: never + +# SIMP_MATRIX_LEVEL=3: Reserved for FULL matrix testing +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_3_or_above + - <<: *force_run_job_when_commit_mssage_lvl_3_or_above + - <<: *run_job_when_level_3_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never -# To avoid running a prohibitive number of tests every commit, -# don't set this env var in your gitlab instance -.only_with_SIMP_FULL_MATRIX: &only_with_SIMP_FULL_MATRIX - only: - variables: - - $SIMP_FULL_MATRIX == "yes" # Puppet Versions #----------------------------------------------------------------------- -.pup_5: &pup_5 +.pup_5_x: &pup_5_x image: 'ruby:2.4' variables: PUPPET_VERSION: '~> 5.0' BEAKER_PUPPET_COLLECTION: 'puppet5' MATRIX_RUBY_VERSION: '2.4' -.pup_5_5_16: &pup_5_5_16 - image: 'ruby:2.4.5' +.pup_5_pe: &pup_5_pe + image: 'ruby:2.4' variables: - PUPPET_VERSION: '5.5.16' + PUPPET_VERSION: '5.5.22' BEAKER_PUPPET_COLLECTION: 'puppet5' - MATRIX_RUBY_VERSION: '2.4.5' + MATRIX_RUBY_VERSION: '2.4' -.pup_6: &pup_6 - image: 'ruby:2.5.1' +.pup_6_x: &pup_6_x + image: 'ruby:2.5' variables: PUPPET_VERSION: '~> 6.0' BEAKER_PUPPET_COLLECTION: 'puppet6' - MATRIX_RUBY_VERSION: '2.5.1' + MATRIX_RUBY_VERSION: '2.5' + +.pup_6_pe: &pup_6_pe + image: 'ruby:2.5' + variables: + PUPPET_VERSION: '6.18.0' + BEAKER_PUPPET_COLLECTION: 'puppet6' + MATRIX_RUBY_VERSION: '2.5' +.pup_7_x: &pup_7_x + image: 'ruby:2.7' + variables: + PUPPET_VERSION: '~> 7.0' + BEAKER_PUPPET_COLLECTION: 'puppet7' + MATRIX_RUBY_VERSION: '2.7' # Testing Environments #----------------------------------------------------------------------- @@ -100,6 +277,7 @@ variables: stage: 'validation' tags: ['docker'] <<: *setup_bundler_env + <<: *with_SIMP_SPEC_MATRIX_LEVEL_1 script: - 'bundle exec rake spec' @@ -107,23 +285,25 @@ variables: stage: 'acceptance' tags: ['beaker'] <<: *setup_bundler_env + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 .compliance_base: &compliance_base stage: 'compliance' tags: ['beaker'] <<: *setup_bundler_env + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 + # Pipeline / testing matrix #======================================================================= -sanity_checks: - <<: *pup_5 +releng_checks: + <<: *pup_5_x <<: *setup_bundler_env - stage: 'sanity' + stage: 'validation' tags: ['docker'] script: - - 'if `hash apt-get`; then apt-get update; fi' - - 'if `hash apt-get`; then apt-get install -y rpm; fi' + - 'command -v rpm || if command -v apt-get; then apt-get update; apt-get install -y rpm; fi ||:' - 'bundle exec rake check:dot_underscore' - 'bundle exec rake check:test_file' - 'bundle exec rake pkg:check_version' @@ -134,60 +314,70 @@ sanity_checks: # Linting #----------------------------------------------------------------------- -pup5-lint: - <<: *pup_5 - <<: *lint_tests +# NOTE: Don't add more lint checks here. +# puppet-lint is a validator, not a parser; it includes its own lexer and +# doesn't use the Puppet gem at all. Running multiple lint tests against +# different Puppet versions won't accomplish anything. -pup6-lint: - <<: *pup_6 +pup-lint: + <<: *pup_6_x <<: *lint_tests # Unit Tests #----------------------------------------------------------------------- -pup5.5.16-unit: - <<: *pup_5_5_16 +pup5.x-unit: + <<: *pup_5_x <<: *unit_tests + <<: *with_SIMP_SPEC_MATRIX_LEVEL_2 -pup6-unit: - <<: *pup_6 +pup5.pe-unit: + <<: *pup_5_pe <<: *unit_tests -# Acceptance tests -# ============================================================================== -pup5.5.16: - <<: *pup_5_5_16 - <<: *acceptance_base - script: - - 'bundle exec rake beaker:suites' +pup6.x-unit: + <<: *pup_6_x + <<: *unit_tests + <<: *with_SIMP_SPEC_MATRIX_LEVEL_2 -pup5.5.16-fips: - <<: *pup_5_5_16 - <<: *acceptance_base - script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites' +pup6.pe-unit: + <<: *pup_6_pe + <<: *unit_tests + +pup7.x-unit: + <<: *pup_7_x + <<: *unit_tests + +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# Everything above the "Repo-specific content" comment will be overwritten by +# the next puppetsync. +# ------------------------------------------------------------------------------ + +# Repo-specific content +# ============================================================================== -pup5.5.16-oel: - <<: *pup_5_5_16 +pup5.x: + <<: *pup_5_x <<: *acceptance_base script: - - 'bundle exec rake beaker:suites[default,oel]' + - 'bundle exec rake beaker:suites[default]' -pup5.5.16-oel-fips: - <<: *pup_5_5_16 +pup6.x: + <<: *pup_6_x <<: *acceptance_base - <<: *only_with_SIMP_FULL_MATRIX script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,oel]' + - 'bundle exec rake beaker:suites[default]' -pup6: - <<: *pup_6 +pup6.pe: + <<: *pup_6_pe <<: *acceptance_base script: - - 'bundle exec rake beaker:suites' + - 'bundle exec rake beaker:suites[default]' -pup6-fips: - <<: *pup_6 +pup7.x: + <<: *pup_7_x <<: *acceptance_base script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites' + - 'bundle exec rake beaker:suites[default]' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 45a04c1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,99 +0,0 @@ -# The testing matrix considers ruby/puppet versions supported by SIMP and PE: -# -# https://puppet.com/docs/pe/2018.1/component_versions_in_recent_pe_releases.html -# https://puppet.com/misc/puppet-enterprise-lifecycle -# https://puppet.com/docs/pe/2018.1/overview/getting_support_for_pe.html -# ------------------------------------------------------------------------------ -# Release Puppet Ruby EOL -# PE 2018.1.7 5.5.16 2.4.5 2020-05 (LTS)*** -# PE 2019.0 6.0 2.5.1 2019-08-31^^^ -# -# *** = Modules created for SIMP 6.3+ are not required to support Puppet < 5.5 - ---- -language: ruby -cache: bundler -sudo: false - -stages: - - check - - spec - - name: deploy - if: 'tag IS present' - -bundler_args: --without development system_tests --path .vendor - -notifications: - email: false - -addons: - apt: - packages: - - rpm - -before_install: - - rm -f Gemfile.lock - - gem install -v '~> 1.17' bundler - -global: - - STRICT_VARIABLES=yes - -jobs: - include: - - stage: check - name: 'Syntax, style, and validation checks' - rvm: 2.4.5 - env: PUPPET_VERSION="~> 5" - script: - - bundle exec rake check:dot_underscore - - bundle exec rake check:test_file - - bundle exec rake pkg:check_version - - bundle exec rake metadata_lint - - bundle exec rake pkg:compare_latest_tag - - bundle exec rake pkg:create_tag_changelog - - bundle exec rake lint - - bundle exec puppet module build - - - stage: spec - rvm: 2.4.5 - name: 'Puppet 5.5 (SIMP 6.3, PE 2018.1)' - env: PUPPET_VERSION="~> 5.5.0" - script: - - bundle exec rake spec - - - stage: spec - name: 'Latest Puppet 5.x' - rvm: 2.4.5 - env: PUPPET_VERSION="~> 5.0" - script: - - bundle exec rake spec - - - stage: spec - name: 'Latest Puppet 6.x' - rvm: 2.5.1 - env: PUPPET_VERSION="~> 6.0" - script: - - bundle exec rake spec - - - stage: deploy - rvm: 2.4.5 - script: - - true - before_deploy: - - "export PUPMOD_METADATA_VERSION=`ruby -r json -e \"puts JSON.parse(File.read('metadata.json')).fetch('version')\"`" - - '[[ $TRAVIS_TAG =~ ^simp-${PUPMOD_METADATA_VERSION}$|^${PUPMOD_METADATA_VERSION}$ ]]' - deploy: - - provider: releases - api_key: - secure: "aMcWn5sujliqskJjS99tMKnhmUqohSH2LdssRdi+hpIWz0OUXjAAB6o36CeCoUmIzCrC1tzxBfYsVHKX98RkyWrMd3YPbvhs93XrK1mHIjUtnc+w8KVZRcK+3xxpmKd1Io3fElqYDzktayVRJStxgBRy5G38PEYkTOvqXE5flCt26jf+UFKP2ogAM6LFhGYaKFE69e4stsSC4oMg3JVfZZzkPt3T7/m/Tai2ZTshTY/p95pWwDuDr7DnE0QZPoOfLn4YaXOobXLOfdo6Kn5VFPwghvEF8n+y8vZ/CEUIB2lAA5kvACkUJI34LMyKdmPaRVAlyPeISwjvln7osgcwSxS8wqLMjzwFjVYR+0qWL2SY+bEU4tA/xHTZHj2V3FwsQa8AwDDIl4CMezIKrtY4H5qn8lcCs57fizJRNmAwxv6uXW3dxGAV3Gbdv1VZdVRmJ/2JpN1RmPYEG1SZd8JdYx4rbAcNVP7Vbd1YwjpL5/VvfPF5OB0xbUbe3rjJ7nIFbWKsUS7cwAIY5pTqmer0NW/v6bua1A4HdsjHgzcZT1LsewAJpR6XRVbpvPhV4s0C2QHrgfxSjNSsSj4GDkHKCbTrD3YzRWpIXtlnPLinpNA7ED+EHSIj4BpdSmlx9NaPt9eIlO2n/gYa86Ieob2cpTtzZFwU2qicYF7WeTLEUGQ=" - skip_cleanup: true - on: - tags: true - condition: '($SKIP_FORGE_PUBLISH != true)' - - provider: puppetforge - user: simp - password: - secure: "YLRv3cKwVbZrberLIfHSSwBpPFv54PLgIXpxYnjKLhuc2zOQmLIPq3ywe7Y8/VH7kxnk9r2w/+qktw5cL0ESeh6msyKnfSKwnHL1nuFj0MysgIl1d1EvfGXOubEICjv/iO5vxS8LNDxcQfDeGYYxmXnqQVw/L+/i6nd2oE8AqKs0Az4uBVjIYy71ilX47dwFFVfbLM1R91zcTI8SU3xRDzAwtOg3RmzDTmufDPgrw5ijy2GATjg4dyro/ck2J92L+PMqH+hwkPAITfVQsEIdhT6fh12n1owYv09foLBqGH9daYu5r8qWlIxr8DAzuQZU6UeRd6IkSnIk0xCj1WBK5sQwBwdEAELLBU8bU44mNBNJ4CScIhoMRHZGw+oMeuzOHxKYByBF6P2ifssHfphq2gweZRuFJKyvDgRdh0q5WBzCucJIWRV+9C/AD5udfp1NbVqaH56AP5FC9HrGQpyz/YjwGK0B6edM6xG31FHbP+PKWRvs1Q4Tczg87W77UNRO2cH2W4a0t2EK9zfbxVykl9iz6v5Muk13R9quRbnT2A7osRd6BAqSofsYYFOWQ1nfaoi54wFA+fLBxNCFpxBmWXOUHxQ8RRARYnkaEv8tyyE490y7ikapMhMu0YxDW/q8zKQYykGsdgA+W/daH6hRpP3nz+klidTiizPma3Cav/8=" - on: - tags: true - condition: '($SKIP_FORGE_PUBLISH != true)' diff --git a/CHANGELOG b/CHANGELOG index a72c628..70bb36f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +* Mon Feb 15 2021 Trevor Vaughan - 1.1.2-0 +- Fixed an issue with running `puppet resource group` +- Added support for Puppet 7 +- Dropped support for Puppet 4 + * Tue Dec 17 2019 Trevor Vaughan - 1.1.1-0 - Execute a single modify command instead of a list of 'add' and 'delete' commands diff --git a/Gemfile b/Gemfile index b466a99..e9a9a7d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,20 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ gem_sources = ENV.fetch('GEM_SERVERS','https://rubygems.org').split(/[, ]+/) +ENV['PDK_DISABLE_ANALYTICS'] ||= 'true' + gem_sources.each { |gem_source| source gem_source } group :test do + puppet_version = ENV['PUPPET_VERSION'] || '~> 6.18' + major_puppet_version = puppet_version.scan(/(\d+)(?:\.|\Z)/).flatten.first.to_i gem 'rake' - gem 'puppet', ENV.fetch('PUPPET_VERSION', '~> 5.5') + gem 'puppet', puppet_version gem 'rspec' gem 'rspec-puppet' gem 'hiera-puppet-helper' @@ -13,17 +23,33 @@ group :test do gem 'puppet-strings' gem 'puppet-lint-empty_string-check', :require => false gem 'puppet-lint-trailing_comma-check', :require => false - gem 'simp-rspec-puppet-facts', ENV.fetch('SIMP_RSPEC_PUPPET_FACTS_VERSION', ['>= 2.4.0', '< 3.0.0'] ) - gem 'simp-rake-helpers', ENV.fetch('SIMP_RAKE_HELPERS_VERSION', ['>= 5.9', '< 6.0']) + gem 'simp-rspec-puppet-facts', ENV['SIMP_RSPEC_PUPPET_FACTS_VERSION'] || '~> 3.1' + gem 'simp-rake-helpers', ENV['SIMP_RAKE_HELPERS_VERSION'] || ['>= 5.11.5', '< 6'] + gem( 'pdk', ENV['PDK_VERSION'] || '~> 1.0', :require => false) if major_puppet_version > 5 + gem 'pathspec', '~> 0.2' if Gem::Requirement.create('< 2.6').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) end group :development do gem 'pry' + gem 'pry-byebug' gem 'pry-doc' end group :system_tests do gem 'beaker' gem 'beaker-rspec' - gem 'simp-beaker-helpers', ENV.fetch('SIMP_BEAKER_HELPERS_VERSION', ['>= 1.17.0', '< 2.0.0']) + gem 'simp-beaker-helpers', ENV['SIMP_BEAKER_HELPERS_VERSION'] || ['>= 1.21.4', '< 2'] +end + +# Evaluate extra gemfiles if they exist +extra_gemfiles = [ + ENV['EXTRA_GEMFILE'] || '', + "#{__FILE__}.project", + "#{__FILE__}.local", + File.join(Dir.home, '.gemfile'), +] +extra_gemfiles.each do |gemfile| + if File.file?(gemfile) && File.readable?(gemfile) + eval(File.read(gemfile), binding) + end end diff --git a/Modulefile b/Modulefile deleted file mode 100644 index 4c6695a..0000000 --- a/Modulefile +++ /dev/null @@ -1,8 +0,0 @@ -name 'onyxpoint-gpasswd' -version '0.1.0' -source 'https://github.com/onyxpoint/puppet-gpasswd' -author 'Trevor Vaughan ' -license 'Apache License, Version 2.0' -summary 'Adds support for :manages_members to the Linux group native type' -description 'The Linux group native type cannot manage local group membership. This remedies the situation by providing the :manages_members attribute and using gpasswd for group modification.' -project_page 'https://github.com/onyxpoint/puppet-gpasswd' diff --git a/lib/puppet/provider/group/gpasswd.rb b/lib/puppet/provider/group/gpasswd.rb index ec65272..eee04b2 100644 --- a/lib/puppet/provider/group/gpasswd.rb +++ b/lib/puppet/provider/group/gpasswd.rb @@ -42,8 +42,18 @@ def addcmd # The self.class.name matches are hard coded so cannot be easily # overridden. def modifycmd(param, value) - cmd = [command(param.to_s =~ /password_.+_age/ ? :password : :modify)] - cmd << flag(param) << value + cmd_type = param.to_s =~ /password_.+_age/ ? :password : :modify + cmd = [command(cmd_type)] + cmd_flag = flag(param) + + # Work around issues with Puppet 6.20+ + # + # Basically, these versions are trying to approach something that works but + # aren't quite there yet and will actually try to remove all of the users + # instead of adding them. + return ['/bin/true'] if (cmd_type == :modify) && (cmd_flag == '-m') + + cmd << cmd_flag << value if @resource.allowdupe? && (param == :gid) cmd << "-o" end @@ -55,6 +65,8 @@ def modifycmd(param, value) def members members_to_set = @resource.parameter('members').shouldorig + return unless members_to_set + @current_members = [] begin current_members = Puppet::Etc.send('getgrnam', name) diff --git a/metadata.json b/metadata.json index 283b767..1251e77 100644 --- a/metadata.json +++ b/metadata.json @@ -1,6 +1,6 @@ { "name": "onyxpoint-gpasswd", - "version": "1.1.1", + "version": "1.1.2", "author": "Trevor Vaughan ", "summary": "Adds support for :manages_members to the Linux group native type", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "requirements": [ { "name": "puppet", - "version_requirement": ">= 4.10.4 < 7.0.0" + "version_requirement": ">= 5.0.0 < 8.0.0" } ] } diff --git a/spec/acceptance/nodesets/default.yml b/spec/acceptance/nodesets/default.yml index ffed805..fd14310 100644 --- a/spec/acceptance/nodesets/default.yml +++ b/spec/acceptance/nodesets/default.yml @@ -6,16 +6,9 @@ end -%> HOSTS: - el6: + el7: roles: - default - - master - - client - platform: el-6-x86_64 - box: centos/6 - hypervisor: <%= hypervisor %> - - el7: platform: el-7-x86_64 box: centos/7 hypervisor: <%= hypervisor %> diff --git a/spec/acceptance/nodesets/oel.yml b/spec/acceptance/nodesets/oel.yml deleted file mode 100644 index 9ca9ece..0000000 --- a/spec/acceptance/nodesets/oel.yml +++ /dev/null @@ -1,31 +0,0 @@ -<% - if ENV['BEAKER_HYPERVISOR'] - hypervisor = ENV['BEAKER_HYPERVISOR'] - else - hypervisor = 'vagrant' - end --%> -HOSTS: - oel6: - roles: - - default - platform: el-6-x86_64 - box: onyxpoint/oel-6-x86_64 - hypervisor: vagrant - - oel7: - platform: el-7-x86_64 - box: onyxpoint/oel-7-x86_64 - hypervisor: vagrant - - oel8: - platform: el-8-x86_64 - box: generic/oracle8 - hypervisor: vagrant - -CONFIG: - log_level: verbose - type: aio -<% if ENV['BEAKER_PUPPET_COLLECTION'] -%> - puppet_collection: <%= ENV['BEAKER_PUPPET_COLLECTION'] %> -<% end -%> diff --git a/spec/acceptance/suites/default/00_default_spec.rb b/spec/acceptance/suites/default/00_default_spec.rb index d286e90..d7e9f8d 100644 --- a/spec/acceptance/suites/default/00_default_spec.rb +++ b/spec/acceptance/suites/default/00_default_spec.rb @@ -34,6 +34,15 @@ hosts.each do |host| context 'with a sorted list of users' do + # When the tests pass with this in place, then upstream puppet has + # achieved functionaly parity +=begin + it 'should whack the module' do + on(host, 'rm -rf /etc/puppetlabs/code/environments/production/modules/gpasswd') + on(host, 'rm -rf `puppet config print vardir`/lib/*') + end +=end + let(:users) { hoopy_froods.sort } # Using puppet_apply as a helper @@ -151,5 +160,11 @@ apply_manifest_on(host, manifest, :catch_changes => true) end end + + context 'ensure that "puppet resource group" still functions' do + it 'should run "puppet resource group" without issue' do + on(host, 'puppet resource group') + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 40c6495..cb5d847 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -91,7 +91,7 @@ def set_hieradata(hieradata) } c.mock_framework = :rspec - c.mock_with :mocha + c.mock_with :rspec c.module_path = File.join(fixture_path, 'modules') c.manifest_dir = File.join(fixture_path, 'manifests') diff --git a/spec/spec_helper_system.rb b/spec/spec_helper_system.rb deleted file mode 100644 index a571239..0000000 --- a/spec/spec_helper_system.rb +++ /dev/null @@ -1,48 +0,0 @@ -# This helper file is specific to the system tests for puppetlabs-firewall -# and should be included by all tests under spec/system -require 'rspec-system/spec_helper' -require 'rspec-system-puppet/helpers' - -# Just some helpers specific to this module -module LocalHelpers - # This helper flushes all tables on the default machine. - # - # It checks that the flush command returns with no errors. - # - # @return [void] - # @todo Need to optionally do the newer tables - # @example - # it 'should flush tables' do - # iptables_flush_all_tables - # end - def iptables_flush_all_tables - ['filter', 'nat', 'mangle', 'raw'].each do |t| - shell "/sbin/iptables -t #{t} -F" do |r| - r.stderr.should be_empty - r.exit_code.should be_zero - end - end - end -end - -include RSpecSystemPuppet::Helpers - -RSpec.configure do |c| - # Project root for the firewall code - proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) - - # Enable colour in Jenkins - c.tty = true - - # Import in our local helpers - c.include ::LocalHelpers - - # This is where we 'setup' the nodes before running our tests - c.before :suite do - # Install puppet - puppet_install - - # Copy this module into the module path of the test node - puppet_module_install(:source => proj_root, :module_name => 'firewall') - end -end diff --git a/spec/unit/provider/group/gpasswd_spec.rb b/spec/unit/provider/group/gpasswd_spec.rb index c743532..ffe51d6 100644 --- a/spec/unit/provider/group/gpasswd_spec.rb +++ b/spec/unit/provider/group/gpasswd_spec.rb @@ -3,11 +3,11 @@ describe Puppet::Type.type(:group).provider(:gpasswd) do before do - described_class.stubs(:command).with(:add).returns '/usr/sbin/groupadd' - described_class.stubs(:command).with(:delete).returns '/usr/sbin/groupdel' - described_class.stubs(:command).with(:modify).returns '/usr/sbin/groupmod' - described_class.stubs(:command).with(:addmember).returns '/usr/bin/gpasswd' - described_class.stubs(:command).with(:modmember).returns '/usr/bin/gpasswd' + allow(described_class).to receive(:command).with(:add).and_return('/usr/sbin/groupadd') + allow(described_class).to receive(:command).with(:delete).and_return('/usr/sbin/groupdel') + allow(described_class).to receive(:command).with(:modify).and_return('/usr/sbin/groupmod') + allow(described_class).to receive(:command).with(:addmember).and_return('/usr/bin/gpasswd') + allow(described_class).to receive(:command).with(:modmember).and_return('/usr/bin/gpasswd') if members @resource = Puppet::Type.type(:group).new(:name => 'mygroup', :members => members, :provider => provider) @@ -25,23 +25,31 @@ it "should add -o when allowdupe is enabled and the group is being created" do @resource[:allowdupe] = :true @resource[:gid] = '555' + + expect(provider).to receive(:execute).with( + '/usr/sbin/groupadd -g 555 -o mygroup', + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) + # This is an unfortunate hack to prevent the parent class from # breaking when we execute everything in gpasswd instead of # returning the expected string to execute. - provider.expects(:execute).with('/bin/true', - :custom_environment => {}, - :failonfail => true, - :combine => true, - :sensitive => false) - - provider.expects(:execute).with( - '/usr/sbin/groupadd -g 555 -o mygroup', - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + expect(provider).to receive(:execute).with( + '/bin/true', + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) + ) provider.create end @@ -49,20 +57,27 @@ describe "on system that feature system_groups", :if => described_class.system_groups? do it "should add -r when system is enabled and the group is being created" do @resource[:system] = :true - provider.expects(:execute).with('/bin/true', - :custom_environment => {}, - :failonfail => true, - :combine => true, - :sensitive => false) + expect(provider).to receive(:execute).with( + '/bin/true', + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) + ) - provider.expects(:execute).with( + expect(provider).to receive(:execute).with( '/usr/sbin/groupadd -r mygroup', - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) provider.create end end @@ -70,20 +85,27 @@ describe "on system that do not feature system_groups", :unless => described_class.system_groups? do it "should not add -r when system is enabled and the group is being created" do @resource[:system] = :true - provider.expects(:execute).with('/bin/true', - :custom_environment => {}, - :failonfail => true, - :combine => true, - :sensitive => false) + expect(provider).to receive(:execute).with( + '/bin/true', + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) + ) - provider.expects(:execute).with( + expect(provider).to receive(:execute).with( '/usr/sbin/groupadd mygroup', - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) provider.create end @@ -92,31 +114,40 @@ describe "when adding additional group members to a new group" do let(:members) { ['test_one','test_two','test_three'] } - it "should pass all members individually as group add options to gpasswd" do - provider.expects(:execute).with('/bin/true', - :custom_environment => {}, - :failonfail => true, - :combine => true, - :sensitive => false) - - provider.expects(:execute).with( + it "should pass all members individually as groupadd options to gpasswd" do + expect(provider).to receive(:execute).with( '/usr/sbin/groupadd mygroup', - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) - - members.each do |member| - provider.expects(:execute).with( - "/usr/bin/gpasswd -a #{member} mygroup", + hash_including( { - :custom_environment => {}, + :custom_environment => anything, :failonfail => false, :combine => true } - ).returns(success_output) + ) + ).and_return(success_output) + + expect(provider).to receive(:execute).with( + include('/bin/true'), + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) + ).at_least(:once) + + members.each do |member| + expect(provider).to receive(:execute).with( + "/usr/bin/gpasswd -a #{member} mygroup", + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) end provider.create @@ -127,19 +158,33 @@ let(:members) { ['test_one','test_two','test_three'] } it "should add all new members" do - Etc.stubs(:getgrnam).with('mygroup').returns( - Struct::Group.new('mygroup','x','99999',[]) + allow(provider).to receive(:execute).with( + include('/bin/true'), + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) ) + + expect(Puppet::Etc).to receive(:getgrnam).with('mygroup').and_return( + Struct::Group.new('mygroup','x','99999',[]) + ).at_least(:once) + @resource[:auth_membership] = :false members.each do |member| - provider.expects(:execute).with( + expect(provider).to receive(:execute).with( "/usr/bin/gpasswd -a #{member} mygroup", - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) end provider.create @@ -153,19 +198,34 @@ it "should add all new members and preserve all existing members" do old_members = ['old_one','old_two','old_three','test_three'] - Etc.stubs(:getgrnam).with('mygroup').returns( - Struct::Group.new('mygroup','x','99999',old_members) + + allow(provider).to receive(:execute).with( + include('/bin/true'), + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) ) + + expect(Puppet::Etc).to receive(:getgrnam).with('mygroup').and_return( + Struct::Group.new('mygroup','x','99999',old_members) + ).at_least(:once) + @resource[:auth_membership] = :false (members | old_members).each do |member| - provider.expects(:execute).with( + expect(provider).to receive(:execute).with( "/usr/bin/gpasswd -a #{member} mygroup", - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) end provider.create @@ -180,20 +240,33 @@ it "should add all new members and delete all, non-matching, existing members" do old_members = ['old_one','old_two','old_three','test_three'] - Etc.stubs(:getgrnam).with('mygroup').returns( - Struct::Group.new('mygroup','x','99999',old_members) + allow(provider).to receive(:execute).with( + include('/bin/true'), + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) ) + expect(Puppet::Etc).to receive(:getgrnam).with('mygroup').and_return( + Struct::Group.new('mygroup','x','99999',old_members) + ).at_least(:once) + @resource[:auth_membership] = :true - provider.expects(:execute).with( + expect(provider).to receive(:execute).with( "/usr/bin/gpasswd -M #{members.join(',')} mygroup", - { - :custom_environment => {}, - :failonfail => false, - :combine => true - } - ).returns(success_output) + hash_including( + { + :custom_environment => anything, + :failonfail => false, + :combine => true + } + ) + ).and_return(success_output) provider.create @@ -207,14 +280,18 @@ it "should add -o when allowdupe is enabled and the gid is being modified" do @resource[:allowdupe] = :true if Gem::Version.new(Puppet.version) >= Gem::Version.new('5.4.0') - provider.expects(:execute).with(['/usr/sbin/groupmod', '-g', 150, '-o', 'mygroup'], { - :combine => true, - :sensitive => false, - :custom_environment => {}, - :failonfail => true - }) + expect(provider).to receive(:execute).with( + ['/usr/sbin/groupmod', '-g', 150, '-o', 'mygroup'], + hash_including( + { + :custom_environment => anything, + :failonfail => true, + :combine => true + } + ) + ) else - provider.expects(:execute).with(['/usr/sbin/groupmod', '-g', 150, '-o', 'mygroup']) + expect(provider).to receive(:execute).with(['/usr/sbin/groupmod', '-g', 150, '-o', 'mygroup']) end provider.gid = 150